尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

百度地图小区边界爬取实战:从接口解析到GeoJSON导出

百度地图小区边界爬取实战:从接口解析到GeoJSON导出 开头先交代一下背景我是个常年跟地图数据打交道的爬虫工程师年初接了个物业数据整理的需求甲方开口就要全市几千个小区的边界数据还要能导入GIS系统里看。我翻遍百度地图开放平台的文档地点检索只给中心点坐标行政区划接口也只到区县一级压根没有“小区边界”这么细的能力。后来折腾了两天绕了个弯子从百度地图前端项目里把那层边界数据直接“抠”了出来最后导出了标准的矢量文件。这篇博文就是想把这套“百度地图小区边界爬取”的完整思路、代码和坑记录下来给同样被边界数据卡住的朋友一条可复现的路。项目本身说简单也不简单难点不在HTTP请求而在“边界数据藏在哪”“返回的boundary怎么解析”“坐标怎么转换”这三件事。适合谁看呢做智慧城市、物业可视化、房产分析、地图数据整理或者纯粹想学怎么从Web应用里提取结构化数据的人这篇应该都能用到。全程我用Python跑通代码量不大依赖也就requests一个库剩下全是思路。1. 先搞清楚边界数据藏在哪官方API到底给不给小区边界搞爬虫最忌讳一上来就写代码连数据源在哪都没想明白。我先花时间把百度地图开放平台的能力盘了一遍结论就是官方对外开放的接口确实没有一个能直接返回小区的多边形边界。1.1 官方能力盘点地点检索、行政区划、地理编码百度地图开放平台的Web服务API大致分几类地点检索Place API、行政区划区域检索、地理编码、路线规划、逆地理编码等。跟“小区边界”沾边的只有两个地点检索按关键词在某个城市内搜索POI能返回小区名称、地址、经纬度但那个经纬度是POI的展示中心点。小区可能很大一个点没法表示边界轮廓甲方要的是能落到GIS里算面积的多边形。行政区划区域检索网上有人介绍过用它可以查省、市、区县、乡镇的边界数据返回地理范围和多边形坐标。但试过就知道它的粒度最多到乡镇街道再往下到小区、楼盘这种“非行政单位”基本拿不到。所以可以断言官方API在“小区边界”这个粒度上处于盲区。不是说没有这个数据——百度地图前端在搜索小区、渲染小区轮廓时明明有边界线在页面上只是这个能力没有走对外开放的Web服务API拿而是由前端调用了一套“对应用开放”的内部数据接口。1.2 三个常见的“弯道方案”为什么都不靠谱知道官方没有之后很多人第一反应是换路子我总结三个最常见的坑第一去爬地图瓦片。就是把网页上显示的地图切成几万张小图片下载下来再想办法矢量化。做过几次这种活零件便宜人力贵图片拼接、坐标对齐、边缘提取每一步都容易翻车而且最终拿到的还是栅格图不是坐标串离“边界”差得远。第二拿中心点画圆代替边界。确实能出图但小区轮廓千奇百怪一个圆或者一个矩形完全不能用面积计算误差太大甲方一眼就能看出来是糊弄事。第三买第三方商业数据。数据商的边界数据一方面是贵另一方面更新未必及时一个小项目就为几百个小区掏几万块钱不划算。最终我发现网页端搜索小区时地图上会“框”出一个轮廓这个轮廓位置的数据就是我要的东西。于是方案定了构造请求从百度地图前端正在使用的数据接口里把边界坐标串拉出来。这一步是整个项目最关键的分水岭。2. 技术选型与前置知识请求接口而不是渲染页面确定数据源在网页前端后还有一个选择要做是模拟浏览器点来点去还是直接HTTP请求后端接口。我的答案是后者。2.1 为什么用 Pythonrequests而不是Selenium凡是搞过抓取的人都懂Selenium这类浏览器自动化方案“能做所有事但做任何事都慢”。启动浏览器、加载地图JS、等待瓦片渲染一个小区可能要卡好几秒爬几千个小区得跑到天荒地老。而且地图页面动不动就触发轨迹检测、滑块验证反而更容易被封。真正正确的做法是打开浏览器开发者工具F12切到Network面板筛选XHR请求然后在页面里搜索一个小区名。观察几次请求会发现有个别请求特别“眼熟”它返回的JSON里包含一个叫boundary或者类似的字段值是一长串用分号隔开的经纬度。这意味着后台根本不用计算它把边界数据已经算好了只是序列化成字符串发给前端。你要做的事情很简单用Python模拟这个请求拿到同样的JSON自己解析。省去了页面渲染的性能损耗也绕开了操作浏览器的复杂性。技术上就是requests一次GET请求几毫秒完成一个小区比Selenium快几个数量级。2.2 坐标系与坐标转换的基础搞地图数据躲不开坐标系问题这个不提前说清楚后面解析完发现边界飘到海里别怪我没提醒。百度地图对外输出的大部分坐标不是标准GPS坐标而是“百度坐标系”简称BD09。它是在火星坐标系GCJ02基础上又做了一次二次加密偏移跟你在GPS设备里拿到的WGS84坐标相比往往偏移几十米到几百米。所以爬下来的边界坐标直接用在大部分地图或GIS软件里都会发现边界整体往东北方向偏出去一截。如果要跟天地图、GPS采集数据、谷歌地球数据叠加就必须做坐标转换。转换算法本身是公开的我后面会给完整Python实现BD09先转GCJ02GCJ02再迭代转WGS84。这个步骤不能省也别想着让GIS软件自动纠偏很多软件不认BD09。2.3 环境准备与数据落地的格式约定这次项目只需要Python3环境核心依赖就一个requests解析JSON用标准库json就够了。我习惯再用os、time、random这几个标准库做文件写入和频率控制不需要装重型框架。落地格式建议直接写GeoJSON。为什么不是CSV小区边界本质是面数据CSV只能存点或者存一长串文本后续导入QGIS、ArcGIS、Leaflet都麻烦。GeoJSON是Web GIS的事实标准一个文件里能同时存名称、城市、地址、边界坐标系免转换直接拖进地图工具。另外再落一份CSV用来快速预览名称和经纬度范围算是工作习惯。3. 核心实现定位小区、拉取边界、解析导出这章是实操重头戏我会按三步讲清楚整个流程最后给一份能直接改改就用的完整脚本。每一步我都会解释为什么这么写免得你复制了代码但不会应对变化。3.1 第一步用地点检索确定小区精确位置虽然前面吐槽过地点检索只给中心点但中心点并不是没用——它是后续请求边界数据的前置参数。你要先确定这个小区在地图库里的唯一标识通常就是经纬度坐标或POI id。我们可以调用官方对外开放的地点检索接口这一步合法且稳定目的是拿到目标小区的地图坐标和名称标准化形式。请求URL是https://api.map.baidu.com/place/v2/search核心参数如下query要搜的小区名比如“中粮海景壹号”tag固定为“小区”能过滤掉同名餐饮、写字楼region城市名比如“上海”或者城市adcodeoutputjsonak你在百度地图开放平台申请的密钥scope2返回更详细的POI信息page_size10同一城市同名小区可能有多个多拿几条备选Python代码很简单import requests AK 你的百度地图开放平台AK def search_place(city, keyword): url https://api.map.baidu.com/place/v2/search params { query: keyword, tag: 小区, region: city, output: json, ak: AK, page_size: 10, scope: 2, } resp requests.get(url, paramsparams, timeout10) data resp.json() if data.get(status) ! 0: raise RuntimeError(f地点检索失败: {data.get(message)}) return data.get(results, [])返回的results数组里每个元素有name、location.lng、location.lat。选取规则我一般这样优先选名称完全匹配且location附近有小区边界的那个如果多个同名小区按城市区划判断比如用户只关心浦东南路那个。确定了中心点之后把lng和lat记下来下一步要把它拿出来当参数。3.2 第二步构造边界请求并处理返回结构中心点有了接下来是边界数据。这部分我建议你打开百度地图网页版在开发者工具的Network面板里过滤“boundary”关键字搜索一个小区名观察返回数据里包含边界坐标的那个请求。不同时期的前端项目使用的接口域名、参数名都会变但交互模式几乎一致——GET请求返回JSONJSON里有边界坐标串。我不建议直接把某个固定URL硬编码进脚本因为这类接口一旦变动你的代码马上失效。正确做法是把抓包看到的URL和参数配置化。演示代码里我用一个占位URL你把它替换成自己抓包得到的实际地址即可BOUNDARY_API https://your-boundary-api-url.example.com/getboundary def get_boundary(lng, lat, name): params { location: f{lng},{lat}, name: name, ak: AK, output: json, } resp requests.get(BOUNDARY_API, paramsparams, timeout15) data resp.json() return data需要注意返回结构。我见过几种典型形态封装函数时要兼容最外层是JSON对象边界字段名可能是boundary、bounds、boundaryDataboundary字段值可能直接是一串经度,纬度用分号连接也可能在不同JSON层里嵌套了多条边界比如复杂小区由多个地块组成个别接口返回的boundary带前缀控制信息例如编码名称或颜色配置需要用分隔符切开后再解析经纬度所以拿到响应后别急着split先把JSON打印出来看结构这一步永远是第一步。3.3 第三步把边界字符串解析成可用坐标核心解析逻辑是这套流程里最需要写健壮的地方。boundary的典型字符串长这样121.491,31.230;121.492,31.231;121.493,31.230;121.492,31.229分号分隔一个点逗号分隔经纬度。但复杂情况还包含多环、多地块有些返回里会用竖线“|”分隔多个闭合环有些会在坐标串前面带“colorxxx;”之类的前缀。我的解析函数写成这样def parse_boundary(boundary_str): if not boundary_str: return [] # 1. 去掉可能存在的控制前缀找到第一个纯经纬度分号组合的起点 import re match re.search(r(\d\.\d,\d\.\d), boundary_str) if match: boundary_str boundary_str[match.start():] # 2. 按竖线拆分成多个环如果没有竖线只有一个环 rings [] for part in boundary_str.split(|): part part.strip().strip(;) if not part: continue coords [] for point in part.split(;): point point.strip() if not point: continue lng, lat point.split(,) coords.append([float(lng), float(lat)]) if len(coords) 3: # 少于3个点无法构成面直接丢弃 rings.append(coords) return rings这段解析有几个细节值得说正则定位纯坐标起点是为了防前缀竖线拆环是为了防多地块小区长度小于3的环直接丢是为了防脏数据污染。每次地图接口返回的boundary我都建议用这个函数过一遍你能省掉后续一大半数据清洗时间。3.4 完整脚本一条命令跑完把上面三段逻辑串起来再加一个GeoJSON导出函数就是一套完整脚本。GeoJSON的Polygon结构是一个三维数组外环是由若干坐标点构成的闭合数组坐标顺序遵循“右旋规则”即外环应该是逆时针方向不过多数GIS软件对方向不敏感后续可以统一纠正。import json import time import random import requests AK 你的百度地图开放平台AK BOUNDARY_API https://your-boundary-api-url.example.com/getboundary def search_place(city, keyword): url https://api.map.baidu.com/place/v2/search params { query: keyword, tag: 小区, region: city, output: json, ak: AK, page_size: 10, scope: 2, } resp requests.get(url, paramsparams, timeout10) data resp.json() if data.get(status) ! 0: raise RuntimeError(f地点检索失败: {data.get(message)}) return data.get(results, []) def get_boundary(lng, lat, name): params { location: f{lng},{lat}, name: name, ak: AK, output: json, } resp requests.get(BOUNDARY_API, paramsparams, timeout15) return resp.json() def parse_boundary(boundary_str): import re if not boundary_str: return [] match re.search(r(\d\.\d,\d\.\d), boundary_str) if match: boundary_str boundary_str[match.start():] rings [] for part in boundary_str.split(|): part part.strip().strip(;) if not part: continue coords [] for point in part.split(;): point point.strip() if not point: continue lng, lat point.split(,) coords.append([float(lng), float(lat)]) if len(coords) 3: rings.append(coords) return rings def build_geojson(features): return { type: FeatureCollection, features: features, } def main(): city 上海 keyword 中粮海景壹号 results search_place(city, keyword) if not results: print(未找到该小区) return result results[0] lng result[location][lng] lat result[location][lat] name result[name] data get_boundary(lng, lat, name) boundary_str data.get(boundary, ) rings parse_boundary(boundary_str) if not rings: print(未解析到边界) return feature { type: Feature, properties: { name: name, city: city, center: [lng, lat], }, geometry: { type: Polygon, coordinates: rings, }, } geojson build_geojson([feature]) with open(f{name}.geojson, w, encodingutf-8) as f: json.dump(geojson, f, ensure_asciiFalse, indent2) print(f已导出: {name}.geojson共 {len(rings)} 个环) time.sleep(random.uniform(0.5, 1.5)) if __name__ __main__: main()这个脚本已经能跑通单个小区的边界导出。规模化处理的时候把所有小区名放在一个列表里外部套一层循环每次循环之间睡眠0.5到1.5秒避免请求频率过高。批量过程中建议每次导出后把结果追加写进一个汇总GeoJSON避免程序中断丢进度。4. 数据验证与清洗坐标转换和可视化检查爬完数据不等于项目交付我见太多人卡在“爬到了坐标但不知道坐标准不准”这一步。边界数据落地后一定要做验证和清洗否则给你老板看的时候边界偏出一公里场面会很尴尬。4.1 导出GeoJSON并修复常见脏数据解析出来的环在写文件前建议做几项清洗闭合检查多边形首尾两点应该相同如果解析回来的数据首尾不一致要手动把第一个点追加到末尾否则很多GIS软件会报拓扑错误。自相交检查部分复杂小区的boundary可能包含洞内部有湖水、绿地这类数据在简单Polygon结构里表达不了建议先按闭合环导出后续在QGIS里人工处理。精度控制boundary字符串里经纬度可能给到六位甚至更多小数浮点数本身没影响但文件体积会偏大。统一保留六位小数大概是0.1米精度完全足够还能减小文件体积。GeoJSON文件内部建议显式声明坐标系信息crs: { type: name, properties: { name: urn:ogc:def:crs:EPSG::4547 } }但说实话很多在线工具不认这个字段更多时候是坐标转换后在文件命名或属性里备注“WGS84”或“BD09”字样。如果你不转坐标务必在属性表里加一个coord_type字段并填BD09省得过两个月自己都不记得这份数据底细。4.2 坐标转换BD09到WGS84如果边界数据要跟GPS采集的数据或者其他标准地图叠加就需要坐标转换。这里给出一个经过项目验证的Python实现思路分为两步BD09先转GCJ02再把GCJ02转成WGS84。网上流传的wandergis/coordTransform_py库就是这个思路我这里把它拆开讲方便你按需调整。import math x_pi 3.14159265358979324 * 3000.0 / 180.0 a 6378245.0 ee 0.00669342162296594323 def bd09_to_gcj02(lng, lat): x lng - 0.0065 y lat - 0.006 z math.sqrt(x * x y * y) - 0.00002 * math.sin(y * x_pi) theta math.atan2(y, x) - 0.000003 * math.cos(x * x_pi) gcj_lng z * math.cos(theta) gcj_lat z * math.sin(theta) return [gcj_lng, gcj_lat] def _transform_lat(lng, lat): ret -100.0 2.0 * lng 3.0 * lat 0.2 * lat * lat 0.1 * lng * lat 0.2 * math.sqrt(abs(lng)) ret (20.0 * math.sin(6.0 * lng * math.pi) 20.0 * math.sin(2.0 * lng * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(lat * math.pi) 40.0 * math.sin(lat / 3.0 * math.pi)) * 2.0 / 3.0 ret (160.0 * math.sin(lat / 12.0 * math.pi) 320 * math.sin(lat * math.pi / 30.0)) * 2.0 / 3.0 return ret def _transform_lng(lng, lat): ret 300.0 lng 2.0 * lat 0.1 * lng * lng 0.1 * lng * lat 0.1 * math.sqrt(abs(lng)) ret (20.0 * math.sin(6.0 * lng * math.pi) 20.0 * math.sin(2.0 * lng * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(lng * math.pi) 40.0 * math.sin(lng / 3.0 * math.pi)) * 2.0 / 3.0 ret (150.0 * math.sin(lng / 12.0 * math.pi) 300.0 * math.sin(lng / 30.0 * math.pi)) * 2.0 / 3.0 return ret def gcj02_to_wgs84(lng, lat): 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) mglat lat dlat mglng lng dlng return [lng * 2 - mglng, lat * 2 - mglat] def bd09_to_wgs84(lng, lat): gcj bd09_to_gcj02(lng, lat) return gcj02_to_wgs84(gcj[0], gcj[1])转换时遍历坐标串里所有点逐点转换最后重新封装成环。注意经纬度顺序前面函数返回的数组我按[lng, lat]习惯写别搞混了。4.3 用QGIS等工具检查边界是否贴合清洗和转换完成后一定要做可视化检查。最省事的办法是把GeoJSON拖进QGIS再叠加官方底图或者天地图卫星图肉眼观察边界是否贴合小区真实轮廓。我实际操作中遇到过三种情况完全贴合说明坐标本来就准直接交付。整体偏移但形状不变说明坐标系没转对检查是不是BD09直接当WGS84用了。解决办法是走一遍坐标转换或者反过来从WGS84转GCJ02。部分小区缺边界或边界粗糙这是数据源本身的问题可能是接口返回的是简化的边界顶点数量少形状失真也可能是小区搜索落到了错误的POI上。此时要回到第一步调整query关键词用“楼盘名”“小区名期数”重新试。还有些隐藏技巧输出GeoJSON后可以用在线工具转成KML直接在谷歌地球里打开跟卫星影像叠加看。这个检查方法简单粗暴非GIS工程师也能一眼看出边界对不对。5. 实战中绕不开的坑频率控制、接口变更与合规做地图数据抓取真正浪费时间的往往不是主流程而是各种边界情况。按我这边的经验整理一份避坑清单。5.1 高频问题速查表问题现象可能原因处理方式地点检索返回空数组region城市名不标准或确实查无此小区用城市adcode替代region去掉“小区”后缀重试地点检索报“APP AK校验失败”AK没有开通Place API服务或配额耗尽登录百度地图开放平台给应用勾选对应服务查配额余量边界接口返回空字符串该小区名匹配不到多边形数据换搜索关键词加“楼盘”或“花园”等后缀手工验证boundary带了一堆“colorxxx”前缀接口返回了样式控制信息用正则把坐标起始点提取出来再解析爬了几百个后被限流请求频率过快IP被临时封禁降低QPS到1以内随机睡眠必要时用代理池注意合规坐标整体偏移几百米重灾区把BD09坐标直接当WGS84用必须走BD09→GCJ02→WGS84两段转换不能省这张表基本覆盖了我踩过的所有坑。第一条最容易忽略地点检索的region参数如果传“上海市”有时反而返回空传adcode“021”反而稳建议两种都试。5.2 请求频率、UA和重试策略地图服务接口并不欢迎被打爆。即使你的AK合法高频请求也会触发限制。我自己的策略是单线程每请求之间sleep 0.5到1.5秒随机值这样一分钟最多几十次既不会让自己陷入反爬泥潭也照顾到了服务器的压力。请求头建议加一个常见的浏览器User-Agent有些网关对裸Python请求识别得更严格。示例headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, paramsparams, headersheaders, timeout15)重试策略也有讲究。对状态码5xx或者超时指数退避重试三次对4xx参数错误、鉴权失败不要重试重试只会浪费配额。代码里判断一下status字段不是网络错误就不动。5.3 接口变化与合规使用的边界最后说点实在的。这类边界接口并不是百度地图官方声明中“鼓励”的采集方式它随时可能改版、加签名、下线。所以我前面才啰嗦地让你抓包确认URL而不是给你一个所谓“永久有效”的接口。脚本要写成配置驱动的形式接口变了只改配置不用重写逻辑。从工程上来讲这比什么花哨的重试都重要。数据合规方面同样要放在心上。采集范围建议只限公开可见的小区轮廓信息用于正当的工程分析、学术研究或者自己业务系统的底图制作不要拿去批量售卖、恶意爬取全量也不要存太多超出需求的字段。控制采集频率尊重网站正常运行秩序这是爬虫从业者的基本底线。我在项目交付时还会在数据库建表注释里写明“坐标来源百度地图公开前端接口采集日期2025年”既方便溯源也是留存合规证据。整体项目做完我最深的体会是地图边界爬取的技术门槛其实不高真正值钱的是对数据结构、坐标系和接口背后的理解。整个过程踩过不少坑但把方法固化下来之后再遇到类似的“官网没有、前端有”的数据需求就都能举一反三地应对了。下次再有人问我怎么爬那个什么什么地图的小区边界我大概会把这篇文章甩过去然后补一句先开F12抓包别在网上找过期的代码。
返回列表