☰
百度地图API地点检索:Python爬虫高效采集POI数据实践
2026/10/5 1:12:53 网站建设 项目流程

前阵子接了个小活儿,要把某市行政范围内所有酒店的坐标、电话和标签整理成表,还要按类别分目录输出。当时手边已经有一套基于requests的python爬虫脚本,但真去解析地图网页才知道有多痛苦——动态加载、滚动翻页、加密参数,坑一个接一个。后来我把方向换成了直接调百度地图开放平台的地点检索API,返回的是结构化的JSON,字段全是官方定义好的,我只需要处理配额和分页,数据质量比网页解析稳定太多。这篇文章就把整个从百度api爬取地点数据的思路、参数细节、代码实现和踩坑过程完整过一遍,适合正在做python爬虫实战、或者需要批量拿POI数据做分析的开发者参考。

1. 项目整体思路拆解:为什么地图地点数据适合用API“爬”

1.1 先想清楚:这到底是爬虫还是API调用

很多人一听到“爬虫”,脑子里浮现的就是抓网页HTML、逆向JS加密参数那套活儿。但真正做地图这种空间数据采集时,最稳定的路径其实是走官方API。百度地图开放平台的地点检索API本质上是“受控的数据出口”,你按照它的参数协议发一个HTTP GET请求,它就返回一页结构化地点数据,包含名称、经纬度、地址、电话、标签这些字段。

这不是钻空子,而是把官方提供的能力用对地方。百度api的配额机制决定了你不能无限刷,但只要你把请求频率控制在合理范围内,它就是一个非常干净、高效的数据源。我经常跟身边同事说,做爬虫最怕的不是网站反爬强,而是目标数据没有结构化出口,明明有API不接,非要去跟前端渲染死磕,纯属给自己加戏。

用API做地点采集,核心优势有几点:第一,不依赖前端环境,不用维护浏览器内核,一个requests库就够;第二,返回字段稳定,不会今天叫这个名字明天叫那个名字;第三,维度直接给坐标,省的自己去做地理编码和坐标纠偏。缺点是配额有限制,翻页有上限,所以后面我会重点讲怎么用矩形切割绕开800条限制。

1.2 技术选型:行政区划检索还是矩形区域检索

百度地图的地点检索API有几种不同的检索模式:行政区划区域检索、圆形区域检索、矩形区域检索、多边形区域检索。对批量采集来说,最常用的是行政区划和矩形区域这两种。

行政区划检索最简单,参数里传一个region比如“北京市”,加上query和tag,API就会返回该区域内的匹配地点。但这种模式有个硬限制——单次检索最多返回800条数据,也就是page_size最大20、page_num最大19。如果你要爬“酒店”这种热门分类,北京市的POI数量远不止800条,分页根本翻不完,后面的数据就全丢了。

矩形区域检索就是解决这个问题的。你可以把整个城市切成一个一个小网格,每个网格分别去检索,只要网格切得足够小,每个网格里的地点数不超过800,就能把全量数据“拼”出来。这个思路跟切片下载大文件是一个道理,后面我会给出具体的切割逻辑和步长经验值。

2. 前期准备:开发者认证、AK申请与配额门槛

2.1 创建应用并拿到AK

操作路径不复杂:登录百度地图开放平台,完成开发者注册和实名认证,然后在控制台里创建一个“服务端”类型的应用,系统会生成一串AK(访问密钥)。这个AK就是后续所有请求的身份凭证,相当于你进门的门卡。

个人开发者认证通常当天就能通过,不需要企业资质。创建应用的时候会让你选应用类型,做服务端爬取就选“服务端”。如果只是本地写脚本测试,也不用买服务器,直接在本地调就行。有一点要注意:AK生成之后,建议同时开启SN校验功能,开了之后所有请求除了传AK,还要额外带一个sn签名参数。SN签名说白了就是用SK(私钥)对请求参数做一次MD5,防止别人盗用你的AK。虽然测试阶段嫌麻烦可以不开,但只要脚本要长期跑,我建议还是把签名逻辑写进去,网上有现成的生成算法,不算复杂。

2.2 配额解读:个人版到底够不够用

百度地图开放平台的地点检索API是有免费配额的,个人认证和企业认证额度不一样。一般来说,个人应用的日配额在几万次左右,并发上限(QPS)在2到10之间浮动,具体数值以控制台显示为准。这个量级看起来不大,但对爬一个城市某个类目的POI来说,基本够用。

我算过一笔账:如果要把一个城市切成500个网格,每个网格翻40页,那就是2万次请求。分到一天慢慢跑,每次请求间隔控制在0.5秒左右,8小时内跑完完全没问题。如果配额不够,可以申请配额提升,也可以注册多个应用用多个AK轮询,后者操作更快,但要注意别把并发顶得太高,否则得不偿失。

2.3 官方文档快速上手路径

百度地图开放平台的文档入口不太好找,我一般直接搜“百度地图地点检索API”进入对应的文档页。重点看两个部分:一是请求参数说明,二是返回字段说明。参数里面query、tag、region、bounds、output、ak、scope、page_size、page_num这几个是核心,返回字段里uid、name、location、address、detail_info会频繁用到。

官方文档还会给请求示例,默认是浏览器直接访问返回JSON那种,你可以先在浏览器里把URL拼出来看效果,然后把参数原样搬进Python代码里,这样排查问题会快很多。

3. 核心请求链路:从参数到POI数据的完整实现

3.1 接口参数深度解析

地点检索API的完整请求地址是https://api.map.baidu.com/place/v2/search,所有参数都走GET。我最常打的一套参数组合是这样的:

参数取值示例说明
query酒店检索关键词,可以为空
tag美食/酒店/购物分类筛选,和query配合使用
region北京市行政区划检索时的区域名
bounds39.8,116.2;40.1,116.6矩形区域检索,左下角和右上角坐标
outputjson返回格式,强烈建议json
ak你的AK访问密钥
scope2返回详情信息,1或2
page_size20每页条数,最大20
page_num0页码,从0开始,最大19

有一个容易忽略的参数叫city_limit,传true可以限定只返回region行政区内数据,避免检索结果混入周边城市,做城市级地点统计时这个参数几乎必开。

scope=2也非常关键。scope默认是1,只返回基础信息;设成2之后,返回字段会多出来detail_info,里面包含tag、电话、营业时间、评分这些细节。如果你做的是商户数据清洗、电话营销名单整理之类的需求,scope=2是必须的。

3.2 地区编码与矩形切割策略

行政区划检索虽然简单,但800条上限卡得很死,所以我更推荐“先分割、再检索”的组合打法。第一步先用行政区划检索跑一遍,拿到这个category的大致数量级;第二步按矩形把城市切网,逐格检索;第三步用uid去重,拼出完整数据集。

矩形切割的步长是核心调参项,我实测的经验值是这样的:

  • 市区中心人口密集区域,建议步长0.02度左右,约等于2公里网格
  • 城市近郊,步长放到0.05度,约5公里
  • 远郊或农村地区,步长可以拉到0.1度以上
def split_rect(min_lat, min_lng, max_lat, max_lng, step=0.02): lat = min_lat while lat < max_lat: lng = min_lng while lng < max_lng: yield lat, lng, min(lat + step, max_lat), min(lng + step, max_lng) lng += step lat += step

注意bounds参数是左下角坐标和右上角坐标,顺序是纬度,经度;纬度,经度,千万别写反了。经纬度顺序写反会导致检索区域直接跑到海里或者邻国,返回结果要么是0要么数据全部错位。

3.3 翻页请求与字段清洗代码实现

单格翻页的逻辑很简单,就是一个for循环,page_num从0一直加到19。每页拿到结果后,判断一下当前页返回的条数是否等于page_size,如果小于page_size说明数据到底了,再翻下去也没意义,可以直接跳出。

def fetch_poi_by_bounds(bounds, query, ak, max_pages=20): base_url = "https://api.map.baidu.com/place/v2/search" all_data = [] for page in range(max_pages): params = { "query": query, "bounds": bounds, "output": "json", "ak": ak, "scope": "2", "page_size": "20", "page_num": page } resp = requests.get(base_url, params=params, timeout=10) data = resp.json() if data.get("status") != 0: break results = data.get("results", []) all_data.extend(results) if len(results) < 20: break return all_data

拿到原始数据后不要直接入库,先做字段提取和清洗。每个POI对象里核心字段有uid、name、location.lat、location.lng、address、area,详情字段在detail_info里,比如telephone和tag。我通常把它们拍平成一层再落库,这样后续查起来方便。uid就是百度POI的唯一标识,全程去重靠它。同一家店在不同的网格里可能被重复检索到,但uid不变,用它做主键就稳了。

4. 并发设计与数据落盘

4.1 并发度设置:别让高并发把配额打爆

关于爬虫并发设计到底用线程池还是异步,网上吵得挺凶。我的结论很直接:对于百度api这种有严格QPS限制的接口,并发模型不是瓶颈,配额才是。你就算用上分布式爬虫那种架构,把并发开到100,只要触发QPS超限,API立刻给你返回限流错误码,然后再等多久都没用。

我实测下来,个人应用配额下把ThreadPoolExecutor的max_workers设在5到10之间,每个请求之间随机sleep 0.1到0.3秒,是比较稳的组合。如果你用asyncio + aiohttp,效果也差不多,因为限制你的是服务端配额,不是你本机性能。

from concurrent.futures import ThreadPoolExecutor, as_completed import requests import time import random def request_with_retry(params, retries=3): for i in range(retries): try: resp = requests.get("https://api.map.baidu.com/place/v2/search", params=params, timeout=10) data = resp.json() if data.get("status") == 0: return data if data.get("status") in (301, 302, 208): time.sleep(1 + i * 2) continue except requests.exceptions.RequestException: time.sleep(1) return None with ThreadPoolExecutor(max_workers=5) as pool: futures = [] for grid in grids: for page in range(40): params = {...} futures.append(pool.submit(request_with_retry, params)) for future in as_completed(futures): data = future.result() if data: # 处理数据 pass

这里有个细节容易被忽略——生成的矩形网格数量一多,任务总量会暴涨。比如一个1000格的网格,每格最多翻40页,那就是4万个请求,如果你一口气全部提交到线程池,内存里会堆积大量future对象。稳妥的做法是不要一次性把所有任务丢进去,而是外层循环网格,每处理一个网格就提交该网格的20页请求,等这一轮的future都返回完了再处理下一个网格。

4.2 数据清洗、去重与SQLite落盘

爬完的数据最终要存下来,我通常不用CSV,因为地点数据字段多、重复率高,CSV处理起来容易乱。我更推荐直接怼进SQLite,轻量又支持SQL查询,后期筛数据方便。

import sqlite3 conn = sqlite3.connect("poi.db") conn.execute(""" CREATE TABLE IF NOT EXISTS poi ( uid TEXT PRIMARY KEY, name TEXT, lng REAL, lat REAL, address TEXT, area TEXT, tag TEXT, telephone TEXT, query TEXT, crawled_at TEXT DEFAULT (datetime('now', 'localtime')) ) """)

清洗的时候重点做三件事:第一,去空格和换行符,尤其是address和name字段,地图接口偶尔会在后面拼接一些区域代码;第二,把area字段规范化,有的返回“朝阳区”,有的返回“北京市朝阳区”,统一一下才能按区统计;第三,尽量把detail_info里的电话单独拎出来,方便后面打电话或加微信做运营触达。

4.3 失败重试与日志记录不能省

接口跑的时间长了,遇到网络抖动或限流是家常便饭。重试机制必须做,但重试不是盲目重来。我的策略是:遇到status=0的正常返回直接处理;遇到301、302、208这类配额或并发错误,先sleep一段时间再重试,sleep时间按指数退避;遇到网络请求异常,间隔1秒重试;连续重试3次还不行就放弃,把失败的参数写进日志文件。

日志我一般打两份:一份是标准输出,打印实时进度,比如“第1000个网格完成,累计采集POI数:5200条”;另一份写进文件,记录每个网格的起止坐标和结果状态。这样就算中途崩了,也能快速定位是从哪里断的,直接续跑就行。

5. 常见问题与排查实录

5.1 高频错误码对照表

跑百度api爬虫的这几天里,我把最容易遇到的几个错误码整理了一下,纯个人记录,官方文档里也有,但没我这张表直白:

错误码含义解决办法
401AK不存在或非法检查AK是否填错,是否复制多了空格
403无权限访问确认应用是否开通地点检索API服务
208并发超限降低并发数,增加请求间隔
301QPS超限等一会儿再跑,或降低请求频率
302天配额超限换AK、申请提额,或第二天再跑
0请求成功不需要处理

302这个错误如果连续出现,说明当天配额已经打满了,再等也没用。这时候要么换AK轮询,要么调整切割步长,把一些不必要的小网格合并,减少请求量。我经常用后一种办法,因为换AK多了之后管理成本很高,很容易把AK混在代码里传到公共仓库去。

5.2 坐标漂移、数据边界与“明明在范围里却搜不到”

最大的坑其实是返回数据的坐标和范围边界问题。百度使用bd09ll坐标系,跟常见的WGS84或者高德的GCJ02都不一样。如果你拿百度返回的经纬度直接在地图工具里看,位置会有一点点偏移。所以后续做空间分析之前,要先用百度的坐标转换接口,把bd09ll转成你需要的坐标系,别等到画地图的时候才发现店标偏离了真实街道。

另一个坑是“明明这个点在矩形网格里,为什么检索不到”。我排查下来发现,这往往不是API的问题,而是query和tag的组合太苛刻,或者POI被分到了邻近网格。解决办法是网格加一点点重叠,比如步长0.02度,实际边框往外扩0.001度,把边界附近的POI多捞一遍,最后靠uid去重,宁可多请求几次也别漏数据。

还有一类情况是检索返回为0,但你在百度地图App里明明看到有店。这个大概率是tag选错了。百度POI的tag体系分大类和小类,比如“美食”是大类,下面还有“中餐厅”“快餐”“火锅店”这些小类。如果你query为空、只传tag="美食",返回的数据可能没有你预想的那么多。正确的姿势是query和tag同时传,比如query="烤鱼",tag="美食",这样召回率会高不少。

5.3 关于数据合规的一点个人建议

最后想多说一句。百度api提供的地点数据,拿来做个人学习、空间分析、商业选址参考,这些都没问题。但如果你要把这些数据公开到网上做成果展示,或者用来给商户推送营销信息,务必先确认自己的使用场景是否在百度的服务条款允许范围内。爬虫技术本身没有原罪,但使用数据的方式决定了风险高低。建议大家爬下来的数据自己分析用,不要轻易批量打包传播,这个习惯不管做哪个平台的数据采集都适用。

这套方案我后续其实一直在迭代,比如把矩形网格参数做成配置文件、把采集任务定时化、把同一个城市的多个分类放到一个队列里顺序执行。如果你也在做地图POI数据相关的事情,希望这篇能帮你少走点弯路,有更好的切割策略或者去重技巧,也欢迎回来交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询