ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

百度地图API地点检索:Python爬虫高效采集POI数据实践

百度地图API地点检索:Python爬虫高效采集POI数据实践 前阵子接了个小活儿要把某市行政范围内所有酒店的坐标、电话和标签整理成表还要按类别分目录输出。当时手边已经有一套基于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和tagAPI就会返回该区域内的匹配地点。但这种模式有个硬限制——单次检索最多返回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返回格式强烈建议jsonak你的AK访问密钥scope2返回详情信息1或2page_size20每页条数最大20page_num0页码从0开始最大19有一个容易忽略的参数叫city_limit传true可以限定只返回region行政区内数据避免检索结果混入周边城市做城市级地点统计时这个参数几乎必开。scope2也非常关键。scope默认是1只返回基础信息设成2之后返回字段会多出来detail_info里面包含tag、电话、营业时间、评分这些细节。如果你做的是商户数据清洗、电话营销名单整理之类的需求scope2是必须的。3.2 地区编码与矩形切割策略行政区划检索虽然简单但800条上限卡得很死所以我更推荐“先分割、再检索”的组合打法。第一步先用行政区划检索跑一遍拿到这个category的大致数量级第二步按矩形把城市切网逐格检索第三步用uid去重拼出完整数据集。矩形切割的步长是核心调参项我实测的经验值是这样的市区中心人口密集区域建议步长0.02度左右约等于2公里网格城市近郊步长放到0.05度约5公里远郊或农村地区步长可以拉到0.1度以上def split_rect(min_lat, min_lng, max_lat, max_lng, step0.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_pages20): 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, paramsparams, timeout10) 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, retries3): for i in range(retries): try: resp requests.get(https://api.map.baidu.com/place/v2/search, paramsparams, timeout10) 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_workers5) 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 失败重试与日志记录不能省接口跑的时间长了遇到网络抖动或限流是家常便饭。重试机制必须做但重试不是盲目重来。我的策略是遇到status0的正常返回直接处理遇到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数据相关的事情希望这篇能帮你少走点弯路有更好的切割策略或者去重技巧也欢迎回来交流。
返回列表