简介:基于OpenStreetMap项目的郑州市道路网络矢量数据,已经过处理,可直接作为Shapefile加载进QGIS、ArcGIS等GIS软件,面向城市规划、交通研究等需要道路基础底图的开发者与研究者。资源包共9个文件,压缩后仅4.49MB:其中.shp存储道路线几何,.dbf记录道路等级、名称等属性,.prj定义坐标投影,.sbn/.sbx/.shx用于空间与属性索引,.cpg确保中文编码正确,另附.xml元数据和jpg格式的OSM类别对照表,方便理解道路分类标签。已有395人学习/下载。借助这份数据,使用者能快速获得郑州市完整的道路网络拓扑与属性信息,进行最短路径计算、交通热点识别、与人口或公交数据叠加分析等空间操作;配套的类别对照表也降低了数据清洗与字段解读门槛,适合作为GIS教学、交通规划或学术研究的可靠底图数据。
1. 郑州市OSM道路矢量数据:一份能直接开工的路网,背后处理了什么
做城市路网分析的人多少都有过这种经历:从 OSM 导出一份道路矢量数据,满心欢喜打开,结果属性表里混着步行道、梯级、甚至只有两三米的小区内部路,坐标系还是 WGS84 经纬度,直接按米算长度全是错的。标题里“已处理”三个字,意味着这份数据已经把最脏最累的活干完了:裁到郑州市域、过滤掉非机动车道、统一坐标系、把 highway 标签映射成可用的道路等级。这份数据适合三类人:做路网分析的研究者、做地图可视化的前端、以及给政企项目做底图校验的 GIS 工程师。本文不聊这份数据本身长什么样——我没法替作者逐字段核对——而是把“已处理”背后一套可复现的处理链路完整拆出来,让你要么能直接信任这份数据,要么自己也能从原始 OSM 加工出一份同级别的郑州路网。
2. OSM 原始道路数据的底子:字段、层级与坐标系统
2.1 highway 字段才是路网骨架,name 和 ref 只是参考
OSM 的道路要素核心不是名字,而是highway这个标签。它是 OSM 社区约定的一套道路分类体系,从高速公路到田埂小路都覆盖。拿到任何一份 OSM 路网,先不要急着看几何,先把highway字段的取值分布统计一遍。我用 QGIS 或 Python 加载数据后,第一件事一定是跑一次字段值计数,看看数据里都有哪些类型。
郑州这种城市的 OSM 路网里,highway常见值大概包括:motorway(高速公路)、trunk(城市快速路,郑州的中州大道、陇海快速路这类)、primary(主干道)、secondary(次干道)、tertiary(支路)、residential(居住区道路)、service(服务性道路)、unclassified(未分类道路)、footway(步行道)、cycleway(自行车道)、path(小径)、track(田间/林间道路)。
这里有个新手最容易踩的认知误区:把 OSM 导出的所有highway要素都当作“道路”参与路网分析。实际上footway、cycleway、path根本不该进入机动车路网模型,否则算出来的路径全是穿公园走的小路。做数据处理时,highway字段既是分类依据,也是过滤依据。
import geopandas as gpd # 加载原始 OSM 道路数据 roads = gpd.read_file("zhengzhou_osm_raw.shp") # 统计 highway 字段分布,先看清家底 print(roads["highway"].value_counts()) # 定义机动车道路类型白名单 drivable_types = [ "motorway", "motorway_link", "trunk", "trunk_link", "primary", "primary_link", "secondary", "secondary_link", "tertiary", "tertiary_link", "unclassified", "residential", "service" ] # 过滤出可通行道路 drivable = roads[roads["highway"].isin(drivable_types)].copy() print(f"原始要素数: {len(roads)},机动车道路要素数: {len(drivable)}")这段代码先把数据整体读进来,用value_counts()看每个highway类型的要素数量,然后按白名单过滤。_link后缀代表匝道和连接路,在路网分析中不能丢,它们是高速出入口和立交桥的连通关键。过滤比例通常很惊人——一份城市级 OSM 数据里,footway和path的要素数量往往超过机动车道路,不过滤直接分析,结果基本没法看。
2.2 属性字段里藏着的可用信息:不止是几何
除了highway,OSM 道路要素还有一批高频使用的属性字段,这里挑几个在郑州路网处理中真正用得上的说明。
name是道路名称,但在 OSM 里它很不可靠。郑州的“金水路”在不同路段可能被标注成“金水路”“金水东路”“金水路(中段)”,甚至直接缺失。ref是道路编号,比如 G107、S312、连霍高速的 G30,这个字段比name稳定,但也存在同一编号挂在多个路段上的情况。
oneway字段标记单行道,值是yes、no或true、false,部分路段可能缺失。做路径规划或方向性分析时,这个字段决定图模型的边是否有向。maxspeed是限速值,单位默认 km/h,但 OSM 里也有填none或空值的,使用时需要兜底处理。lanes是车道数,surface是路面材质(asphalt、concrete、unpaved等),这两个字段在做交通承载力分析时会被用到,但同样有大量缺失。
# 检查关键属性字段的缺失情况 key_fields = ["name", "ref", "oneway", "maxspeed", "lanes", "surface"] for field in key_fields: if field in drivable.columns: null_count = drivable[field].isna().sum() print(f"{field}: 缺失 {null_count} 条,占比 {null_count / len(drivable) * 100:.1f}%")这段代码跑完,你对一份 OSM 数据的“可信度”心里就有数了。处理时对缺失属性要有一套默认策略:oneway缺失默认按双向算,maxspeed缺失按道路等级推默认限速,lanes缺失按等级给参考值。这些策略不是瞎猜,而是各地道路交通设计规范里的推荐值,处理的时候用等级映射比用空值安全得多。
2.3 坐标参考系的陷阱:WGS84 与投影坐标之间差着一个量级
OSM 原始数据的几何存储格式是 WGS84 经纬度,也就是 EPSG:4326。经纬度坐标的单位是度,不能直接在几何上做长度、面积计算——在纬度 34.5 度的郑州,1 度的经度长度和 1 度的纬度长度差了大约 0.83 倍,直接算出来的欧氏距离毫无意义。
标题写明“已处理”的数据,处理链路里必然包含坐标参考系(CRS)转换这一环。郑州地区做米制分析,我用过两种方案:一是 CGCS2000 3 度带投影,郑州经度范围大约在 112.8°E 到 114.1°E 之间,对应 CGCS2000 3 度带第 38 带的中央经线 114°E,EPSG 代码是 EPSG:4547;二是 UTM 49N(EPSG:32649),精度也够。前者更贴合国内测绘成果的坐标系,后者在 OSM 社区里更常见。
# 检查当前坐标系 print("当前 CRS:", drivable.crs) # 投影到 CGCS2000 3-degree Gauss-Kruger zone 38(中央经线 114°E) drivable_projected = drivable.to_crs("EPSG:4547") # 投影后验证一下长度是否合理 total_length_km = drivable_projected.geometry.length.sum() / 1000 print(f"投影后路网总长度: {total_length_km:.0f} 公里")判断投影是否正确,可以直接看几何长度量级。郑州市域内机动车道路总长度在几千公里量级,如果算出来几万公里甚至更高,那大概率是几何包含了市域外数据,或者坐标系处理出了问题。这里有一个玄学但好用的经验:投影完随手算一下路网总长度,如果量级离谱,先别查属性,回去看 CRS 和裁剪边界。
3. 从原始导出到“已处理”:一条可复现的处理流水线
3.1 获取郑州 OSM 原始数据:两种常见渠道
加工一份城市级路网,首先得有原始数据。常见做法是两种:一是从 OSM 的定期镜像站下载所在区域的 pbf 文件——亚洲区域或中国区域——再做空间裁剪;二是用 Overpass API 按行政边界或包围盒直接拉取指定范围的要素。
我一般建议先下载大区域 pbf 再本地裁剪,别直接用 Overpass 拉。原因很简单:Overpass 请求有超时和数据量限制,城市级全要素路网动辄几十万条线段,直接拉取很容易超时,而且拉回来的数据在边界上经常缺胳膊少腿。先有全量数据,再自己做裁剪,每一步都可控。得到 pbf 后,用osmium工具按行政边界裁剪:
# 用郑州市行政边界 GeoJSON 裁剪全国/全省 pbf osmium extract --bbox=112.75,34.35,114.15,34.90 \ -o zhengzhou_raw.osm.pbf china-latest.osm.pbf # 或者用多边形边界裁剪(推荐,贴合真实市域边界) osmium extract --polygon=zhengzhou_boundary.geojson \ -o zhengzhou_raw.osm.pbf china-latest.osm.pbf--bbox方式快,但拿到的是一个矩形范围,把郑州周边县市的数据也圈进来了,后面还得再处理。--polygon方式精确到市域边界,但前提是你有一个可靠的边界文件——注意,这里的边界坐标必须是 WGS84 经纬度,如果用投影坐标系的边界文件,osmium 会直接报错。裁剪完成后,用 ogr2ogr 把 pbf 转成 shapefile 或 GeoPackage,注意 OSM 的 pbf 不是普通矢量格式,必须用专门工具转换。
# 从 pbf 提取道路要素为 GeoPackage ogr2ogr -f GPKG zhengzhou_roads.gpkg zhengzhou_raw.osm.pbf \ -sql "SELECT * FROM lines WHERE highway IS NOT NULL" \ -nln roads_all这条命令把 pbf 中的lines图层读取出来,只保留highway字段非空的要素。OSM pbf 里的道路在lines层,点层是 POI,面层是建筑物和地块,别选错。转换后得到的roads_all仍包含所有类型的道路,包括行人道,后续再按业务需求做细分。
3.2 清洗道路要素:过滤、去重、字段规整
拿到裁剪后的数据后,清洗这步决定了后续分析的上限。完整的清洗动作,我按顺序固定做四件:类型过滤、几何去重、属性规整、字段映射。
类型过滤就是把第 2 章那个 whitelist 用上,把footway、cycleway、path、track这类非机动车要素剔除。这里注意一点,track在郑州周边县市分布也不少,如果是做城区路网,建议直接过滤;如果是做全域路网含乡村道路,track需要保留,它代表田间道路,等级映射时算最低一级。
import geopandas as gpd # 读取上一步导出的全要素路网 roads_all = gpd.read_file("zhengzhou_roads.gpkg", layer="roads_all") # 过滤机动车道路类型 drivable_mask = roads_all["highway"].isin(drivable_types) roads = roads_all[drivable_mask].copy() # 按几何去重:OSM 中同一条路可能被重复描绘 roads = roads.drop_duplicates(subset="geometry") # 去掉零长度要素(有些编辑操作会留下退化几何) roads = roads[~roads.geometry.is_empty & (roads.geometry.length > 0)] print(f"清洗后剩余要素数: {len(roads)}")drop_duplicates(subset="geometry")处理的是 OSM 里常见的重复描绘问题——同一条路在不同时期被不同编辑者画了两次,几何完全一致或高度重合。length > 0过滤掉退化的点状要素和零长度线段,这些要素在后续拓扑构建中会引发不可预知的错误。两行命令,能省掉后面网络分析时一半的崩溃现场。
属性规整这步要和业务需求结合。比如oneway字段,OSM 里的值是yes/no/true/false混用,还有空值。统一成1/0/-1三种状态,后面构建有向图才清爽。
| 原始值 | 规范化值 | 含义 |
|---|---|---|
| yes / true / 1 | 1 | 单向,沿几何方向 |
| no / false / 0 | 0 | 双向 |
| 空 / other | 0 | 默认双向 |
| -1(极少见) | -1 | 单向,逆几何方向 |
maxspeed字段的处理逻辑也类似:缺失值按道路等级填默认限速,none值的路段按无明确限速的最高等级处理。这些默认值不完美,但比让分析程序读到一个 NaN 直接崩溃强得多。
3.3 道路分级映射:为什么要把 highway 转成 road_class
OSM 的highway分类粒度很细,但很多业务场景不需要这么细。比如做城市噪声传播模拟,只需要快速路、主干道、次干道、支路四个级别;做物流路径规划,只关心高速、快速、普通公路三个级别。把 highway 标签映射到业务分级,是“已处理”数据里最体现功力的部分。
# highway 到行业通用四级分类的映射 road_class_map = { "motorway": 1, # 高速 "motorway_link": 1, "trunk": 2, # 快速路 "trunk_link": 2, "primary": 3, # 主干道 "primary_link": 3, "secondary": 4, # 次干道 "secondary_link": 4, "tertiary": 5, # 支路 "tertiary_link": 5, "unclassified": 5, "residential": 5, "service": 6 # 服务性道路 } roads["road_class"] = roads["highway"].map(road_class_map) # 未映射的值(此时不应对存在)标记为 0,便于事后检查 roads["road_class"] = roads["road_class"].fillna(0).astype(int) # 看看分级后的结果分布 print(roads["road_class"].value_counts().sort_index())映射表里我把tertiary和unclassified、residential都归为支路级别,因为在实际道路属性上,这三者的通行能力差异不大。如果你做的是车流仿真,可能需要更细的划分——比如给service单独一个级别,甚至拆出alley(巷弄),这完全取决于业务模型需要多少个状态。分级映射没有绝对正确答案,关键是映射逻辑要固化在代码里,可审计、可修改、可复现。
映射完之后还有一个关键操作:按道路等级做几何融合。OSM 里的主干道经常被切成几十上百段,每段长度甚至不足百米,直接做网络分析会让节点数量爆炸。用dissolve把同等级、同路名、同 ref 的相邻线段合并成完整路段:
# 按 road_class + name + ref 融合相邻线段 dissolve_fields = ["road_class", "name", "ref"] roads_dissolved = roads.dissolve(by=dissolve_fields, aggfunc="first").reset_index() # 融合后计算每条路段的长度(需已投影到米制坐标系) roads_dissolved["length_m"] = roads_dissolved.geometry.length print(f"融合前要素数: {len(roads)},融合后: {len(roads_dissolved)}")融合的做法在高速路和国道上效果最明显——G107 在郑州境内往往被切成上百段,融合后变成几条完整贯通的路段。注意dissolve的by参数接受字段名列表,aggfunc="first"表示融合后保留第一个要素的属性值,这里因为融合字段本身已经包含在分组里,其他属性如maxspeed取首个值即可,基本不影响准确性。
4. 避坑:OSM 道路数据处理的五个经典翻车现场
4.1 边界裁剪导致路网断裂,市域边缘的路全成了断头路
现象:用 bbox 或行政边界裁剪后,通往周边县市的路在边界处全部截断,跨越市界的道路在郑州侧变成不连通的悬空端。做路网连通性分析时,边界一带的节点度全部为 1,统计结果完全失真。
原因:OSM 的道路要素是完整的折线,跨市界的路一条要素可能横跨两个城市。按边界裁剪时,几何被硬生生切断,切断处不会自动生成新的拓扑节点。
解决:裁剪时不要把边界切得太死。我的做法是先按行政边界外扩 5 公里生成缓冲区,用缓冲区做要素裁剪,再对裁剪结果做拓扑修复——找到所有悬空端点,如果悬空点距离另一条路的端点小于阈值(一般取 10 米),就把它们合并。这样既能保住边界处的连通性,又不至于把周边县市几十万条小路都圈进来。
4.2 G107 在数据里被拆成五十段,属性和名称全是乱的
现象:在郑州路网里查 G107,结果拉出来几十段要素,长度从几十米到几公里不等,name字段有的叫“G107”、有的叫“国道 107”、有的直接为空,ref字段也是同样的混乱。
原因:OSM 是众包编辑,不同人编辑的路段接合处属性不统一。更麻烦的是,同一条路可能被不同编辑者各画了一段,首尾相连但属性的写法各不相同。
解决:属性规整时不要只盯着name洗数据,改为建立“ref 优先、name 兜底”的融合策略。处理流程上,dissolve的分组字段里,ref 有值就用 ref,ref 缺失才考虑用 name。正是因为这种不确定性,处理数据时必须保留融合前的原始属性备查,不要把原字段覆盖掉。
4.3 投影坐标弄错,郑州的路网跑到了海里
现象:加载数据后发现,郑州的路网和底图完全对不上,图层跑到渤海去了,或者整个路网旋转了一个奇怪的角度。
原因:几何坐标是 WGS84 经纬度(EPSG:4326),却被按 Web Mercator(EPSG:3857)或某个投影坐标来渲染,或者投影时参数用错。这类问题在初学者身上反复发生,因为 QGIS 和 ArcGIS 对无坐标系数据的默认处理方式不同,一个默认按 WGS84 显示,另一个默认按投影坐标显示。
解决:拿到数据的第一个动作永远是检查 CRS,不是看数据范围。在 QGIS 里看图层属性里的坐标系,在 Python 里用gdf.crs打印。凡是 OSM 原始数据,一律先确认是 EPSG:4326,再按目标坐标系显式转换。转换时写好目标 EPSG 代码,不要选“默认”或“自动”。
4.4 郑州主城区路网要素数量爆炸,加载一次卡五分钟
现象:把“已处理”的郑州路网全部加载进 QGIS 或 Leaflet,操作一下卡半天,缩放都要等一两秒。
原因:城市级路网即使过滤掉步行道,service和residential这两类低等级道路的要素数量也极其可观。郑州主城区的 service 道路可以占到全部机动道路要素的六成以上,这些短小的内部路在可视化场景中往往不需要全部显示。
解决:处理时把路网按等级拆成多层输出——高速/快速路一层、主干道/次干道一层、支路/服务道路一层。做可视化时先加载高层级路网,缩放级别深入到街区再加载低等级图层。做分析时按需只加载对应层,不要一股脑全导入。这个分层策略看起来没什么技术含量,实际使用中比任何性能优化都管用。
4.5 同一条路被画了两遍,路网长度统计直接翻倍
现象:统计郑州路网总长度,算出来比实际里程多出一大截;做路网密度分析,某些区域密度高到不合常理。
原因:OSM 的编辑历史里存在重复绘制的线段——同一条路在不同时间被不同的人各画一遍,几何完全重合或基本重合。这类重复要素在属性表里看不出问题,但会在长度统计、面积分析、路径规划中造成双重计数。
解决:用几何去重加拓扑检查双层过滤。几何去重用drop_duplicates(subset="geometry")处理完全重合的情况;部分重合的,做一次“线段重叠检查”——将图层与自身做空间连接(intersects),筛选出互相重叠超过 80% 的要素对,人工抽检后决定合并还是删除。处理完后,用不同等级道路的里程统计做个合理性验证:郑州的高速公路里程、快速路里程应该和统计年鉴的数量级对得上,对不上就说明还有重复没清干净。
5. 拿到“已处理”数据后,先做这三件事验证它是否真的可用
5.1 三个步骤,验证一份处理好的路网数据是否靠谱
第一步,验证坐标系。打开属性表看 CRS,如果是投影坐标系,随手量一条路的长度——中州大道从黄河桥到南四环大约 30 多公里,如果量出来是 3 或 300,坐标系十有八九有问题。第二步,做拓扑检查。在 QGIS 里用拓扑检查插件(Topology Checker)跑一遍“dangling edges”规则,看有没有大量悬空端点。城市路网中悬空端点的存在不可避免,但悬空比例超过 5% 就要警惕,说明数据准备阶段有系统性问题。第三步,对照卫星影像抽检。随便挑五个区域,把路网叠加在影像底图上,看道路走向和实际建筑、田野的分布是否吻合。OSM 在中国的覆盖质量存在地域差异,郑州主城区数据质量较好,郊区可能有些路是失真的。
以上三步都过了,这份“已处理”数据基本可以信任。还有一条经验:拿到任何第三方处理好的数据,先跑一遍简单的统计分析——各等级道路里程分布、要素数量、平均长度。如果 output 的结果和你对郑州路网结构的认知出入太大,别急着用,先去问数据处理方要处理报告,没有处理报告的数据就是个黑匣子。
5.2 从路网到分析:构建拓扑图做路径规划
“已处理”数据的终点不是画图好看,而是进入分析流程。我常用的进阶做法是把路网转成网络图,做最短路径或等时圈分析。这一步用osmnx或networkx都可以,核心是把路网几何转成图。
import networkx as nx # 假设 roads_projected 是已处理、已投影的郑州市路网 GeoDataFrame G = nx.Graph() # 遍历每条道路要素,把端点作为节点,整条线作为边 for idx, row in roads_projected.iterrows(): geom = row.geometry if geom.geom_type == "LineString": start = (geom.coords[0][0], geom.coords[0][1]) end = (geom.coords[-1][0], geom.coords[-1][1]) weight = row["length_m"] G.add_edge(start, end, weight=weight, length=weight, road_class=row["road_class"]) # 求中州大道附近两个点之间的最短路径 path = nx.shortest_path(G, source=start_point, target=end_point, weight="weight") print(f"最短路径经过 {len(path)} 个节点,总长度 {nx.shortest_path_length(G, start_point, end_point, weight='weight'):.0f} 米")这个构建过程把每条 LineString 的首尾坐标当作图的节点,整条线的长度作为边权。需要提醒的是,这种简单构建方式不会自动处理真实道路的交口——两条路几何交叉但 OSM 里没有公共节点时,图里就不会有交叉点。更完整的方案是把节点捕捉到最近的道路端点上,或者用专门的路网工具包处理。不过对于路段级别的宏观分析,这种简化模型已经能给出有价值的结论。
这些年处理过不少城市的 OSM 路网,回头看最深刻的教训是:三分数据七分处理,OSM 的数据永远是“原材料”,不是“成品”。别拿到手就用,先把坐标系、字段、拓扑三关过了,再谈分析。如果你手上这份郑州数据已经把这层功夫下足了,那确实值得认真用起来。希望帮到你。
本文还有配套的精品资源,点击获取