基于OpenStreetMap的AOI与POI数据批量获取工具:一键导出GeoJSON
2026/9/21 1:50:48 网站建设 项目流程

1. 这个工具到底解决了什么问题

做地理数据分析的人都有一个共同的痛点:想要一份干净的、带边界坐标的全国范围AOI(Area of Interest,兴趣面)或POI(Point of Interest,兴趣点)数据,要么花钱买,要么手动一个个画。商业地图API虽然能查,但导出受限、坐标系加密、批量获取成本高,而且拿到的数据格式往往不通用。

OpenStreetMap(OSM)是全球最大的开源地图数据库,里面沉淀了海量的建筑轮廓、道路、公园、学校、医院等地理要素。理论上,你可以从OSM里把全国任何一个城市的AOI边界和POI点位扒下来,导出成GeoJSON,直接丢进QGIS、ArcGIS或者做空间分析。但问题是:OSM的数据结构跟商业地图完全不一样,它不是按“POI”“AOI”这种业务概念组织的,而是用node、way、relation三种基础元素加上tag标签来描述世界。你想查“上海市所有医院”,得自己写Overpass QL查询语句,还得处理坐标系、数据清洗、格式转换、边界裁剪等一系列问题。

我做的这个开源工具,核心目标就一个:让你输入一个地名或者一个行政区划代码,一键拉取该区域内所有AOI边界和POI点位,直接输出标准GeoJSON文件。不需要你懂Overpass QL语法,不需要你手动处理坐标系偏移,不需要你写数据清洗脚本。工具内部封装了查询构建、分块请求、去重合并、坐标转换、格式标准化这一整套流程。

适合谁用?做城市规划的、做选址分析的、做物流路径优化的、做学术研究需要地理数据的、做数据可视化需要底图要素的,甚至你只是想看看自己家周边有哪些便利店和公园,都能用。对新手来说,你不需要有GIS背景,只要能跑Python脚本就行;对老手来说,工具提供了完整的参数配置接口,可以精细控制查询范围、要素类型、输出精度。

2. 为什么选择OpenStreetMap而不是商业地图

2.1 商业地图API的硬伤

先说说为什么不直接用某德、某度。第一,坐标系问题。国内商业地图普遍使用GCJ-02或者BD-09坐标系,跟国际通用的WGS-84之间存在非线性偏移。你从API拿到的坐标,直接叠加到OSM底图或者卫星影像上,会发现整体偏移几百米。要纠偏就得做坐标转换,而转换算法虽然公开,但批量处理时精度和效率都是问题。

第二,数据导出限制。商业地图API通常只提供查询接口,不提供批量导出。你想拿一个区的所有POI,得按关键词分页请求,每个关键词还有返回数量上限。一个中等规模的区,可能要发几百次请求才能覆盖全,而且很多API对QPS有严格限制,跑一遍下来半天没了。

第三,数据授权问题。商业地图的数据你不能随便分发、不能用于商业产品、不能二次加工后公开发布。做学术研究发论文,审稿人可能会质疑数据来源的合规性。

2.2 OSM的独特优势

OSM的数据是ODbL协议,你可以自由使用、分发、修改,只要署名并以相同方式共享。坐标系是WGS-84,跟GPS原始坐标一致,不需要纠偏。数据结构虽然复杂,但一旦理解了tag体系,你会发现它的信息密度极高。一个building的way,可能带着building=yes、building:levels=6、addr:street=xxx、addr:housenumber=xxx等十几个标签,这些信息在商业地图里往往是分散的、不完整的。

更重要的是,OSM有Overpass API。这是一个专门为OSM数据设计的查询接口,支持按区域、按标签、按几何关系组合查询。你可以写一条语句,把某个行政区内所有amenity=school的node和way全部拉出来,返回格式可以是JSON、XML或者CSV。Overpass QL的语法虽然有点陡峭,但一旦掌握,查询效率极高。

2.3 工具的核心设计思路

我的工具本质上是一个Overpass QL的封装器加数据后处理器。整体流程分四步:

第一步,地理编码。用户输入“上海市浦东新区”或者行政区划代码“310115”,工具调用Nominatim(OSM的官方地理编码服务)获取该区域的OSM关系ID和边界多边形。Nominatim返回的边界多边形是WGS-84坐标的GeoJSON,直接可用。

第二步,查询构建。根据用户选择的要素类型(AOI还是POI,或者两者都要),工具动态生成Overpass QL语句。AOI查询主要针对闭合的way和relation,比如building、landuse、leisure、amenity等标签;POI查询主要针对node和部分way,比如shop、amenity、tourism、office等标签。

第三步,分块请求与合并。Overpass API对单次查询有面积和超时限制。全国范围一次查完基本不可能,所以工具会把大区域切成网格,逐块查询,然后按OSM ID去重合并。切块的大小可以配置,默认是0.1度×0.1度,大约11公里×11公里。

第四步,数据清洗与导出。原始返回的JSON里,每个元素都带着一堆元数据(版本号、时间戳、变更集、用户等),工具会剥离这些无关字段,只保留geometry和核心tags,然后统一转换成GeoJSON FeatureCollection。AOI输出Polygon或MultiPolygon,POI输出Point。

3. 环境准备与依赖安装

3.1 基础环境要求

工具用Python 3.8+开发,主要依赖requests、shapely、geopandas、rtree这几个库。如果你只是跑基础功能,不装geopandas也行,但做空间裁剪和几何运算时会慢很多。建议用conda建一个独立环境,避免跟系统Python冲突。

conda create -n osm-tool python=3.10 conda activate osm-tool pip install requests shapely geopandas rtree pyproj

如果你不用conda,用venv也可以:

python -m venv osm-env source osm-env/bin/activate # Linux/Mac osm-env\Scripts\activate # Windows pip install requests shapely geopandas rtree pyproj

3.2 Overpass API端点选择

Overpass API有多个公共端点,不同端点的负载和响应速度差异很大。主端点overpass-api.de负载最高,经常排队;kumi.systems和overpass.openstreetmap.ru相对快一些,但稳定性看运气。工具默认配置了三个端点,自动轮询,某个端点超时就切下一个。

注意:公共端点都有 fair use 政策,不要短时间内发大量请求。工具内置了请求间隔控制,默认每次请求间隔1秒,批量查询时建议调到2-3秒。

3.3 地理编码服务配置

Nominatim是OSM官方的地理编码服务,免费但有限制:每秒最多1次请求,每天不超过一定量。工具里做了缓存机制,同一个地名第二次查询直接读本地缓存,不重复请求。如果你需要高频地理编码,可以自建Nominatim服务,或者用其他开源地理编码方案。

4. 核心查询逻辑与Overpass QL实战

4.1 Overpass QL基础语法速成

Overpass QL的语法结构可以类比SQL,但它是专门为空间数据设计的。一条典型的查询语句长这样:

[out:json][timeout:60]; area["name"="浦东新区"]->.searchArea; ( node["amenity"="school"](area.searchArea); way["amenity"="school"](area.searchArea); relation["amenity"="school"](area.searchArea); ); out body; >; out skel qt;

逐行解释:第一行设置输出格式为JSON,超时60秒。第二行查找name为“浦东新区”的area,存到变量searchArea。第三行到第五行,在searchArea范围内分别查node、way、relation中amenity=school的元素。第六行输出元素的完整信息。第七行是递归向上查询,把way和relation引用的node也拉出来。第八行输出骨架信息和四叉树排序。

这个查询会返回浦东新区所有学校的点位和建筑轮廓。但实际用的时候,你会发现几个问题:一是area的name匹配可能不准确,同名区域很多;二是有些学校没有amenity=school标签,而是用landuse=education或者building=school;三是relation类型的学校(比如大学校园)需要特殊处理。

4.2 AOI查询的标签体系

AOI的本质是一个闭合区域,在OSM里对应way或relation。但不是所有闭合way都是AOI,比如一个环形交叉路口也是闭合way,但它不是兴趣面。所以需要按标签过滤。我整理了常用的AOI标签组合:

类别标签键典型值几何类型
建筑轮廓buildingyes, residential, commercialPolygon
土地利用landuseresidential, commercial, industrialPolygon
休闲设施leisurepark, sports_centre, gardenPolygon
教育科研amenityschool, university, hospitalPolygon
交通设施aeroway, railwayterminal, stationPolygon
水系naturalwater, wetlandPolygon

查询时用正则表达式组合多个值,比如["building"~"yes|residential|commercial"],这样一条语句就能覆盖多种建筑类型。

4.3 POI查询的标签体系

POI在OSM里主要是node,但也有少量way(比如一个便利店可能画成小矩形)。POI的标签体系比AOI更丰富,因为POI的分类更细。常用的POI标签:

类别标签键典型值
餐饮amenityrestaurant, cafe, fast_food
购物shopsupermarket, convenience, mall
医疗amenityhospital, clinic, pharmacy
教育amenityschool, kindergarten, library
交通highwaybus_stop, crossing
金融amenitybank, atm
旅游tourismhotel, attraction, museum

POI查询时要注意,有些POI同时有多个标签,比如一个加油站既有amenity=fuel又有shop=convenience,去重时按OSM ID去重即可,不会重复。

4.4 分块查询的实现细节

全国范围直接查Overpass基本会超时,必须分块。我的做法是:先用Nominatim拿到目标区域的边界多边形,然后用shapely的box函数生成网格,跟边界多边形做相交运算,得到每个网格块的实际查询范围。每个网格块单独发一次Overpass请求,返回结果按OSM ID去重后合并。

网格大小的选择是个权衡:网格太小,请求次数多,总耗时长;网格太大,单次查询可能超时。实测下来,0.1度×0.1度在大多数情况下能在30秒内返回,是比较稳妥的选择。如果目标区域建筑密度特别高(比如上海内环),可以调到0.05度;如果目标区域是郊区或农村,可以放大到0.2度。

from shapely.geometry import box import geopandas as gpd def generate_grid(boundary_gdf, grid_size=0.1): minx, miny, maxx, maxy = boundary_gdf.total_bounds grid_cells = [] x = minx while x < maxx: y = miny while y < maxy: cell = box(x, y, x + grid_size, y + grid_size) if cell.intersects(boundary_gdf.geometry.iloc[0]): grid_cells.append(cell) y += grid_size x += grid_size return gpd.GeoDataFrame(geometry=grid_cells, crs="EPSG:4326")

这段代码生成覆盖目标区域的网格,只保留与边界相交的格子。实际查询时,每个格子单独构造Overpass QL,把格子的bbox传进去。

5. 数据清洗与GeoJSON导出

5.1 原始数据的结构问题

Overpass返回的JSON里,每个元素的结构是这样的:

{ "type": "way", "id": 123456789, "bounds": {"minlat": 31.0, "minlon": 121.0, "maxlat": 31.1, "maxlon": 121.1}, "nodes": [123, 456, 789, 123], "tags": {"building": "yes", "addr:street": "某某路"} }

注意nodes字段只存了node的ID,没有坐标。坐标在另外的node元素里。所以处理时需要先建立node ID到坐标的映射,然后按nodes顺序重建几何。如果nodes的第一个和最后一个ID相同,说明是闭合way,可以构成Polygon;否则是LineString,对于AOI来说应该丢弃。

relation更复杂,它由多个way和node组成,有outer和inner角色之分。outer构成外环,inner构成内环(比如一个小区中间有个湖)。重建relation几何时,需要把outer的way拼成外环,inner的way拼成内环,然后构造Polygon或MultiPolygon。

5.2 几何有效性修复

OSM数据是人工编辑的,难免有几何错误:自相交、重复点、环方向不对、外环和内环嵌套错误等。shapely提供了make_valid函数可以修复大部分问题,但修复后可能变成MultiPolygon,需要做类型统一。

from shapely.validation import make_valid from shapely.geometry import Polygon, MultiPolygon def fix_geometry(geom): if not geom.is_valid: geom = make_valid(geom) if isinstance(geom, Polygon): return MultiPolygon([geom]) return geom

统一成MultiPolygon的好处是后续处理不用判断类型,直接遍历所有polygon即可。

5.3 坐标系与精度处理

OSM原始坐标是WGS-84,精度到小数点后7位,大约1厘米。但实际数据中,很多node的坐标只精确到小数点后5位,大约1米。导出GeoJSON时,建议保留6位小数,既能保证精度,又不会让文件过大。

如果你需要做面积计算,WGS-84的度单位不能直接用,需要投影到等面积投影。中国区域常用EPSG:4527(CGCS2000 3度带)或者EPSG:3857(Web Mercator,但面积变形大)。工具里提供了投影转换的选项,默认不转换,保持WGS-84输出。

5.4 GeoJSON输出格式规范

输出的GeoJSON遵循RFC 7946标准,FeatureCollection里每个Feature包含geometry和properties。properties里保留原始tags,但去掉元数据字段(version、timestamp、changeset、user、uid)。另外增加两个字段:osm_id和osm_type,方便溯源。

{ "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "MultiPolygon", "coordinates": [[[[121.0, 31.0], [121.1, 31.0], [121.1, 31.1], [121.0, 31.1], [121.0, 31.0]]]] }, "properties": { "osm_id": 123456789, "osm_type": "way", "building": "yes", "addr:street": "某某路" } } ] }

提示:GeoJSON文件超过100MB后,很多软件打开会卡顿。如果目标区域很大,建议按行政区拆分输出,或者用GeoPackage格式替代。

6. 实操全流程:以上海市浦东新区为例

6.1 第一步:获取行政区边界

输入“上海市浦东新区”,工具调用Nominatim查询。Nominatim返回的结果里,有一个osm_type=relation、osm_id=某值的记录,这就是浦东新区的行政边界。获取边界GeoJSON后,用geopandas读入,确认坐标系是EPSG:4326。

import requests import geopandas as gpd from shapely.geometry import shape def get_boundary(place_name): url = "https://nominatim.openstreetmap.org/search" params = { "q": place_name, "format": "geojson", "polygon_geojson": 1, "limit": 1 } headers = {"User-Agent": "osm-aoi-tool/1.0"} resp = requests.get(url, params=params, headers=headers) data = resp.json() if data["features"]: return shape(data["features"][0]["geometry"]) return None

实测下来,Nominatim对国内地名的匹配准确率还不错,但“浦东新区”可能返回多个结果,需要按osm_type=relation过滤。如果匹配不到,可以改用行政区划代码查询,或者手动指定OSM relation ID。

6.2 第二步:生成查询网格

浦东新区面积约1210平方公里,按0.1度网格切分,大约产生60-80个网格块。每个网格块单独查询,总请求数在80次左右。按每次请求间隔1.5秒算,总耗时约2分钟。如果并发请求,可以压缩到30秒内,但要注意公共端点的限流。

boundary = get_boundary("上海市浦东新区") grid = generate_grid(boundary, grid_size=0.1) print(f"生成 {len(grid)} 个网格块")

6.3 第三步:构建并发送Overpass查询

对每个网格块,构造Overpass QL。AOI查询和POI查询分开,因为标签体系不同。AOI查询用way和relation,POI查询用node和way。

def build_aoi_query(bbox): minx, miny, maxx, maxy = bbox bbox_str = f"{miny},{minx},{maxy},{maxx}" query = f""" [out:json][timeout:60]; ( way["building"]({bbox_str}); way["landuse"]({bbox_str}); way["leisure"]({bbox_str}); way["amenity"]({bbox_str}); relation["building"]({bbox_str}); relation["landuse"]({bbox_str}); relation["leisure"]({bbox_str}); relation["amenity"]({bbox_str}); ); out body; >; out skel qt; """ return query

发送请求时,用requests的POST方法,把query作为data传过去。Overpass API支持GET和POST,POST更适合长查询。

def query_overpass(query, endpoint="https://overpass-api.de/api/interpreter"): resp = requests.post(endpoint, data={"data": query}, timeout=120) if resp.status_code == 200: return resp.json() return None

6.4 第四步:解析与合并结果

每个网格块返回的JSON里,elements数组包含node、way、relation。先建立node ID到坐标的映射,然后遍历way和relation重建几何。所有网格块的结果按OSM ID去重,合并成一个大的FeatureCollection。

def parse_elements(elements): nodes = {} ways = [] relations = [] for el in elements: if el["type"] == "node": nodes[el["id"]] = (el["lon"], el["lat"]) elif el["type"] == "way": ways.append(el) elif el["type"] == "relation": relations.append(el) features = [] for way in ways: coords = [nodes[nid] for nid in way["nodes"] if nid in nodes] if len(coords) >= 4 and coords[0] == coords[-1]: geom = Polygon(coords) features.append({ "type": "Feature", "geometry": geom.__geo_interface__, "properties": {"osm_id": way["id"], "osm_type": "way", **way.get("tags", {})} }) return features

relation的解析更复杂,需要按role分组,outer拼外环,inner拼内环。这里不展开代码,核心思路是用shapely的polygonize或者手动拼接。

6.5 第五步:导出GeoJSON

合并去重后,用geopandas的to_file导出。如果数据量大,建议用GeoPackage格式,读写更快,支持空间索引。

gdf = gpd.GeoDataFrame.from_features(features, crs="EPSG:4326") gdf.to_file("pudong_aoi.geojson", driver="GeoJSON") gdf.to_file("pudong_aoi.gpkg", driver="GPKG")

实测浦东新区全量AOI大约15万条,GeoJSON文件约200MB,GeoPackage约80MB。用QGIS打开GeoPackage流畅,GeoJSON会卡几秒。

7. 常见问题与排查技巧实录

7.1 查询超时怎么办

Overpass API超时是最常见的问题。原因通常是查询范围太大、标签太宽泛、或者端点负载高。解决办法:缩小网格尺寸、减少标签组合、换端点、增加超时时间。如果某个网格块反复超时,可以单独把它再切小,或者跳过该块,最后用相邻块的数据补。

实操心得:把[timeout:60]改成[timeout:180]能解决大部分超时,但公共端点可能直接拒绝。更好的做法是优化查询语句,比如用nwr代替分开写node、way、relation,减少语句长度。

7.2 数据缺失与标签不全

OSM的数据覆盖度在国内城市差异很大。一线城市核心区数据很全,郊区就差很多,农村地区可能只有道路没有建筑。如果你发现某个区域查不到数据,先确认OSM上有没有数据。打开openstreetmap.org,定位到该区域,看看有没有建筑轮廓。如果没有,那就是源头没有数据,工具也变不出来。

另一个常见问题是标签不规范。比如一个学校,可能标了amenity=school,也可能只标了building=yes加name=某某学校。工具默认只查标准标签,如果你需要更全的结果,可以放宽标签条件,比如查所有带name标签的way。

7.3 几何错误与修复

自相交多边形、重复点、环方向错误是OSM数据的常见问题。shapely的make_valid能修复大部分,但修复后可能产生空几何或碎片。建议在导出前做一次过滤,丢弃面积为0或极小的多边形。

gdf = gdf[gdf.geometry.area > 1e-8] gdf = gdf[gdf.geometry.is_valid]

如果修复后还是有问题,可以用QGIS的“检查几何有效性”工具手动修,或者用PostGIS的ST_MakeValid函数。

7.4 坐标系偏移问题

OSM是WGS-84,如果你要跟国内商业地图数据叠加,需要做坐标转换。GCJ-02的转换算法是公开的,但批量转换时要注意精度损失。工具里提供了可选的坐标转换模块,默认关闭。如果你只是做可视化,用OSM底图,不需要转换;如果要跟某德数据叠加,再开启转换。

7.5 常见问题速查表

问题现象可能原因解决方法
查询返回空区域无数据或标签不匹配放宽标签条件,确认OSM有数据
查询超时范围太大或端点负载高缩小网格,换端点,增加超时
几何无效自相交或环方向错误make_valid修复,过滤空几何
坐标偏移坐标系不一致开启坐标转换模块
文件过大数据量太大按行政区拆分,用GeoPackage
去重不彻底OSM ID重复按osm_type+osm_id联合去重

8. 进阶用法与扩展思路

8.1 按自定义多边形查询

除了按行政区查询,工具还支持传入自定义多边形。比如你有一个园区的边界GeoJSON,想查园区内的所有建筑和POI,直接把GeoJSON传给工具,它会用这个多边形做空间过滤。实现方式是用shapely的intersects判断每个要素是否在多边形内,或者用Overpass的poly过滤器。

way["building"](poly:"31.0 121.0 31.1 121.0 31.1 121.1 31.0 121.1");

poly过滤器接受一串经纬度点,构成查询多边形。注意点的顺序要闭合,且不能太多(Overpass有限制)。

8.2 定时增量更新

OSM数据每天都在变,如果你需要保持数据最新,可以设置定时任务,每天或每周跑一次增量更新。Overpass API支持按时间范围查询变更,用[diff:"2024-01-01T00:00:00Z"]可以只返回指定时间之后的变更。增量更新比全量查询快很多,适合长期维护的数据集。

8.3 与其他数据源融合

OSM数据可以和人口数据、经济数据、交通数据融合。比如把POI密度和人口密度做相关性分析,或者把AOI边界和房价数据叠加。工具输出的GeoJSON可以直接导入PostGIS、DuckDB Spatial、或者GeoPandas做进一步分析。

8.4 可视化展示

导出的GeoJSON可以用QGIS、Kepler.gl、Mapbox GL JS、Leaflet等工具可视化。如果做网页展示,推荐用Mapbox GL JS或者MapLibre GL JS,矢量瓦片渲染性能好。如果只是快速看效果,QGIS拖进去就行。

提示:GeoJSON文件在网页端加载时,超过50MB建议转成矢量瓦片(Vector Tiles),用tippecanoe工具切片,加载速度能提升一个数量级。

9. 我在实际使用中踩过的坑

第一个坑是Nominatim的地理编码结果不稳定。同一个地名,不同时间查询可能返回不同的relation ID,因为OSM的行政区划边界在调整。解决办法是把第一次查询到的relation ID缓存下来,后续直接用ID查询,不再走地理编码。

第二个坑是Overpass API的公共端点限流。有次我并发发了20个请求,直接被封了IP,等了半小时才恢复。后来改成串行加间隔,虽然慢但稳定。如果你需要高频查询,建议自建Overpass实例,或者用商业的OSM数据服务。

第三个坑是relation几何重建的复杂性。有些relation的outer way不是首尾相连的,需要手动排序和拼接。我写了一个基于端点距离的排序算法,但遇到断开的环还是得特殊处理。后来发现用shapely的linemergepolygonize组合能解决大部分情况。

第四个坑是内存溢出。全国范围的AOI数据,如果一次性加载到内存,16GB内存的机器直接爆。后来改成流式处理,每个网格块解析完就导出,最后用ogr2ogr合并。这样内存占用稳定在2GB以内。

第五个坑是标签编码问题。OSM的tags里可能有中文、日文、韩文等多语言字符,导出GeoJSON时如果编码不对,会变成乱码。确保Python脚本用UTF-8编码,geopandas导出时指定encoding='utf-8'。

10. 这个工具后续还能怎么扩展

目前工具的核心功能是AOI和POI的批量获取与导出。后续可以扩展的方向有几个:一是增加更多要素类型,比如道路网络、水系、土地利用分类;二是支持按属性过滤,比如只查面积大于1000平米的建筑;三是增加数据质量评估模块,自动检测几何错误和标签缺失;四是提供REST API,让其他系统可以直接调用;五是做QGIS插件,在QGIS里一键拉取数据。

如果你对这个工具感兴趣,代码已经开源。核心逻辑不复杂,主要是对Overpass QL的封装和几何处理。你可以根据自己的需求修改标签体系、调整网格大小、增加输出格式。遇到问题欢迎交流,我踩过的坑你可以直接跳过。

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

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

立即咨询