欢迎光临
我们一直在努力

Python爬虫进阶实战:高德/百度地图API批量采集周边POI商户数据

在做线下门店选址、商圈分析、竞品调研这类业务场景时,周边商户的POI数据是核心参考依据。手动逐个搜索不仅效率极低,也很难做到全量覆盖。通过高德、百度地图开放平台的官方API,结合Python实现批量自动化采集,可以高效获取标准化的POI数据,大幅提升数据获取效率。

本文基于高德和百度地图的公开API,完整实现周边商户POI的批量采集,覆盖网格切分突破数量限制、分页拉取、自动去重、数据清洗、Excel导出全流程,同时整理了实际开发中常见的坑点与解决方案。

一、前期准备与整体方案

1.1 技术栈说明

只用基础库即可完成,无需复杂依赖:

  • requests:负责API接口的HTTP请求
  • pandas:负责结构化数据处理、去重与导出
  • math:负责经纬度网格切分计算
  • time:负责请求间隔控制与限流规避
  • json:负责接口返回数据解析

1.2 API密钥申请

两个平台的API都需要先申请密钥,步骤基本一致:

  • 进入对应地图开放平台官网,注册开发者账号
  • 创建应用,选择「Web服务」类型的密钥
  • 开通「周边搜索」相关的接口权限
  • 复制生成的密钥填入代码配置项
  • 两个平台都有免费调用额度,个人学习和小规模使用基本足够,大批量采集建议购买官方额度,避免触发限流。

    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数据的批量采集,通过网格切分的方式解决了单接口返回数量上限的问题,完整覆盖从参数配置、数据拉取、去重清洗到导出的全流程。

    如果需要进一步扩展能力,可以尝试这几个方向:

  • 多关键词批量采集,一次性获取多个品类的POI数据
  • 加入坐标转换功能,统一两个平台的数据坐标体系
  • 接入数据库存储,实现增量采集与定期更新
  • 结合可视化库,生成POI分布热力图
  • 扩展多边形区域采集,适配不规则的商圈或行政区边界
  • 赞(0)
    未经允许不得转载:171主机测评 » Python爬虫进阶实战:高德/百度地图API批量采集周边POI商户数据
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址