
做小区边界数据的时候很多朋友第一反应是“直接去百度地图上把边界抠下来”。真操作起来你会发现这事儿没有想象中那么简单。百度地图的地图瓦片是图片格式想要得到矢量边界就得换一套思路绕过去。我最早是被一个做房产数据分析的项目推到这条路上的需求很明确把目标城市所有住宅小区的轮廓坐标落进数据库供后续做空间关联和可视化。折腾了几个晚上最终跑通了一条还算稳定的链路今天把完整过程拆开讲包括方案怎么选、接口怎么调、坐标怎么处理、数据怎么验证以及我踩过的那些坑。这篇内容适合已经会用Python发HTTP请求、想拿真实地图数据做分析的开发者也适合刚接触地理数据爬取、对百度地图开放平台不太熟的新手。核心思路不复杂用百度地图Web服务API的Place检索接口拿小区POI锚点再用JS API的Boundary接口或者前端页面内部的数据接口拿边界轮廓最后统一做坐标纠偏、数据清洗和格式转换。整个过程里最容易翻车的地方往往不是代码而是对官方API的能力边界理解不到位。1. 需求拆解与总体思路1.1 “爬小区边界”到底在爬什么先明确一个概念百度地图上的“小区边界”在数据层面上不是一个整体文件而是分层存在的。第一层是POI点数据代表小区入口或中心点的经纬度坐标附带名称、地址、类型等属性第二层是面数据也就是边界的闭合多边形由一串有序坐标点组成。POI数据通过官方API就能拿到但面数据并没有一个公开的、一次性下载全量的接口。很多人一开始会想到用爬虫直接抓百度地图Web页面的XHR请求比如拖动地图时network面板里出现的那一堆带boundary字样的JSON。这个思路理论上可行但实际做起来有门槛接口带签名参数、有访问频率限制、返回的数据结构会随版本变化。另一个常见思路是用Selenium模拟操作去画边界但性能太差跑几百个小区就让人崩溃。我最终采用的方案是“官方API为主、前端接口为辅”的组合拳。先用Place检索API把小区POI捞下来拿到每个小区的名称和中心点坐标再以中心点为线索去请求边界相关的数据接口。这个顺序很重要因为大部分边界接口都需要先用名称或坐标定位到具体小区才能返回对应的围栏坐标。1.2 三种主流方案的优劣对比在我动手之前花了点时间把网上能找到的方式都过了一遍列个对比表供你参考方案数据来源优点缺点适用场景Web服务APIPlace检索官方稳定、有配额、返回结构化数据只有POI点没有边界多边形批量位置筛查、作为锚点数据JS API Boundary接口官方能拿到部分边界轮廓主要是行政区划边界小区级支持有限城市、区县边界获取前端页面XHR接口非官方数据全、直接有边界坐标接口会变、有风控、需解析签名小批量、研究性质第三方数据商商业开箱即用、数据完整收费、更新周期不确定生产环境、预算充足我个人的建议是如果是正式项目、要长期更新数据直接考虑采购合规的商业数据如果是个人研究、原型验证或者数据量在几千条量级以内官方API加前端接口的组合足够用。下面我讲的实操就是基于这个组合展开的。1.3 为什么不能直接依赖HTML解析这里要专门说一句因为后台经常有人问“为什么不用BeautifulSoup去解析百度地图的HTML页面”。原因是百度地图的前端页面是重度异步渲染的页面里的地图数据是通过一堆加密的JavaScript动态加载的HTML源码里几乎没有POI和边界信息。即便你用Selenium渲染完整页面拿到的也只是canvas画出来的像素不是结构化坐标数据。所以“爬”的落点不是HTML解析而是数据接口。这一点想通了后面就顺了。用requests直接请求接口、处理JSON响应比模拟浏览器操作要轻量得多也更容易控制频率和出错重试。2. 关键技术方案选型2.1 官方Place检索API怎么用百度地图开放平台的Place检索API是拿POI数据的正道。它支持区域检索、圆形区域检索和矩形区域检索三种方式。对于“小区边界爬取”这个需求我推荐用区域检索按城市和关键词“小区”来拉取POI列表。接口地址是https://api.map.baidu.com/place/v2/search关键参数包括query检索关键词填“小区”region行政区划名称比如“北京市”output返回格式填jsonak你在百度地图开放平台申请的密钥scope填2这样可以返回更详细的POI信息包括uid和locationpage_size每页返回数量最大是20page_num页码从0开始需要注意普通开发者账号的单日配额有限而且每个请求返回的total并不是全量数据量API存在一定的“最多返回前几百条”的隐性限制。这个问题我放在后面“常见问题”里细讲这里先按下不表。我拿到的返回结构大概是这样的{ status: 0, results: [ { name: 某某花园, location: { lat: 39.908823, lng: 116.397470 }, uid: e1f2c9d8a0b1c2d3e4f5, area: 朝阳区, detail_info: { tag: 住宅区;小区 } } ] }这里的lat和lng是百度坐标系BD-09下的坐标不是标准经纬度后面要转换先记住这一点。2.2 边界数据从哪里来拿到POI锚点后下一步就是拿边界。我试过三条路第一条是直接用JS API里的Boundary方法。但实测下来这个接口对行政区划边界支持得很好对小区级别的支持有限很多小区根本查不到对应边界。原因是百度并没有把小区边界做成一个公开的行政边界数据源开放出来。第二条是抓百度地图Web版页面内部使用的接口。在新版百度地图的页面里搜索某个小区后地图上会画出一个围栏这个围栏的坐标就是从某个内部接口返回的。这个接口的URL和参数会随前端版本变化而且返回的数据里带一些校验字段。这条路能走通但需要花时间逆向分析风险是接口随时可能调整。第三条是我最后用的主力方案通过百度地图拾取坐标系统页面的关联接口来获取。这个页面本身是百度官方提供给开发者用的工具用来查询某个地点对应的经纬度和行政区划信息。在查询小区名称后页面会调用一个内部的geocoder接口返回的JSON里包含了地点详情和经纬度但依然没有完整的边界多边形。说到这里我得坦白一个现实百度地图官方渠道并没有一个完全公开的小区边界矢量数据接口。我最终的落地方案是把POI锚点拿到后结合高德地图的行政区划接口和第三方开放数据比如某些城市开放平台发布的住宅小区轮廓数据做补充再用百度地图的坐标拾取器做交叉验证。这样虽然不能保证每个小区都有精确的围栏但锚点准确性、属性完整度都能达到分析需求。2.3 坐标系的坑BD-09与GCJ-02百度地图的坐标系是BD-09高德地图是GCJ-02两者的坐标值会有一个偏移。这个偏移足够让你在地图上展示时错位几十米到几百米不等。如果你后面要做跨平台的数据对比或者想用高德地图做可视化就必须做坐标转换。百度官方提供了一个坐标转换API可以把百度坐标转成其他坐标系https://api.map.baidu.com/geoconv/v1/?coords116.397470,39.908823from5to3ak你的AK这里from5代表源坐标系是百度坐标to3代表目标坐标系是GCJ-02高德坐标。实际使用中我也试过在本地用算法做转换网上有很多公开的坐标偏移算法代码。但官方API的精度更好而且不容易因为算法版本差异出现批次性偏移。建议优先用API转换如果请求量太大再用本地算法兜底。3. 实操完整流程与代码实现3.1 环境准备与密钥申请动手之前先把环境备齐。我用的Python 3.9依赖库只有requests和json后面做数据保存时用geopandas写GeoJSON但这一步不是必须的单机小规模用普通JSON文件足够。百度地图开放平台的密钥申请流程不复杂打开百度地图开放平台官网注册并登录账号。进入控制台创建一个“服务端”类型的应用。应用名称随意填IP白名单留空表示不限制。创建完成后页面会给你一个AK访问密钥这就是后面所有接口请求都需要带的参数。提示如果你在浏览器里看到请求返回APP Referer校验失败之类的错误多半是因为把应用类型选成了“浏览器端”。做服务端爬取一定要选“服务端”。3.2 批量获取小区POI锚点先写一个最基础的请求函数用来翻页拉取某个城市的小区POI数据。import requests import json import time AK 你的AK def search_community(city, page_num): url https://api.map.baidu.com/place/v2/search params { query: 小区, region: city, output: json, scope: 2, page_size: 20, page_num: page_num, ak: AK } resp requests.get(url, paramsparams, timeout10) data resp.json() return data请求返回后把results里的POI字段抽出来保存成列表。这里有个细节百度Place API对同一关键词的搜索最多返回几百条POI不能像想象中那样把整个城市所有小区都拉完。针对这个问题我的折中策略是把城市切成多个检索区域用矩形区域检索去分块拉取。矩形区域检索的用法是换上bounds参数比如bounds39.80,116.20;40.10,116.50表示经纬度范围左下角和右上角的坐标用分号分隔。然后在循环里按固定步长滑动这个矩形窗口把整个城市覆盖一遍。这样能拿到比单纯区域检索多不少的数据。def search_by_bounds(bounds_str, page_num): url https://api.map.baidu.com/place/v2/search params { query: 小区, bounds: bounds_str, output: json, scope: 2, page_size: 20, page_num: page_num, ak: AK } resp requests.get(url, paramsparams, timeout10) return resp.json()这里要注意矩形检索模式下region参数不能同时使用。另外分割矩形时步长不要太大我建议每个窗口覆盖大约0.1度乘以0.1度的范围这样既能控制POI密度又不会让单个窗口返回超过20条的POI把数据冲掉。3.3 爬取边界轮廓的推荐路径拿到POI锚点后就进入最核心的环节为每个小区匹配边界轮廓。这里我尝试过多条路径最终稳定可复现的方案是方案A通过百度页面接口获取指定小区边界在百度地图网页版中搜索一个小区后界面会调用的接口大概形如https://map.baidu.com/?qtextuid你的小区UIDak你的AK这里的uid是Place检索结果里POI自带的那一串字符。这个接口的返回值里有时会包含content字段底下有geo之类的边界信息。但实测下来这个接口的返回结构和可用性非常不稳定有的小区有有的小区没有而且字段经常改。方案B用高德地图API补边界如果目标城市的小区边界以高德数据为主那可以考虑用高德的行政区划查询接口。高德的接口对小区的支持比百度好一些https://restapi.amap.com/v3/geocode/geo?address小区名city城市名key你的高德Key返回的经纬度是GCJ-02坐标可以作为辅助锚点。但高德同样没有直接暴露小区多边形边界的接口。方案C结合开源边界数据做兜底真正能让批量数据落地的方式是把上面拿到的锚点坐标和网络上已有的开源小区轮廓数据做匹配。比如很多城市开放数据平台会发布住宅小区的矢量轮廓格式通常是GeoJSON或Shapefile。把POI锚点与这些数据按名称和空间位置做JOIN能匹配上的就保留轮廓匹配不上的就只保留锚点。跑完整个流程后我的数据表基本是这样的结构字段示例说明name某某花园小区名称bd_lat39.908823百度坐标纬度bd_lng116.397470百度坐标经度gcj_lat39.903824高德坐标纬度gcj_lng116.391234高德坐标经度boundary[[116.3912,39.9038],...]多边形坐标可为空uide1f2c9d8a0b1百度POI唯一标识3.4 批量坐标转换拿到百度坐标后批量转换我用的是官方geoconv接口一次最多转换100个坐标点。写个简单的批量处理函数def batch_convert(coords_list): result [] for i in range(0, len(coords_list), 100): batch coords_list[i:i100] coords_str ;.join([f{lng},{lat} for lat, lng in batch]) url https://api.map.baidu.com/geoconv/v1/ params { coords: coords_str, from: 5, to: 3, ak: AK } resp requests.get(url, paramsparams, timeout10) data resp.json() if data.get(status) 0: for item in data[result]: result.append((item[y], item[x])) time.sleep(0.3) return result注意我代码里的sleep(0.3)这是必须的。虽然单个请求很轻但如果不限速阿克很快就会触发并发限制然后整个IP段都会被临时封掉。爬数据这件事稳比快重要。3.5 输出GeoJSON最后一步把数据结构化成GeoJSON方便在QGIS、Mapbox或者任意地理可视化工具里打开。import json geojson { type: FeatureCollection, features: [] } for item in data_list: feature { type: Feature, properties: { name: item[name], source: baidu }, geometry: { type: Point, coordinates: [item[gcj_lng], item[gcj_lat]] } } geojson[features].append(feature) with open(communities.geojson, w, encodingutf-8) as f: json.dump(geojson, f, ensure_asciiFalse, indent2)如果你拿到了边界轮廓把Point换成Polygon即可geometry: { type: Polygon, coordinates: [[[lng1, lat1], [lng2, lat2], ...]] }4. 数据验证与常见问题4.1 坐标偏移导致的划区错位我第一次把爬下来的POI数据叠加到高德底图上时发现点位全部往东南方向偏移了几百米。排查完后确认就是坐标系不一致的问题。这个问题的典型表现是百度坐标在高德地图上显示时点位会偏向东北方向而高德坐标在百度地图上会偏向西南方向。解决办法就是上面提到的geoconv转换一定要做不要省。4.2 接口请求频率限制百度地图开放平台的默认并发限制是每秒不超过10次超过后接口会返回APP并发超限。我连续踩了两次之后在代码里加了一个限速器也就是统一在每次请求之间sleep 0.2到0.5秒同时增加了请求失败指数退避重试逻辑。import time import random def safe_request(url, params, retries3): for i in range(retries): try: resp requests.get(url, paramsparams, timeout10) data resp.json() if data.get(status) 0: return data elif data.get(status) 302: # IP被临时限制等待较长时间再重试 time.sleep(60) else: time.sleep(1) except Exception as e: print(e) time.sleep(2 * (i 1)) return None这个函数看着简单但在整个流程里帮我节省了大量时间。尤其是status302这个是百度专门用来限流的返回码遇到它就别再疯狂重试了直接等。4.3 Place API返回结果不全前面说过Place API对同一个关键词检索有隐性结果截断。实际表现是无论你怎么翻页总POI数超过一定数量后页数就不变了。这个问题没有完美的绕过方案但用矩形分块可以极大缓解。分块时注意两个坑每个矩形块返回的POI数量若达到20条上限说明这个块太小了可以适当扩大范围再跑一次。若某个矩形块返回的POI数量为0先别急着跳过有可能是这个块内确实没有POI也有可能是接口在你请求时临时抽风。稳妥起见可以重试一次。4.4 边界轮廓经常缺数据对于边界缺失我的处理原则是不硬凑。有些小区在百度地图上本身就没有独立的围栏数据比如一些老式筒子楼或者新建未交付的楼盘。对于这类情况保留锚点和属性信息边界字段留空即可。硬凑一个正方形轮廓出来的做法做出来的地图一眼假分析结果也没有意义。如果项目真的需要完整边界建议转向开源的OpenStreetMap数据。从OSM上可以拿到部分住宅区面数据但覆盖率和更新时效跟百度地图比差不少适合做补充不适合做主力数据源。4.5 数据清洗与去重爬完数据后去重是必备步骤。同一个小区可能被多个矩形窗口重复采集到这时候需要按uid去重。如果换用了不同接口导致uid缺失就用“名称坐标距离”双重条件去重先按名称拼音聚簇再计算簇内坐标两两距离距离小于100米的视为重复。from math import radians, cos, sin, asin, sqrt def haversine(lng1, lat1, lng2, lat2): lng1, lat1, lng2, lat2 map(radians, [lng1, lat1, lng2, lat2]) dlng lng2 - lng1 dlat lat2 - lat1 a sin(dlat/2)**2 cos(lat1) * cos(lat2) * sin(dlng/2)**2 return 2 * 6371 * asin(sqrt(a))这是最常用的距离计算公式我在去重时会配合它做一个简单的循环判断把距离过近的POI合并掉。4.6 数据可视化结果异常处理完的数据用QGIS打开后如果发现点位分布非常奇怪比如大量POI聚集在同一条街上那大概率是矩形检索窗口设计得有问题。我遇到过一种情况某个窗口把城市里的主干道整个圈了进去结果拉回来的POI全是沿街商铺而不是小区。解决方法是增加POI标签过滤只保留detail_info.tag里包含“住宅区”或“小区”的记录。5. 从爬虫到数据服务的实用心得5.1 先想清楚数据要拿来干什么爬数据的过程很有意思但我现在回头看最关键的决策其实是在动手之前做的——你到底要拿这些数据干嘛。如果只是想在地图上点几个点看分布那POI锚点就够用了如果是想做覆盖范围分析、计算每个小区到地铁站的距离那没有边界也能做用中心点代替如果是要做楼盘轮廓对比、拿多边形做空间拓扑运算那就必须找到可靠的边界数据源这决定了你的技术方案完全不同。很多人一上来就盯着“边界”两个字忽略了业务本身对精度的要求。比如我之前做的项目实际上用中心点坐标加一个预设半径就能满足需求根本不需要去死磕边界轮廓白白浪费了几天时间。这个教训还挺深刻的。5.2 数据更新的节奏把握地图POI数据是会变化的新楼盘入市、旧小区改造、物业更名都会影响数据质量。我做数据同步时采用“全量周更、增量日更”的策略每周用Place API把所有目标城市全量扫一遍保证基础数据不丢每天对重点关注的高频区域做一次小范围增量更新。这样既控制了API配额消耗又能保证关键数据的新鲜度。增量更新的做法不复杂就是拿已有的POI名称列表和新爬到的数据做比对凡是新出现的名称或者UID就标记为新增凡是在旧数据里有但新数据里消失的就标记为下架。5.3 不要忽视数据脱敏与合规最后提醒一下爬到的数据里包含小区名称和精确坐标这类数据属于敏感地理信息。如果只是本地研究用途问题不大但如果你要把数据发布出来、做展示、或者提供给第三方使用一定要谨慎。我个人的做法是对外展示时只保留到城市或区县级别的聚合数据不给到具体小区坐标也不做单点精确查询的接口。合规这根弦比技术实现更重要别等出了事再后悔。6. 代码封装做一个可复用的爬取工具6.1 完整工具类结构如果只是跑一次性的脚本上面那些零散的函数足够用了。但要长期维护数据更新我建议把代码封装成一个工具类把API调用、数据存储、日志记录都统一起来。下面是一个简化版的类结构class BaiduCommunityCrawler: def __init__(self, ak): self.ak ak self.session requests.Session() self.base_url https://api.map.baidu.com def search_communities(self, region, page_num0, page_size20): pass def convert_coords(self, coords_list): pass def get_community_detail(self, uid): pass def save_to_geojson(self, file_path): pass6.2 增加持久化存储数据量小的时候存在JSON文件里方便但数据量一旦上万条建议上SQLite。SQLite不需要额外部署服务Python标准库自带非常适合这种单机爬虫场景。import sqlite3 conn sqlite3.connect(communities.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS communities ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, name TEXT, bd_lat REAL, bd_lng REAL, gcj_lat REAL, gcj_lng REAL, boundary TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.commit()数据库表建好后每次爬取开始先查一遍已有数据把已经存在的UID过滤掉这样能省下大量重复请求的配额。6.3 断点续爬的实现爬虫最怕跑了一半崩了。为了应对中断我在代码里加了断点续爬的逻辑每处理完一个矩形窗口就把进度写入一个单独的progress.json文件里下次启动时读取这个文件跳过已经完成的窗口。这样即使程序因为网络波动、断电等原因中断也不需要从头再来。import os def save_progress(completed_windows): with open(progress.json, w, encodingutf-8) as f: json.dump({completed: completed_windows}, f) def load_progress(): if os.path.exists(progress.json): with open(progress.json, r, encodingutf-8) as f: return json.load(f).get(completed, []) return []6.4 多线程是否必要我的答案是个人项目完全没必要。百度地图API的并发限制摆在那里多线程不能帮你突破配额反而容易触发封禁。与其花时间优化并发不如把单线程跑稳、做好重试和断点续爬。如果你的数据量大到单线程跑不完更合适的方案是申请更高的配额或者换用商业数据源而不是在爬虫层面无限压榨接口。7. 边界数据缺失时应考虑的替代方向7.1 城市开放数据平台国内不少城市的数据开放平台会公布住宅小区的基础信息部分城市甚至直接提供GeoJSON格式的小区边界数据。这类数据是政府发布的权威性和准确性都不错但问题是格式不统一、更新不及时而且不是所有城市都有。爬取百度地图的同时可以顺手把目标城市的开放数据平台过一遍能补多少算多少。7.2 OpenStreetMap的building数据OSM上有一部分住宅区residential area的面数据虽然在国内覆盖有限但在一些城市的新区、开发区OSM的数据反而比商业地图更新更勤。通过Overpass API可以按区域条件查询返回的JSON直接就是经纬度坐标。7.3 技术之外的替代思路如果最终拿不到精确边界还有一个“曲线救国”的思路用小区POI锚点做缓冲区分析。具体做法是以POI为中心、按小区规模生成一个圆形或者椭圆的缓冲区作为边界的近似替代。对大多数统计分析场景这种近似已经够用。如果再讲究一点可以把缓冲区半径和小区属性做关联比如户数多的小区半径大一点户数少的小区半径小一点。这个方法虽然学术上不够严谨但做出来的可视化效果和空间分析结果在业务层面是说得通的。8. 最后的一些经验提醒8.1 不要把接口文档当唯一参考百度地图开放平台的文档更新速度跟不上接口的实际变更速度。文档里写的参数实际调用时可能会多出几个默认字段文档里没写的限制实际跑到某个量级就会冒出来。我的习惯是每次调用前把resp.url打出来看一眼确认最终发出去的请求长什么样很多莫名其妙的错误都是参数没有被正确拼接导致的。8.2 一定要做好日志记录写日志不是给机器看的是给你自己看的。我踩过的最大一个坑是数据爬到一半报错但错误信息被吞掉了导致我花了一个小时排查才知道是AK过期了。从那以后我每个关键步骤都会加一行print或者logging把当前页码、窗口范围、返回状态码记录下来。数据出问题时翻日志比猜原因高效太多了。8.3 这个事深入下去还能做什么说实话百度地图小区边界爬取这件事技术难点不算高真正有价值的是后面那一层拿到坐标之后你能不能结合其他数据做出有用的分析。比如把小区POI和房价数据进行空间关联分析不同区域的小区密度与均价的关系或者把小区边界和公交站点数据叠加计算每个小区的公共交通覆盖率甚至可以把多个时间点的POI数据做对比观察城市扩张方向和新区开发节奏。数据本身不会说话但你整理好、分析好、可视化好它就能讲出不少有意思的故事。