在做线下门店选址、商圈分析、竞品调研这类业务场景时,周边商户的POI数据是核心参考依据。手动逐个搜索不仅效率极低,也很难做到全量覆盖。通过高德、百度地图开放平台的官方API,结合Python实现批量自动化采集,可以高效获取标准化的POI数据,大幅提升数据获取效率。
本文基于高德和百度地图的公开API,完整实现周边商户POI的批量采集,覆盖网格切分突破数量限制、分页拉取、自动去重、数据清洗、Excel导出全流程,同时整理了实际开发中常见的坑点与解决方案。
一、前期准备与整体方案
1.1 技术栈说明
只用基础库即可完成,无需复杂依赖:
- requests:负责API接口的HTTP请求
- pandas:负责结构化数据处理、去重与导出
- math:负责经纬度网格切分计算
- time:负责请求间隔控制与限流规避
- json:负责接口返回数据解析
1.2 API密钥申请
两个平台的API都需要先申请密钥,步骤基本一致:
两个平台都有免费调用额度,个人学习和小规模使用基本足够,大批量采集建议购买官方额度,避免触发限流。
1.3 整体实现流程
#mermaid-svg-zMwGRkFeufoijbYg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zMwGRkFeufoijbYg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zMwGRkFeufoijbYg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zMwGRkFeufoijbYg .error-icon{fill:#552222;}#mermaid-svg-zMwGRkFeufoijbYg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zMwGRkFeufoijbYg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zMwGRkFeufoijbYg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zMwGRkFeufoijbYg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zMwGRkFeufoijbYg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zMwGRkFeufoijbYg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zMwGRkFeufoijbYg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zMwGRkFeufoijbYg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zMwGRkFeufoijbYg .marker.cross{stroke:#333333;}#mermaid-svg-zMwGRkFeufoijbYg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zMwGRkFeufoijbYg p{margin:0;}#mermaid-svg-zMwGRkFeufoijbYg .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-zMwGRkFeufoijbYg .cluster-label text{fill:#333;}#mermaid-svg-zMwGRkFeufoijbYg .cluster-label span{color:#333;}#mermaid-svg-zMwGRkFeufoijbYg .cluster-label span p{background-color:transparent;}#mermaid-svg-zMwGRkFeufoijbYg .label text,#mermaid-svg-zMwGRkFeufoijbYg span{fill:#333;color:#333;}#mermaid-svg-zMwGRkFeufoijbYg .node rect,#mermaid-svg-zMwGRkFeufoijbYg .node circle,#mermaid-svg-zMwGRkFeufoijbYg .node ellipse,#mermaid-svg-zMwGRkFeufoijbYg .node polygon,#mermaid-svg-zMwGRkFeufoijbYg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zMwGRkFeufoijbYg .rough-node .label text,#mermaid-svg-zMwGRkFeufoijbYg .node .label text,#mermaid-svg-zMwGRkFeufoijbYg .image-shape .label,#mermaid-svg-zMwGRkFeufoijbYg .icon-shape .label{text-anchor:middle;}#mermaid-svg-zMwGRkFeufoijbYg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zMwGRkFeufoijbYg .rough-node .label,#mermaid-svg-zMwGRkFeufoijbYg .node .label,#mermaid-svg-zMwGRkFeufoijbYg .image-shape .label,#mermaid-svg-zMwGRkFeufoijbYg .icon-shape .label{text-align:center;}#mermaid-svg-zMwGRkFeufoijbYg .node.clickable{cursor:pointer;}#mermaid-svg-zMwGRkFeufoijbYg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zMwGRkFeufoijbYg .arrowheadPath{fill:#333333;}#mermaid-svg-zMwGRkFeufoijbYg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zMwGRkFeufoijbYg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zMwGRkFeufoijbYg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zMwGRkFeufoijbYg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zMwGRkFeufoijbYg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zMwGRkFeufoijbYg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zMwGRkFeufoijbYg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zMwGRkFeufoijbYg .cluster text{fill:#333;}#mermaid-svg-zMwGRkFeufoijbYg .cluster span{color:#333;}#mermaid-svg-zMwGRkFeufoijbYg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zMwGRkFeufoijbYg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zMwGRkFeufoijbYg rect.text{fill:none;stroke-width:0;}#mermaid-svg-zMwGRkFeufoijbYg .icon-shape,#mermaid-svg-zMwGRkFeufoijbYg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zMwGRkFeufoijbYg .icon-shape p,#mermaid-svg-zMwGRkFeufoijbYg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zMwGRkFeufoijbYg .icon-shape .label rect,#mermaid-svg-zMwGRkFeufoijbYg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zMwGRkFeufoijbYg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zMwGRkFeufoijbYg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zMwGRkFeufoijbYg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
采集参数配置
目标区域网格切分
单网格分页调用API
数据解析与字段提取
跨网格数据去重
数据清洗与格式标准化
Excel文件导出
二、核心原理与接口说明
2.1 为什么要做网格切分
这是批量采集中最关键的一个点。地图API的周边搜索接口都有返回数量上限:高德单次周边搜索最多返回1000条结果,百度最多返回760条。如果直接在大范围内调用,核心区域的大量POI会被截断,无法获取全量数据。
最开始做的时候图省事,直接用5公里大半径调用,结果发现核心商圈的店铺缺了近一半,后来才知道有返回上限,踩了不小的坑。解决方案就是把目标区域按经纬度切分成若干个小网格,每个网格的半径控制在返回结果不超过上限的范围内,逐个网格采集后再合并去重,就能实现大范围的全量POI获取。
2.2 高德地图周边搜索API
接口地址:[https://restapi.amap.com/v3/place/around](https://restapi.amap.com/v3/place/around)
核心请求参数:
- key:开发者密钥
- location:中心点经纬度,格式为「经度,纬度」
- keywords:搜索关键词,如“餐饮”“酒店”
- radius:搜索半径,单位米,最大50000
- offset:单页返回数量,最大25
- page:页码
- extensions:返回结果控制,base返回基本信息,all返回详细信息
2.3 百度地图周边搜索API
接口地址:[https://api.map.baidu.com/place/v2/search](https://api.map.baidu.com/place/v2/search)
核心请求参数:
- ak:开发者密钥
- query:搜索关键词
- location:中心点经纬度,格式为「纬度,经度」
- radius:搜索半径,单位米
- page_size:单页返回数量,最大20
- page_num:页码
- output:返回格式,选json
这里踩过第二个坑:两个平台的经纬度顺序是反的,高德是经度在前,百度是纬度在前,一开始没注意的时候返回的结果全是错位的,调了半天才发现是参数顺序的问题。
三、分步代码实现
3.1 配置项与网格切分工具
先定义基础配置和通用工具函数,核心是经纬度网格切分的实现。
import requests
import pandas as pd
import math
import time
# ===================== 配置项 =====================
# 高德配置
AMAP_KEY = "你的高德开发者Key"
# 百度配置
BAIDU_AK = "你的百度开发者AK"
# 采集参数
KEYWORD = "便利店"
# 目标区域:[西南经度, 西南纬度, 东北经度, 东北纬度]
BOUNDS = [116.30, 39.90, 116.50, 40.05] # 示例:北京部分区域
# 网格步长,单位度,数值越小网格越密,数据越全但速度越慢
GRID_STEP = 0.02
# 单网格搜索半径,单位米,配合步长设置
RADIUS = 1500
# 请求间隔,单位秒
SLEEP_TIME = 0.2
网格切分函数,根据输入的区域边界和步长,生成所有网格的中心点经纬度:
def generate_grid_points(bounds, step):
"""
根据区域边界生成网格中心点列表
:param bounds: [西南经度, 西南纬度, 东北经度, 东北纬度]
:param step: 网格步长,单位度
:return: 中心点经纬度列表 [(lon, lat), …]
"""
min_lon, min_lat, max_lon, max_lat = bounds
points = []
lon_steps = math.ceil((max_lon – min_lon) / step)
lat_steps = math.ceil((max_lat – min_lat) / step)
for i in range(lon_steps + 1):
for j in range(lat_steps + 1):
lon = min_lon + i * step
lat = min_lat + j * step
if lon <= max_lon and lat <= max_lat:
points.append((round(lon, 6), round(lat, 6)))
print(f"共生成 {len(points)} 个网格中心点")
return points
3.2 高德地图POI采集实现
封装单页采集函数,再实现批量分页与网格遍历:
def fetch_amap_poi(lon, lat, keyword, radius, page):
"""采集单页高德POI数据"""
url = "[https://restapi.amap.com/v3/place/around](https://restapi.amap.com/v3/place/around)"
params = {
"key": AMAP_KEY,
"location": f"{lon},{lat}",
"keywords": keyword,
"radius": radius,
"offset": 25,
"page": page,
"extensions": "base"
}
try:
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
if data.get("status") != "1":
print(f"高德接口报错:{data.get('info')}")
return []
return data.get("pois", [])
except Exception as e:
print(f"请求失败:{str(e)}")
return []
def collect_amap_all(keyword, bounds, step, radius):
"""高德全量POI采集:网格切分+分页拉取"""
grid_points = generate_grid_points(bounds, step)
all_pois = []
for idx, (lon, lat) in enumerate(grid_points):
print(f"正在采集第 {idx+1}/{len(grid_points)} 个网格:{lon},{lat}")
page = 1
while True:
pois = fetch_amap_poi(lon, lat, keyword, radius, page)
if not pois:
break
all_pois.extend(pois)
# 达到单页最大数量时继续翻页,不足则停止
if len(pois) < 25:
break
page += 1
time.sleep(SLEEP_TIME)
time.sleep(SLEEP_TIME)
print(f"原始采集完成,共获取 {len(all_pois)} 条数据")
return all_pois
3.3 百度地图POI采集实现
百度的逻辑和高德基本一致,重点注意参数名和经纬度顺序的区别:
def fetch_baidu_poi(lon, lat, keyword, radius, page):
"""采集单页百度POI数据"""
url = "[https://api.map.baidu.com/place/v2/search](https://api.map.baidu.com/place/v2/search)"
params = {
"ak": BAIDU_AK,
"query": keyword,
"location": f"{lat},{lon}", # 百度是纬度在前
"radius": radius,
"page_size": 20,
"page_num": page,
"output": "json"
}
try:
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
if data.get("status") != 0:
print(f"百度接口报错:{data.get('message')}")
return []
return data.get("results", [])
except Exception as e:
print(f"请求失败:{str(e)}")
return []
def collect_baidu_all(keyword, bounds, step, radius):
"""百度全量POI采集:网格切分+分页拉取"""
grid_points = generate_grid_points(bounds, step)
all_pois = []
for idx, (lon, lat) in enumerate(grid_points):
print(f"正在采集第 {idx+1}/{len(grid_points)} 个网格:{lon},{lat}")
page = 0
while True:
pois = fetch_baidu_poi(lon, lat, keyword, radius, page)
if not pois:
break
all_pois.extend(pois)
if len(pois) < 20:
break
page += 1
time.sleep(SLEEP_TIME)
time.sleep(SLEEP_TIME)
print(f"原始采集完成,共获取 {len(all_pois)} 条数据")
return all_pois
3.4 数据去重与清洗
网格重叠会产生大量重复数据,需要根据POI唯一ID去重,再做字段标准化:
def clean_poi_data(raw_pois, platform="amap"):
"""POI数据去重与字段清洗"""
if not raw_pois:
return pd.DataFrame()
cleaned = []
for poi in raw_pois:
if platform == "amap":
location = poi.get("location", "").split(",")
item = {
"POI_ID": poi.get("id"),
"名称": poi.get("name"),
"类型": poi.get("type"),
"地址": poi.get("address"),
"经度": location[0] if location else "",
"纬度": location[1] if location else "",
"电话": poi.get("tel"),
"省份": poi.get("pname"),
"城市": poi.get("cityname"),
"区县": poi.get("adname")
}
elif platform == "baidu":
location = poi.get("location", {})
item = {
"POI_ID": poi.get("uid"),
"名称": poi.get("name"),
"类型": poi.get("category"),
"地址": poi.get("address"),
"经度": location.get("lng"),
"纬度": location.get("lat"),
"电话": poi.get("telephone"),
"省份": poi.get("province"),
"城市": poi.get("city"),
"区县": poi.get("area")
}
cleaned.append(item)
df = pd.DataFrame(cleaned)
# 根据官方ID去重,比按名称去重准确得多
before_count = len(df)
df.drop_duplicates(subset="POI_ID", keep="first", inplace=True)
after_count = len(df)
print(f"去重完成:移除 {before_count – after_count} 条重复数据,剩余 {after_count} 条")
df.reset_index(drop=True, inplace=True)
return df
3.5 导出Excel
最后把清洗好的数据导出为Excel,方便后续分析使用:
def export_to_excel(df, keyword, platform):
"""导出为Excel文件"""
filename = f"{platform}_POI_{keyword}.xlsx"
df.to_excel(filename, index=False, sheet_name="POI明细")
print(f"数据已导出至:{filename}")
主函数调用示例:
if __name__ == "__main__":
# 高德采集示例
raw_data = collect_amap_all(KEYWORD, BOUNDS, GRID_STEP, RADIUS)
clean_df = clean_poi_data(raw_data, platform="amap")
export_to_excel(clean_df, KEYWORD, "高德")
# 百度采集示例
# raw_data = collect_baidu_all(KEYWORD, BOUNDS, GRID_STEP, RADIUS)
# clean_df = clean_poi_data(raw_data, platform="baidu")
# export_to_excel(clean_df, KEYWORD, "百度")
四、常见问题与避坑指南
4.1 接口返回权限错误
- 检查密钥是否正确,有没有复制多余空格
- 确认应用类型是否正确,Web服务API不能用浏览器端的密钥
- 查看对应接口是否已经开通权限,部分高级接口需要单独申请
- 检查是否超出当日调用配额,超出后会返回报错
4.2 返回结果数量明显偏少
- 检查网格步长和搜索半径是否匹配,步长太大导致区域覆盖不全
- 确认关键词是否准确,部分分类关键词需要用平台的标准分类词
- 查看是否已经翻到最大页数,高德最多返回1000条,百度最多760条,超出的话必须缩小网格
- 部分偏远区域本身POI数量少,属于正常情况
4.3 坐标偏移问题
两个平台的坐标体系不一样:
- 高德使用GCJ-02(火星坐标系)
- 百度使用BD-09(百度坐标系)
如果需要统一坐标,需要做坐标转换。两个平台都提供了官方坐标转换API,也可以自己实现转换算法,注意不要直接用WGS84的经纬度去调用,会出现几百米的偏移。
4.4 触发限流与封禁
- 严格控制请求频率,不要短时间内大量请求
- 免费额度用完后会返回超限错误,等待次日重置或者购买额度
- 不建议使用多线程并发请求,很容易触发风控,导致密钥临时封禁
- 大批量采集建议使用多个密钥轮询,或者拉长请求间隔
4.5 数据重复或遗漏
- 网格步长和搜索半径要配合设置,半径要略大于网格的对角线,避免出现缝隙
- 重叠区域产生的重复数据通过POI_ID去重即可,不要用名称去重,容易误删同名店铺
- 边界区域建议适当扩大采集范围,再按目标区域裁剪,避免边缘数据遗漏
五、总结与扩展思路
本文基于高德和百度地图的官方API,实现了大范围POI数据的批量采集,通过网格切分的方式解决了单接口返回数量上限的问题,完整覆盖从参数配置、数据拉取、去重清洗到导出的全流程。
如果需要进一步扩展能力,可以尝试这几个方向:

