突破高德地图数量限制:基于四叉树递归的高德 POI 抓取技术
前言
在地理空间数据(GIS)抓取领域,高德地图(Amap)等服务商的 API 接口通常存在严格的返回数量限制。
例如,高德的搜索接口虽然强大,但单次请求即便翻页也存在总量限制(通常为 800-900 条或 20 页)。
对于北京、上海或杭州这样 POI(兴趣点)密集的城市,仅靠单纯的翻页无法获取全量数据。
根据笔者的测试,设置page_num为第9分页的时候POI数据量为25(分页1-8的数据量均为25),然而当修改参数page_num为10的时候则数量量为0。
本文将解读一套基于 Python 与 Shapely 库实现的“动态四叉树递归抓取”方案。该方案完美契合了技术文章《突破高德地图多边形搜索POI数据的数量限制—等大渔网栅格分割法》中的核心思想,通过“分而治之”的策略,实现了对高密度区域的无遗漏采集。
核心痛点与解决思路
痛点:API 的“天花板”
当你请求一个很大的矩形区域(例如整个北京市)时,如果该区域内包含 5000 个“餐饮”POI,API 只会返回前 800 个。
剩下的 4200 个数据会被直接“截断”,无论你如何调整page_num都无法获取。
解决思路:递归分裂(Divide and Conquer)
参考文章中提到的核心算法如下:
- 探测:尝试请求当前区域的数据。
- 判断:如果数据量达到 API 返回上限(溢出),说明当前区域过大,数据过密。
- 分裂:将当前矩形切割成 4 个相等的子矩形(左上、右上、左下、右下)。
- 递归:对这 4 个子区域重复上述步骤,直到数据量未溢出或达到最小面积限制。
代码实现深度剖析
为了实现这一逻辑,我首先需要知道一个城市的轮廓图的坐标。
我已经收集了全国所有城市的轮廓坐标信息,这个不再赘述。
我 在几何计算上引入了Shapely库进行优化。以下是关键模块的解读:
1. 宏观调度:初始网格化 (Initial Grid)
在run方法中,代码并没有直接把整个城市丢进递归函数,而是先做了一次粗粒度的切分:
# 初始网格大小 (千米)INITIAL_GRID_KM=100# ...whilecurr_lon<max_lon:whilecurr_lat<max_lat:# 生成 100km * 100km 的大网格self.fetch_recursive(...)解读:
这是一个很好的工程实践。虽然递归可以处理所有情况,但先将巨大的城市切割成若干个 100km 级别的区块,可以减少单次递归栈的深度,同时方便断点续传和并行处理(如果未来扩展的话)。
2. 核心引擎:递归与溢出检测 (fetch_recursive)
这是整个脚本的灵魂所在。
A. 空间裁剪优化 (Geometry Intersection)
参考文章中提到的一个重要优化点是:不要请求空白区域。我的代码通过shapely实现了这一检查:
# 1. 构造当前网格的 shapely 多边形current_box=box(min_x,min_y,max_x,max_y)# 2. 空间判断:如果当前网格跟城市轮廓完全不相交,直接跳过ifnotself.city_geometry.intersects(current_box):return技术亮点:
在递归过程中,网格会切得非常细。如果不加判断,通过矩形切割会产生大量位于城市边缘但在城市轮廓(GeoJSON)之外的网格(例如海面、荒山)。
通过intersects判断,如果网格在边界外,直接return,极大地节省了 HTTP 请求次数和 API 配额。
B. 溢出判断逻辑 (Overflow Detection)
如何知道 API 是否“截断”了数据?
# 检查是否溢出:如果是第 MAX_PAGE_NUM 页,且数据填满了 PAGE_SIZE 条ifpage==MAX_PAGE_NUMandlen(items)==PAGE_SIZE:is_overflow=Trueprint(f" -> [溢出] 区域{coord_param}数据过多,准备分裂...")break解读:
如果设定最大翻页数为 7,每页 25 条。
- 如果你能翻到第 7 页,且第 7 页还是满的(25条),这在概率上极大概率意味着后面还有第 8 页数据被丢弃了。
- 这时标记
is_overflow = True,触发分裂机制。
C. 四叉树分裂 (Quadtree Splitting)
当检测到溢出时,代码执行经典的分裂操作:
ifis_overflowanddepth<10:mid_x=(min_x+max_x)/2mid_y=(min_y+max_y)/2# 递归调用四个子区域self.fetch_recursive(min_x,mid_y,mid_x,max_y,depth+1)# 左上self.fetch_recursive(mid_x,mid_y,max_x,max_y,depth+1)# 右上self.fetch_recursive(min_x,min_y,mid_x,mid_y,depth+1)# 左下self.fetch_recursive(mid_x,min_y,max_x,mid_y,depth+1)# 右下这种逻辑能确保无论 POI 多么密集(例如市中心的商圈),网格都会自动细分到足够小(例如 50米 x 50米),直到能一次性把该小格子的数据完全拉取下来。
代码与参考文章的异同点
| 特性 | 参考文章思路 | 我的代码实现 | 评价 |
|---|---|---|---|
| 核心算法 | 四叉树递归分割 | 四叉树递归分割 | 一致,抓取核心稳固。 |
| 无效区域过滤 | 提及利用多边形判断 | 使用shapely.intersects | 优秀,利用专业库不仅准确且代码简洁。 |
| 初始处理 | 直接从最大边界开始 | 先按 100km 切分初始网格 | 我的更好,适合处理超大城市或跨城抓取,降低了单个递归树的复杂度。 |
| 坐标系处理 | 未详细描述 | 手动计算经纬度步长 | 够用,但高纬度地区需注意经度变形(代码中已包含cos(lat)修正)。 |
总结
我的码是一份非常标准的高精度 POI 采集脚本。它不仅仅是简单的调用 API,而是通过几何计算和递归算法解决了数据采集中的“有界性”难题。
这种“检测-分裂-再检测”的模式,就像显微镜一样:在稀疏的郊区使用低倍镜(大网格)快速扫过,在密集的市中心自动切换到高倍镜(小网格)细致观察。
这正是爬虫工程中处理分布不均匀数据的最佳实践。
之前分享的几个代码工具,好多人在拿到链接后就立马取关,实在是令人寒心。
因此本篇文章只讲述思路,给出关键部分代码,不提供全部源码。