☰
高德地图爬虫实战:POI采集、API调用与并发限流详解
2026/10/6 13:06:40 网站建设 项目流程

最近又有一批朋友来问我高德地图爬虫怎么搞。坦白说,这项目不算多难,但也真不是网上随便抄个requests脚本就能跑通的“xx行爬虫”。高德的数据分散在好几个入口:Web服务API、网页端的前端渲染、瓦片图层、移动端离线包,不同入口的抓法完全不在一个量级。很多人一上来就requests乱抓,抓到一堆HTML发现全是JS渲染,然后心态就崩了。

这篇文章我按自己实际跑过的项目来复盘,先讲高德数据的分层和方案选型,再拆解requests请求里的参数细节、限流策略、并发设计,最后给一套可以直接落地的POI采集代码,外加我踩过的各种坑和排查思路。适合刚起步的数据工程师、准备做城市分析的Python选手,也想玩高德Loca组件做可视化的GIS爱好者。

1. 先想清楚:高德爬虫到底要爬哪一层

1.1 三层数据模型:API、网页、瓦片

做高德爬虫,第一个要搞明白的事是:你要的数据到底在哪儿。我习惯把高德地图的数据入口拆成三层。

第一层是Web服务API。这个返回的是结构化JSON,字段非常规整,有名称、坐标、地址、电话、类型、行政区划等,适合做POI采集、地理编码和路径规划。它数据质量最稳,也是最推荐普通人去碰的一层。

第二层是网页端数据。打开高德地图网页版,里面很多信息是前端渲染出来的,比如店面的营业时长、评分、评论数,这些字段API里不一定给全,只能在网页上抠。代价是你要处理JS渲染、SVG地名、字体反爬这些东西,维护成本明显上升。

第三层是瓦片数据。瓦片是地图底图的最小单元,栅格瓦片就是一张张PNG图片,矢量瓦片则是一段段的二进制协议。通常用于自建底图、离线地图、车机导航的数据基础。瓦片下载按xyz编号有固定规律,适合离线包场景,不适合做POI采集。

这三类数据的技术栈差异很大,很多人翻车是因为根本没想清楚自己要哪层,直接把三个混在一起抓,最后什么都抓不干净。

1.2 方案选型:为什么把官方API放在首位

我自己的原则是:能用官方API解决的,绝对不碰网页爬虫。

高德开放平台对个人开发者很友好,注册后创建应用就能拿key,Web服务接口比如地点搜索、地理编码、逆地理编码都是直接可用的。相比解析网页,API返回的数据是官方整理好的,字段有保证,坐标已经处理成GCJ-02火星坐标系,省去大量清洗工作。

网页爬虫什么时候才需要上?我遇到过一种场景:客户要某商圈所有餐饮店面的近期评分和评论数,但高德POI接口里不返回评分,只有网页详情页里有。这种时候才需要配合Playwright或Selenium,模拟浏览器打开详情页,提取评分、评论摘要。要注意,网页端的风控比API明显严格,短时间高频访问会触发验证码甚至IP封锁,所以我会在网页爬虫前先问自己:这几个字段真的没有其他渠道能拿到吗?

瓦片下载则是另一个目的——离线底图。比如你要给森林防火系统做一张无缝大底图,规划区域内所有瓦片拼起来用,这时候API帮不上忙,必须走瓦片下载。后面4.4小节我会专门讲瓦片URL的规律。

1.3 关于API收费与Key安全,提前给你打预防针

网上搜“高德地图api收费坑人”的人不少,但说实话,很多坑是自己踩出来的。

最常见的情况是:个人开发者key直接用在生产环境,测试阶段没事,上线后日配额很快耗尽,系统默认自动切换到了付费流量包,月底账单一看傻眼了。还有一种情况是key写在前端代码里被挖出来,被别人拿去刷接口,配额度直接被打爆。

我的建议是三条。第一,key一定要放服务端,前端要调接口就先请求自己的后端,由后端转发,key永远不要出服务器。第二,在开放平台控制台给key设置IP白名单,只允许你的服务器IP调用,这是性价比最高的风控手段。第三,定期看配额监控,设置用量预警,配额快用完时主动暂停爬虫而不是让它自动扣费。

高德的key权限可以单独关闭服务。比如你只做地点搜索,就把逆地理编码、路径规划这些用不到的服务全部关掉,减少被恶意刷的风险面。

2. 核心细节解析:请求、限流与并发设计

2.1 requests请求与返回结构:先跑通再优化

高德Web服务API的请求方式很简单,就是一个HTTP GET,参数通过query string传递。但有几个细节值得注意。

第一,用requests的params传参,不要自己拼URL。params会自动处理URL编码,中文关键词、特殊字符都不会出错,而手拼URL很容易在城市名带“市辖区”这类词时翻车。

第二,高德返回体里有个status字段,1代表成功,0代表失败。失败时要看infocode字段来判断具体原因。很多新手只判断HTTP状态码200就以为成功了,结果中间夹着一堆失败JSON,数据清洗时才发现不对。

第三,如果开通了数字签名,params里要加sig参数。签名规则是:把除sig外的所有参数按key的字典序排序,拼成key=value&...的字符串,末尾拼接安全密钥,然后做MD5得到sig。这个不复杂,但容易漏。

import requests import hashlib import time # 基础参数 params = { "key": "你的key", "keywords": "便利店", "city": "杭州", "citylimit": "true", "offset": 25, "page": 1, "extensions": "all" } # 如果开通了签名功能,追加sig sorted_params = sorted(params.items()) raw_string = "&".join(f"{k}={v}" for k, v in sorted_params) params["sig"] = hashlib.md5( (raw_string + "你的安全密钥").encode("utf-8") ).hexdigest() # 发送请求 resp = requests.get( "https://restapi.amap.com/v3/place/text", params=params, timeout=10 ) data = resp.json() # 正确姿势:先判断业务状态,再处理数据 if data.get("status") == "1": print("总数:", data.get("count")) for poi in data.get("pois", []): print(poi["name"], poi["location"], poi["adname"]) else: print("失败:", data.get("infocode"), data.get("info"))

注意,requests请求一定要设置timeout,不然某个IP卡住会让整个爬虫停在那里。建议配合一个重试装饰器,遇到超时或5xx错误时自动退避重试,重试2~3次后再把异常抛出来记录。

2.2 限流与代理IP:遇到封禁先从频率找原因

高德API层相对来讲没有特别复杂的反爬,核心就是配额和并发控制。如果你在返回里看到“CUQPS_HAS_EXCEEDED_THE_LIMIT”或者类似提示,意思是QPS超限了,这时候第一反应不是去换IP,而是把并发降下来。

很多做网页爬虫的朋友习惯性先买代理池,其实在POI爬虫场景里,代理IP真的排不上大用场。API是按key鉴权的,你换再多IP,key的配额该不够还是不够。代理IP只在网页端爬虫里能派上用场——用来规避同IP高频访问触发的风控。

如果你确实需要代理,requests里只需要加一个参数:

proxies = { "http": "http://user:pass@你的代理地址:端口", "https": "http://user:pass@你的代理地址:端口" } resp = requests.get(url, params=params, headers=headers, proxies=proxies, timeout=10)

但我的实际经验是,免费代理质量很差,延迟高、掉线快,在你只需要几百个POI的场景下,加代理反而是负优化。我自己做城市级POI采集时,通常不用代理,而是严格控制QPS,比如每秒钟只发1~2个请求,配合指数退避重试,实测下来很少触发风控。

一句话总结:先降频率,后换代理,不要本末倒置。

2.3 并发设计怎么选:协程、线程池还是分布式

搜“并发设计到底哪个好”的朋友大概率是被网上各种方案搞晕了。我直接给结论:高德POI爬虫是典型的IO密集型任务,瓶颈在网络等待,CPU几乎不参与计算。

协程方案比如asyncio+aiohttp,理论效率最高,一个小脚本就能开几百个并发。但高德服务端有QPS限制,并发开大了只会得到一堆限流错误,协程的优势根本发挥不出来。所以对大多数场景,我推荐用Python自带的concurrent.futures.ThreadPoolExecutor,简单、可控,出问题还好调试。

线程池方案的核心技巧是限速。我会用信号量控制同时进行的请求数量,再加一个简单的速率控制器。比如最多同时4个请求,每秒钟最多10次调用,这样既稳定又不会突破配额上限。

import threading import time from concurrent.futures import ThreadPoolExecutor class RateLimiter: def __init__(self, max_calls_per_sec): self.interval = 1.0 / max_calls_per_sec self.lock = threading.Lock() self.last_time = 0.0 def wait(self): with self.lock: now = time.time() wait_time = self.last_time + self.interval - now if wait_time > 0: time.sleep(wait_time) self.last_time = time.time()

分布式爬虫比如Scrapy+Redis方案,适合城市级甚至全国级的超大批量数据抓取,多台机器横向扩展,还要处理任务分发、去重、配额拆分,一套做下来工程成本不小。爬一个小城市全量POI,真不用上分布式。我见过不少团队一上来就搭分布式,结果数据还没爬完,先在各种中间件上花了两周时间。

我的选择逻辑很朴素:小规模用for循环,中等规模用线程池加信号量,遇到跨省、千万级数据量再加分布式。

3. 实操记录:从零写一个可用的POI爬虫

3.1 准备工作:创建应用、申请Web服务Key

这条路我走过很多次,流程其实特别简单:

  1. 打开高德开放平台官网,注册账号。
  2. 进入控制台,选择“应用管理”,创建一个新应用,类型选“出行”。
  3. 在应用下添加Key,服务平台选“Web服务”。
  4. 生成key后,建议立即在安全设置里配置IP白名单,把你服务器或本机的出口IP加进去。
  5. 到“配额管理”里确认你要用的接口有额度,个人开发者的免费额度对测试和中小规模项目来说基本够用。

申请完不要急着写代码,先在控制台里把人机验证、服务开关这些设置过一遍,后面能省很多事。

3.2 第一版:单线程分页抓取POI

高德地点搜索接口的部分逻辑是这样的:每次请求可以设置offset(每页记录数,最大25)和page(页码),返回的count是符合条件的总数。实际能拿到的最大页数有限制,如果翻了几十页还没到底,就说明这个范围的数据量太大,需要用行政区划去拆分。

单线程版本很好写,核心就是一个while循环,不断翻页,直到当前页返回的POI数量不足一页,或者页码超过上限。每页请求之间加0.5秒延时,别把自己手写爬虫变成对服务端的攻击。

import requests import time import hashlib KEY = "你的key" SECRET = "你的安全密钥" def get_poi(city, keyword, page=1): params = { "key": KEY, "keywords": keyword, "city": city, "citylimit": "true", "offset": 25, "page": page, "extensions": "all" } sorted_params = sorted(params.items()) raw = "&".join(f"{k}={v}" for k, v in sorted_params) params["sig"] = hashlib.md5((raw + SECRET).encode("utf-8")).hexdigest() for attempt in range(3): try: resp = requests.get( "https://restapi.amap.com/v3/place/text", params=params, timeout=10 ) data = resp.json() if data.get("status") == "1": return data elif attempt < 2: time.sleep(2 * (attempt + 1)) else: print("请求失败:", data.get("info")) return None except requests.exceptions.RequestException: time.sleep(2 * (attempt + 1)) return None def crawl_city_poi(city, keyword): all_pois = [] page = 1 while page <= 40: data = get_poi(city, keyword, page) if not data: break pois = data.get("pois", []) all_pois.extend(pois) if len(pois) < 25: break page += 1 time.sleep(0.5) return all_pois if __name__ == "__main__": result = crawl_city_poi("杭州", "便利店") print("总共抓取:", len(result))

这一段代码看起来不多,但已经包含了签名、重试、分页三个核心逻辑。先跑通它,再考虑优化。

3.3 第二版:ThreadPoolExecutor并发提速

单线程跑一个小城市还行,要是跑几十个城市,那就有点慢悠悠了。我在第二版里把并发加上去。

思路是这样:每个城市作为一个独立任务丢进线程池,每个任务内部仍然按页码顺序抓取,只是不同城市之间并行。把线程池大小设为4~6,不会太激进,也不容易触发限流。

from concurrent.futures import ThreadPoolExecutor, as_completed def crawl_one_city(city): pois = crawl_city_poi(city, "便利店") return city, pois cities = ["杭州", "宁波", "温州", "嘉兴", "湖州", "绍兴", "金华", "衢州", "舟山", "台州", "丽水"] results = {} with ThreadPoolExecutor(max_workers=5) as executor: future_map = {executor.submit(crawl_one_city, c): c for c in cities} for future in as_completed(future_map): city, pois = future.result() results[city] = pois print(f"{city} 完成,共 {len(pois)} 条")

实测下来,10个城市跑完的速度比单线程快了4倍左右。这里有个容易被忽略的问题:如果每个城市内部也有多个关键词要爬,那么把“城市+关键词”作为任务粒度更合适,否则单个城市内部还是串行的,扩展性受限。

另外,线程数不是开得越多越好。我在某个一起跑项目的朋友那边见过max_workers=50的写法,结果跑了三分钟就被高德限流了,越往后越是各种重试。线程池大小和限速必须一起考虑,高德API对单Key的QPS有硬限制,超过了就是排队重试,吞吐量反而下降。

3.4 第三版:按adcode细分,避免数据遗漏

当你爬一个超大目标比如“餐饮”时,会碰到一个很尴尬的情况:接口返回的count明明有5000多条,但每页25条翻到40页就再也翻不动了。这是高德服务端的限制——单个查询条件下最大返回数量约1000条。超过这个数量,必须把查询范围拆小。

拆分的标准做法是使用adcode,也就是行政区划编码。比如杭州下面有上城区、拱墅区、西湖区、滨江区等等,每个区都有独立的adcode。把城市级查询改成区县级查询,每个区县的数据量基本能控制在1000条以内,不会丢数据。

def get_districts(city_adcode): params = { "key": KEY, "keywords": None, "subdistrict": 2, "extensions": "base" } # 调用行政区划查询接口 url = "https://restapi.amap.com/v3/config/district" resp = requests.get(url, params={**params, "keywords": city_adcode}, timeout=10) data = resp.json() districts = [] for district in data.get("districts", []): for sub in district.get("districts", []): districts.append((sub["name"], sub["adcode"])) return districts

拿到adcode列表后,抓取循环就变成两层:外层遍历区县,内层翻页抓取。配合并发线程池,每个区县一个任务,效率更高,也不容易丢数据。这是我做全量POI采集时最常用的方式。

4. 进阶常见问题与排查实录

4.1 接口状态码与业务code对照速查

爬高德API,最让人头大的就是返回了一堆错误码。我把高频错误整理成一张表,排查的时候直接对着看:

infocode含义常见原因处理建议
10001key不正确或过期key写错、被删除检查控制台key,重新生成
10002用户不是白名单IP白名单限制加IP白名单或关闭白名单
10003QPS超限请求太密集降并发、加sleep
10021签名错误sig参数没算对按字典序排序后拼密钥再MD5
10024用户id无效账号异常或欠费检查账号状态
10033配额已用完日配额耗尽等次日或升级套餐
20000请求参数非法参数类型或值不对仔细检查参数格式

实际排查时,我先看请求是否进入了“成功”分支,再看infocode,最后才看异常堆栈。很多人一看到报错就以为是代码问题,其实七成是配额和签名的问题。

4.2 坐标偏移:GCJ-02与WGS84互转

有个搜索热词叫“苹果手机位置错误”,做LBS应用的朋友大概率遇到过:用高德API拿到的坐标,放到苹果自带地图上显示,位置偏了几百米。

这不是手机坏了,是坐标系不匹配。高德地图用的是GCJ-02火星坐标系,苹果自带地图和大多数海外工具用的是WGS84国际坐标系。两者之间有个非线性偏移,需要做转换。

import math def gcj02_to_wgs84(lng, lat): a = 6378245.0 ee = 0.00669342162296594323 dlat = _transform_lat(lng - 105.0, lat - 35.0) dlng = _transform_lng(lng - 105.0, lat - 35.0) radlat = lat / 180.0 * math.pi magic = math.sin(radlat) magic = 1 - ee * magic * magic sqrtmagic = math.sqrt(magic) dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * math.pi) dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi) return lng - dlng, lat - dlat

我的经验是:爬下来的数据如果不确定对方要什么坐标系,统一存WGS84,用的时候再转GCJ-02。这样可以避免数据在不同地图之间来回切的时候出现二次偏移。百度地图还单独用BD-09坐标系,在高德数据转百度的场景下,要先GCJ-02再BD-09,两步不能省。

4.3 小程序跳转高德App与Loca组件

做爬虫之外的场景,很多人也会遇到“微信小程序跳转到高德App”的需求。这东西不复杂,但容易卡在协议上。高德提供了URI API,用一个跳转链接就能唤起高德App并自动发起导航。

https://uri.amap.com/navigation?to=终点经度,终点纬度,name=目的地&mode=car&coordinate=gaode

在小程序里用wx.openLocation也能直接拉起地图选点,但如果要跳转到高德App做完整导航,就得用上面的URL。注意coordinate参数要传gaode,如果你手里是WGS84坐标,得先转成GCJ-02再拼链接。

再聊聊Loca组件。爬下来的POI数据如果只是存个CSV,价值不大,配一张好的可视化图才是能力的放大。高德Loca是个专门做海量数据可视化的JavaScript库,基于AMap JS API运行,支持热力图层、蜂巢图、飞线图。用的时候先引AMap,再引Loca:

<script src="https://webapi.amap.com/maps?v=2.0&key=你的key"></script> <script src="https://cache.amap.com/loca/dist/loca.min.js"></script>

把爬到的POI按经纬度喂进Loca的DataSet,几万条点数据渲染成热力图基本不卡。这份数据可视化呈现,对我来说才是爬虫项目的终点。

4.4 SVG爬虫与瓦片下载的正确姿势

网页端高德地图里,地名有时是SVG矢量字体或纹理渲染的,直接去抓HTML文本你会发现抓不到完整文字,这就是SVG爬虫的难点。处理思路是分析页面的字体接口和SVG映射文件,把字符id和渲染结果对应起来。这个工程量不小,如果目标数据能从API拿到,尽量不要走这条路。

瓦片下载相对直接。高德栅格瓦片的URL规律网上能找到,我自己验证过:

https://webrd0{1-4}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=7

x、y、z分别代表瓦片坐标和缩放级别。下载前先搞清楚你要用的是XYZ还是TMS规则,两种规则在y轴方向是相反的。高德这套用的是XYZ规则,y从顶部开始。写个小脚本按经纬度范围算出瓦片编号范围,然后并发下载,下载完用PIL或者gdal拼接成一张大图,就能得到一张离线底图。

还有朋友会搜“高德红绿灯算法”这类词,我说句实话:红绿灯倒计时和路况预测是高德基于海量浮动车轨迹、路口信控数据训练出来的结果,不是爬虫能拿到现成参数的东西。爬虫能做到的极限,是基于历史路况接口做延迟预测分析,和红绿灯算法的深度不是一个量级。

关于离线包,如果你是为了做离线地图,与其抓瓦片再自己拼,不如直接走官方离线地图下载通道,安卓、iOS都有成熟的离线包方案。自己抓瓦片只适合做特定区域的深色底图、行业定制图层这类官方不支持的自定义场景。还有人在搜“高德地图精简版”这类App改造话题,那属于另外一条完全不同的路线,这里不展开。


最后聊点个人体会。高德爬虫这个项目,真正花时间的不是写代码,而是把“数据边界”想清楚——你到底需要哪一层的数据、覆盖多大范围、目标坐标系是什么,这些问题没定下来就急急忙忙写代码,后面基本都要返工。

还有一个小技巧分享给准备跑全量城市数据的朋友:正式跑之前,先把所有行政区划的adcode列表导出来,按每个区县跑一条测试请求,确认“单区县数据量没有超过1000条上限”。这个前置验证能帮你精确估算总请求量和耗时,避免爬到一半才发现数据被截断,从头再来。

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

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

立即咨询