☰
OpenStreetMap地图数据下载全流程:基于osmnx的交通路网获取与投影实战
2026/10/15 1:35:52 网站建设 项目流程

1. 交通数据可视化为什么绕不开地图底图

做交通数据分析的人迟早会碰到一个尴尬:手里攒了几万条GPS轨迹、卡口过车记录或者公交刷卡数据,用Matplotlib画出来就是一团密密麻麻的线,看不出任何空间规律。问题不在于数据本身,而在于缺少一张能"托底"的地图。没有路网、没有街区、没有河流和绿地做参照,再漂亮的数据也只是一堆坐标点。

这就是地图底图的价值。它把抽象的经纬度还原成我们熟悉的城市空间,让"早高峰拥堵带""跨江通勤走廊""夜间货运热点"这些概念有了视觉落点。而说到免费、可离线、可商用的地图数据源,OpenStreetMap几乎是绕不开的选择。它是一套由全球志愿者共同维护的开源地理数据库,覆盖道路、建筑、水系、POI等几乎所有你能想到的空间要素,而且以ODbL协议开放,只要署名就能自由使用。

这篇内容面向的是刚接触交通数据可视化的朋友,尤其是那些会写一点Python、但还没系统搭过地图底图流程的人。我会把"用OpenStreetMap下载地图数据"这件事从头到尾拆开讲清楚:为什么选它、下载哪几种数据、用什么工具、参数怎么定、下载完怎么验证。整套流程跑通之后,你就能拿到一份干净的路网数据,为后续的轨迹叠加、热力渲染、路径分析打好地基。整个过程不需要任何商业地图API密钥,也不需要联网调用在线瓦片,全部本地可控。

2. 地图数据选型:为什么是OpenStreetMap而不是别的

2.1 商业地图与开源地图的真实差距

很多人第一反应是用某商业地图的开放平台,毕竟文档齐全、示例丰富。但真到做交通分析的时候,坑就来了。商业地图的POI和路网数据通常有使用条款限制,批量下载、离线存储、二次分发往往踩线;坐标系还经常是加密偏移的,你拿到的经纬度和真实GPS对不上,得额外做纠偏。更麻烦的是,商业地图的路网属性偏"导航友好",比如它会把主辅路合并、把匝道简化,这对导航是好事,对交通流量分析却是灾难——你根本分不清车到底走在主路还是辅路。

OpenStreetMap的优势恰好在这里。它的原始数据保留了完整的道路拓扑和丰富的标签体系,比如highway标签区分了motorway、trunk、primary、residential等十几个等级,lanes记录车道数,maxspeed记录限速,oneway标记单行道。这些字段对交通分析来说就是金矿。而且数据是原始经纬度(WGS84),和绝大多数GPS设备直接对齐,省去纠偏环节。

当然它也有短板。OSM的数据质量依赖志愿者贡献,不同城市密度差异很大,一线城市的路网相当完善,偏远地区可能连主干道都缺。另外它的属性字段是"自由标注"风格,同一个含义可能有多种写法,清洗起来要花点功夫。但对于学习和中小规模项目,这些代价完全值得。

2.2 三种主流获取方式的取舍

拿到OSM数据大致有三条路,我按适用场景排一下。

第一种是直接下载官方导出的区域包。OSM官网提供按大洲、按国家切分的.osm.pbf文件,比如整个中国的数据包大概几个GB。这种方式适合需要全国或全省范围、且磁盘和内存都充裕的场景。缺点是文件太大,光解析就要等很久,而且你往往只需要一个城市,下载全国纯属浪费。

第二种是用Overpass API按条件查询。它像一个针对OSM的SQL,你可以写"给我某市范围内所有highway=primary的道路",服务端只返回匹配结果。这种方式精准、灵活,适合按需抓取特定要素。但Overpass有查询频率限制,大范围复杂查询容易超时,而且返回的是XML或JSON,还得自己转成地理格式。

第三种是用osmnx这类Python库,它把Overpass API和后续的图论处理封装好了,一行代码就能下载某地的路网并转成NetworkX图。这是做交通网络分析最省事的路子,也是我下面重点展开的方式。它的本质还是调Overpass,但帮你处理了坐标投影、拓扑简化、属性保留等琐事。

提示:三种方式不是互斥的。实际项目里我经常先用osmnx快速拉一个城市的路网做探索,确认分析思路可行后,再用Overpass API精确抓取需要的要素做正式生产。

2.3 坐标系与投影的前置认知

在动手之前必须把坐标系这件事说透,否则后面一定翻车。OSM原始数据用的是WGS84地理坐标系,单位是经纬度。经纬度直接拿来算距离是有问题的——在纬度40度的地方,一度经度对应的实际距离只有一度纬度的约0.77倍,越往高纬差异越大。所以只要涉及距离、面积、缓冲区这类计算,就必须先投影到平面坐标系。

国内常用的是UTM分带投影,比如北京大致落在UTM 50N带。osmnx提供了project_graph函数,能自动根据路网中心点选择合适的UTM带并完成投影。投影之后单位变成米,算长度、算面积、做缓冲区都符合直觉。这一步千万别省,我见过太多人直接用经纬度算路网总长度,结果偏差百分之十几还浑然不觉。

3. 环境搭建与依赖安装的实操细节

3.1 Python环境与核心库清单

我建议用conda建一个独立环境,避免和系统里的其他包打架。Python版本选3.9到3.11之间比较稳,太新的版本有些地理库的轮子还没跟上。

conda create -n osm_traffic python=3.10 conda activate osm_traffic

核心依赖其实就几个,但每个都有讲究:

  • osmnx:下载和预处理路网的主力,它会自动带上networkx、geopandas、shapely等依赖。
  • geopandas:处理矢量数据的瑞士军刀,读写GeoJSON、做空间连接都靠它。
  • matplotlib:基础绘图,后面叠加数据要用。
  • contextily:给静态图加在线底图瓦片,做快速预览很方便。
pip install osmnx geopandas matplotlib contextily

这里有个坑要提前说:geopandas在Windows上装有时候会因为GDAL、Fiona这些底层库编译失败。如果pip报错,最省事的办法是改用conda安装,conda的预编译包能避开编译环节。

conda install -c conda-forge geopandas

3.2 验证安装是否真的可用

装完别急着写业务代码,先跑一段最小验证,确认各个库能正常协作。很多人跳过这步,结果后面报错时分不清是环境问题还是代码问题。

import osmnx as ox import geopandas as gpd import matplotlib.pyplot as plt print("osmnx version:", ox.__version__) print("geopandas version:", gpd.__version__) # 测试能否正常请求Overpass try: gdf = ox.geocode_to_gdf("某市中心广场") print("地理编码测试通过:", gdf.geometry.iloc[0].centroid) except Exception as e: print("请求失败,检查网络或Overpass状态:", e)

如果这段能打印出版本号和坐标,说明环境基本就绪。如果卡在请求那一步,多半是网络到Overpass服务器的连通性问题,可以稍后重试或换个时间段。

3.3 缓存与请求频率的自我保护

osmnx默认会缓存请求结果,缓存在用户目录下的cache文件夹里。这个机制很贴心,同一个查询第二次执行会直接读缓存,既快又省得反复打扰公共服务器。但要注意,如果你改了查询参数却感觉结果没变,先检查是不是命中了旧缓存。清理缓存可以用ox.settings.use_cache = False临时关闭,或者手动删缓存目录。

另外Overpass是公共资源,官方对请求频率有软性限制。批量下载多个城市时,建议在循环里加个time.sleep(2),别把人家服务器打爆。这不是技术问题,是基本的网络礼仪,也避免自己的IP被临时限流。

4. 用osmnx下载路网数据的完整流程

4.1 确定研究范围:三种边界定义方式

下载路网第一步是告诉程序"我要哪块地方"。osmnx支持三种方式,各有适用场景。

第一种是按地名查询,最省事:

import osmnx as ox place = "某市, 某省" G = ox.graph_from_place(place, network_type="drive")

它会自动去地理编码服务查这个地名的边界多边形,然后抓取范围内的路网。适合城市级别的快速获取,但地名解析偶尔会出错,比如同名地区。

第二种是按中心点加半径:

G = ox.graph_from_point((39.9042, 116.4074), dist=3000, network_type="drive")

这里的坐标是纬度在前、经度在后,dist单位是米。这种方式适合研究某个商圈、某个枢纽周边,范围精确可控,不受行政区划边界影响。

第三种是按自定义多边形:

from shapely.geometry import Polygon poly = Polygon([(116.35, 39.88), (116.45, 39.88), (116.45, 39.95), (116.35, 39.95)]) G = ox.graph_from_polygon(poly, network_type="drive")

当你研究的区域和行政区划不一致,比如一条跨区的通勤走廊,这种方式最灵活。多边形可以从Shapefile或GeoJSON读进来,和你的业务数据边界对齐。

4.2 network_type参数怎么选

network_type决定了抓哪些道路,这个参数直接关系到后续分析的有效性。常见取值和含义如下:

network_type包含道路典型用途
drive机动车道车流分析、路径规划
drive_service含服务道路含小区内部道路的车流分析
walk步行道步行可达性、慢行系统
bike自行车道骑行网络分析
all全部道路综合网络研究

做交通流量分析一般用drive,它排除了步行街、自行车专用道这些机动车上不去的路。但要注意,drive默认不含服务道路(比如小区内部路、停车场通道),如果你的数据涉及末端配送,得用drive_service。选错了会导致路网不连通,路径规划时出现"断头路"。

4.3 下载后的图对象结构解析

graph_from_place返回的是一个NetworkX的MultiDiGraph对象。理解它的结构是后续所有操作的基础。

节点(node)代表路口或道路端点,每个节点有x(经度)、y(纬度)坐标。边(edge)代表路段,每条边有length(长度,米)、highway(道路等级)、maxspeed(限速)、name(路名)等属性。因为是MultiDiGraph,两个节点之间可以有多条边(比如双向道路就是两条方向相反的边),也可以有自环。

print("节点数:", len(G.nodes)) print("边数:", len(G.edges)) # 看一条边的属性 u, v, key = list(G.edges(keys=True))[0] print(G.edges[u, v, key])

节点数和边数的比例能粗略反映路网复杂度。一般城市路网边数远多于节点数,因为大量路段是双向的。如果边数反而比节点数少很多,可能是抓取范围太小或者network_type选得太窄。

4.4 投影转换:让距离计算回归正常

前面强调过投影的重要性,这里给出具体操作:

G_proj = ox.project_graph(G)

project_graph会自动判断路网中心所在的UTM带,把经纬度转成米制平面坐标。转换后节点的x、y就变成了投影坐标,边的length也会重新计算成准确的米数。你可以对比一下转换前后的总长度:

import networkx as nx length_geo = sum(d["length"] for _, _, d in G.edges(data=True)) length_proj = sum(d["length"] for _, _, d in G_proj.edges(data=True)) print(f"地理坐标总长: {length_geo:.0f} 米") print(f"投影后总长: {length_proj:.0f} 米")

正常情况下两者应该接近,但如果研究区域跨度大,差异会明显。投影后的值才是可信的。

5. 数据落盘与格式选择的经验

5.1 存成什么格式最合适

下载完的数据得存下来,否则每次分析都重新请求太浪费。常见格式有几种,我按推荐度排。

GeoJSON是最通用的选择,文本格式、人类可读、几乎所有GIS软件和Python库都认。缺点是文件偏大,几十万条边的路网可能上百MB。适合中小规模数据和需要跨工具交换的场景。

GeoPackage(.gpkg)是SQLite封装的矢量格式,单文件、支持空间索引、读写快,是QGIS的默认格式。大规模路网我首选它,体积比GeoJSON小很多,查询也快。

Shapefile是老牌格式,兼容性最好,但有字段名长度限制(10字符)、不支持UTF-8中文、必须多个文件配套。除非对接老系统,否则不推荐。

# 转成GeoDataFrame再存 nodes, edges = ox.graph_to_gdfs(G_proj) edges.to_file("road_edges.gpkg", driver="GPKG") nodes.to_file("road_nodes.gpkg", driver="GPKG")

5.2 属性字段的取舍与精简

原始下载的边属性可能有几十个字段,大部分用不上。存盘前精简一下,能显著减小体积、加快后续读取。

keep_cols = ["length", "highway", "maxspeed", "name", "oneway", "lanes"] edges_slim = edges[[c for c in keep_cols if c in edges.columns]] edges_slim.to_file("road_edges_slim.gpkg", driver="GPKG")

但精简要谨慎。如果你后面可能做限速分析,maxspeed就不能删;要做车道级分析,lanes得留着。我的习惯是先全量存一份归档,再另存一份精简版给日常分析用,两不耽误。

5.3 版本管理与可复现性

OSM数据是持续更新的,今天下载的路网和三个月后的可能不一样。做正式研究时,建议在文件名或元数据里记录下载日期和查询参数,比如road_edges_20240115_drive.gpkg。这样别人复现你的结果时,能知道用的是哪个时间切片的数据。有条件的话把查询脚本也一起归档,参数写死在脚本里,比口头描述可靠得多。

6. 下载环节的常见问题与排查

6.1 请求超时与返回空数据

最常见的报错是Overpass请求超时。原因通常是查询范围太大或条件太复杂。解决办法有几个:缩小范围、简化network_type、增加超时时间。

ox.settings.timeout = 300 # 默认180秒,大范围可调大 ox.settings.max_query_area_size = 5e9 # 允许更大的查询面积

如果返回的图节点数为0,先确认地名解析是否正确。有些地名在OSM里对应多个多边形,程序可能选了错误的一个。可以先用ox.geocode_to_gdf看看解析出来的边界在哪。

6.2 路网不连通的处理

下载完发现路网分成好几块互不连通,这在跨江、跨铁路的城市很常见,因为桥梁和隧道可能没被正确抓取。排查思路是先看最大连通分量占比:

import networkx as nx G_undirected = ox.convert.to_undirected(G) components = list(nx.connected_components(G_undirected)) largest = max(components, key=len) print(f"最大连通分量占比: {len(largest)/len(G.nodes):.2%}")

如果占比低于90%,说明路网碎片化严重。可能是network_type太窄,换成drive_service或all试试;也可能是研究范围切断了主干道,适当扩大范围。

6.3 中文路名乱码

OSM的中文路名在部分工具链里会显示成乱码,根源是编码不一致。存GeoJSON时显式指定UTF-8:

edges.to_file("roads.geojson", driver="GeoJSON", encoding="utf-8")

用QGIS打开时也要确认图层编码设置。GeoPackage一般没这个问题,这也是我推荐它的原因之一。

6.4 常见问题速查表

现象可能原因解决方向
请求超时范围过大/条件复杂缩小范围、调大timeout
节点数为0地名解析错误改用坐标或自定义多边形
路网碎片化network_type过窄换drive_service或all
中文乱码编码不一致显式指定UTF-8
结果没更新命中旧缓存清缓存或关use_cache
距离偏差大未投影先project_graph

提示:遇到报错先别急着改代码,把完整的错误信息读一遍。Overpass的错误信息通常会告诉你哪一行查询有问题,比盲目试错高效得多。

7. 从下载到可视化的衔接准备

7.1 快速画一张路网图验证数据

数据下载完,最直接的验证就是画出来看看。用matplotlib画路网很快:

import matplotlib.pyplot as plt fig, ax = ox.plot_graph( G_proj, node_size=0, edge_linewidth=0.5, edge_color="#333333", bgcolor="white", show=False, close=False ) plt.savefig("road_network.png", dpi=200, bbox_inches="tight")

如果画出来是一张结构清晰、疏密合理的路网图,说明数据没问题。如果是一团乱麻或者大片空白,回去检查范围和network_type。

7.2 叠加底图做空间参照

纯路网图缺少地理参照,加个底图更直观。contextily能自动拉取在线瓦片:

import contextily as ctx fig, ax = plt.subplots(figsize=(12, 12)) edges.plot(ax=ax, linewidth=0.5, color="#d95f02") ctx.add_basemap(ax, crs=edges.crs.to_string(), source=ctx.providers.CartoDB.Positron) ax.set_axis_off() plt.savefig("road_with_basemap.png", dpi=200, bbox_inches="tight")

注意add_basemap需要传入路网数据的CRS,否则瓦片会对不齐。这一步能直观看出路网和实际地物的吻合度,是数据质量的最后一道检验。

7.3 为交通数据叠加做的字段准备

路网只是底图,真正的主角是你的交通数据。在进入可视化之前,要确保两边能对上。核心是坐标系一致:你的GPS轨迹如果是WGS84经纬度,路网也得用WGS84;如果轨迹已经投影,路网也要投影到同一坐标系。另外建议给路网边预先算好一些派生字段,比如按highway等级分类的颜色映射、按length加权的线宽,这样后面叠加数据时直接调用,不用临时算。

# 按道路等级预设颜色 highway_colors = { "motorway": "#e41a1c", "trunk": "#ff7f00", "primary": "#ffd92f", "secondary": "#a6d854", "tertiary": "#8da0cb", "residential": "#b3b3b3" } edges["color"] = edges["highway"].map( lambda h: highway_colors.get(h if isinstance(h, str) else h[0], "#cccccc") )

这段代码处理了一个细节:highway字段有时是字符串,有时是列表(一条路可能同时属于多个等级),所以做了类型判断。这种小坑在实际数据里很常见,提前处理能省不少调试时间。

我个人在实际操作中的体会是,下载地图数据这件事看着简单,真正花时间的往往不是写代码,而是搞清楚"我到底需要哪些路、哪个范围、什么精度"。把这三个问题想明白,代码就是十几行的事。反过来,如果需求模糊就急着下载,很容易抓回来一堆用不上的数据,或者范围不对反复重来。下一篇我会接着讲怎么把交通流量数据叠加到这张路网上,包括轨迹匹配、热力渲染和动态可视化的具体做法。

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

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

立即咨询