在做线下门店选址、商圈分析、竞品调研这类业务场景时,周边商户的POI数据是核心参考依据。手动逐个搜索不仅效率极低,也很难做到全量覆盖。通过高德、百度地图开放平台的官方API,结合Python实现批量自动化采集,可以高效获取标准化的POI数据,大幅提升数据获取效率。
本文基于高德和百度地图的公开API,完整实现周边商户POI的批量采集,覆盖网格切分突破数量限制、分页拉取、自动去重、数据清洗、Excel导出全流程,同时整理了实际开发中常见的坑点与解决方案。
一、前期准备与整体方案
1.1 技术栈说明
只用基础库即可完成,无需复杂依赖:
- requests:负责API接口的HTTP请求
- pandas:负责结构化数据处理、去重与导出
- math:负责经纬度网格切分计算
- time:负责请求间隔控制与限流规避
- json:负责接口返回数据解析
1.2 API密钥申请
两个平台的API都需要先申请密钥,步骤基本一致:
- 进入对应地图开放平台官网,注册开发者账号
- 创建应用,选择「Web服务」类型的密钥
- 开通「周边搜索」相关的接口权限
- 复制生成的密钥填入代码配置项
两个平台都有免费调用额度,个人学习和小规模使用基本足够,大批量采集建议购买官方额度,避免触发限流。
1.3 整体实现流程
二、核心原理与接口说明
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 配置项与网格切分工具
先定义基础配置和通用工具函数,核心是经纬度网格切分的实现。
importrequestsimportpandasaspdimportmathimporttime# ===================== 配置项 =====================# 高德配置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网格切分函数,根据输入的区域边界和步长,生成所有网格的中心点经纬度:
defgenerate_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)foriinrange(lon_steps+1):forjinrange(lat_steps+1):lon=min_lon+i*step lat=min_lat+j*stepiflon<=max_lonandlat<=max_lat:points.append((round(lon,6),round(lat,6)))print(f"共生成{len(points)}个网格中心点")returnpoints3.2 高德地图POI采集实现
封装单页采集函数,再实现批量分页与网格遍历:
deffetch_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()ifdata.get("status")!="1":print(f"高德接口报错:{data.get('info')}")return[]returndata.get("pois",[])exceptExceptionase:print(f"请求失败:{str(e)}")return[]defcollect_amap_all(keyword,bounds,step,radius):"""高德全量POI采集:网格切分+分页拉取"""grid_points=generate_grid_points(bounds,step)all_pois=[]foridx,(lon,lat)inenumerate(grid_points):print(f"正在采集第{idx+1}/{len(grid_points)}个网格:{lon},{lat}")page=1whileTrue:pois=fetch_amap_poi(lon,lat,keyword,radius,page)ifnotpois:breakall_pois.extend(pois)# 达到单页最大数量时继续翻页,不足则停止iflen(pois)<25:breakpage+=1time.sleep(SLEEP_TIME)time.sleep(SLEEP_TIME)print(f"原始采集完成,共获取{len(all_pois)}条数据")returnall_pois3.3 百度地图POI采集实现
百度的逻辑和高德基本一致,重点注意参数名和经纬度顺序的区别:
deffetch_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()ifdata.get("status")!=0:print(f"百度接口报错:{data.get('message')}")return[]returndata.get("results",[])exceptExceptionase:print(f"请求失败:{str(e)}")return[]defcollect_baidu_all(keyword,bounds,step,radius):"""百度全量POI采集:网格切分+分页拉取"""grid_points=generate_grid_points(bounds,step)all_pois=[]foridx,(lon,lat)inenumerate(grid_points):print(f"正在采集第{idx+1}/{len(grid_points)}个网格:{lon},{lat}")page=0whileTrue:pois=fetch_baidu_poi(lon,lat,keyword,radius,page)ifnotpois:breakall_pois.extend(pois)iflen(pois)<20:breakpage+=1time.sleep(SLEEP_TIME)time.sleep(SLEEP_TIME)print(f"原始采集完成,共获取{len(all_pois)}条数据")returnall_pois3.4 数据去重与清洗
网格重叠会产生大量重复数据,需要根据POI唯一ID去重,再做字段标准化:
defclean_poi_data(raw_pois,platform="amap"):"""POI数据去重与字段清洗"""ifnotraw_pois:returnpd.DataFrame()cleaned=[]forpoiinraw_pois:ifplatform=="amap":location=poi.get("location","").split(",")item={"POI_ID":poi.get("id"),"名称":poi.get("name"),"类型":poi.get("type"),"地址":poi.get("address"),"经度":location[0]iflocationelse"","纬度":location[1]iflocationelse"","电话":poi.get("tel"),"省份":poi.get("pname"),"城市":poi.get("cityname"),"区县":poi.get("adname")}elifplatform=="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)returndf3.5 导出Excel
最后把清洗好的数据导出为Excel,方便后续分析使用:
defexport_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数据的批量采集,通过网格切分的方式解决了单接口返回数量上限的问题,完整覆盖从参数配置、数据拉取、去重清洗到导出的全流程。
如果需要进一步扩展能力,可以尝试这几个方向:
- 多关键词批量采集,一次性获取多个品类的POI数据
- 加入坐标转换功能,统一两个平台的数据坐标体系
- 接入数据库存储,实现增量采集与定期更新
- 结合可视化库,生成POI分布热力图
- 扩展多边形区域采集,适配不规则的商圈或行政区边界