简介:内蒙古路网矢量数据以Shapefile线矢量格式组织,完整覆盖自治区内高速公路、国道、省道、县道等各级道路,既记录道路几何走向,也包含道路等级、名称、限速等基础属性,适合地理信息数据处理人员、交通规划师和地图制图工作者使用,可直接用于道路网络可视化、空间查询与路网结构分析。压缩包共7个文件,包括承载几何形状的.shp文件、保存属性信息的.dbf文件、定义坐标投影的.prj文件、提升检索效率的空间索引文件以及配套元数据,整体约34.32MB。已有765人浏览学习。数据可加载到ArcGIS、QGIS等常用平台进行编辑与处理,能完成道路长度统计、路网密度与连通性计算,也可与土地利用、人口分布、气候信息叠加,服务于交通规划、环境影响评估、导航底图制作和灾害应急响应路线设计,是开展内蒙古区域研究和实际项目的基础性地理数据集合。
1. 某自治区路网矢量数据:拿到 shp 之后,真正的活才刚刚开始
做路网分析、交通规划或地图可视化的人,几乎都会在某一天拿到一份「某自治区路网矢量数据,shp格式」的文件包。打开一看,几十万条线段铺满屏幕,属性表里有道路等级、名称、编号——看上去挺完整。但等你真的用它做网络分析、算可达性、出专题图,问题就冒出来了:坐标系对不上、拓扑错误多、断头路遍地都是。这份数据不是拿来就能用的,它像一份未经整理的原材料,需要你按确定的步骤清洗、配准、修复,才能真正进分析流程。
这篇文章就是讲这条完整链路的:从认识 shp 格式的结构开始,到处理坐标系、修复拓扑、规避常见坑,最后落到网络分析与可视化。适合刚接触矢量路网数据的新手,也适合被某类边界问题卡住的老手。我尽量把参数、命令和踩过的坑都写清楚。
2. 先搞清楚 shp 的底细:一整个文件家族,缺一个都麻烦
很多人以为「shp 格式」就是那一个后缀为 .shp 的文件,真处理起来才发现,它是一个文件家族。用 GIS 软件打开一份 shp 数据,背后其实有至少三四个文件在协同工作,缺一个,轻则打不开,重则属性表一片空白或者图形错位。
2.1 shp 不是单个文件:主文件、索引、属性表、投影文件各管什么
一份完整的 shp 数据通常包含以下文件(不要求全部存在,但缺了会出问题):
| 文件后缀 | 作用 | 缺失后果 |
|---|---|---|
| .shp | 存储几何图形(点、线、面) | 无图形 |
| .shx | 几何索引,加速读取 | 打开变慢,部分软件报错 |
| .dbf | 属性表,存道路名称、等级等字段 | 图形还在,属性全丢 |
| .prj | 投影/坐标系描述文件 | 软件不知道数据在哪,显示乱飞 |
| .cpg | 属性表编码声明 | 中文乱码 |
拿到数据先做一件事:在文件管理器里把后缀显示打开,看这一套文件齐不齐。最常见的问题就是 .prj 缺失——对方导出时没带投影文件,你打开后图形挤在地图窗口左下角,或者与底图完全对不上。这种情况别急着骂数据,先按第 3 章的方法重新定义坐标系就行。
2.2 用 ogr2ogr 快速检查数据完整性
我一般拿到新数据,第一件事就是用 GDAL 的命令行工具做一次体检,比打开桌面软件快得多:
ogrinfo -so 某自治区路网.shp 某自治区路网这里-so表示只输出概要信息(summary only),后面跟文件路径和图层名。输出里重点看三样:Feature Count(要素总数)、Extent(空间范围)、Geometry(几何类型)。如果 Geometry 显示Line String或Multi Line String,说明是路网该有的线要素;如果显示3D Line String,注意数据带 Z 值,后面做投影转换时要小心维度的变化。Extent 如果正常落在某个小数值范围内,比如经度一百零几、纬度四十左右,说明文件自带正确坐标系统;如果 Extent 显示上千上万的值,说明这数据是投影坐标,不是地理坐标。
2.3 属性表里藏着什么:道路等级、名称、编号,先摸清字段再动手
打开 .dbf 属性表,常见的路网字段有:道路名称(name)、道路等级(class/level)、道路编号(code)、车道数(lanes)、限速(speed_limit)、路面材料(surface)等。不同来源的数据字段名差别很大,有的用中文,有的用拼音缩写,比如DLDJ(道路等级)、DLMC(道路名称)这种。
拿到数据第一步,先用 ogrinfo 把字段清单打出来:
ogrinfo -so -al 某自治区路网.shp | grep -E "name|type"看字段名和字段类型。这步很重要,因为后续做拓扑修复、按等级筛选道路,都要依赖字段名。如果字段名是中文且在地理数据库里,导出成 shp 时经常会被截断成 10 个字符——这是 shp 格式的历史限制,字段名最长 10 字节,中文一个字符占 3 字节,所以中文字段名经常变成乱码或残缺。遇到这种情况,我一般用 Python 的 pyshp 库重写一份字段名规范的数据,后面会讲到。
提示:shp 格式本身有 2GB 上限。如果你的路网数据超过这个体量(大范围高精度路网偶尔会),建议转成 GeoPackage 或 PostGIS 再处理。不过以某自治区范围来看,普通路网数据一般达不到这个上限。
3. 坐标系是路网数据的第一道坎:先确认,再转换,最后才敢动
坐标系搞错是路网数据最常翻车的环节。某自治区范围跨度大,东西向跨度超过 20 个经度,南北也有十几个纬度。拿到数据先分清楚它是地理坐标系(经纬度,单位是度)还是投影坐标系(单位是米),这一步决定了后面所有距离计算和空间分析的准确性。
3.1 区分地理坐标和投影坐标:最直接的判断方法
打开 QGIS 或者 ArcGIS 的图层属性,看坐标系显示。如果显示的是类似「GCS_WGS_1984」或「CGCS2000」,单位是度,那就是地理坐标。如果显示的是「某投影带」、「某兰勃特投影」之类的,单位是米,那是投影坐标。还有个更快的土办法:看 Extent 数值。
用 ogrinfo 打出来的 Extent 如果四个数值都在 -180 到 180、-90 到 90 之间,基本可以确定是经纬度。如果范围值大到几十万甚至上百万,那是投影坐标的米制单位。还有一些数据没有 .prj 文件,打开后在 GIS 软件里显示在原点附近,或者需要你手动指定坐标系。这个阶段最容易踩坑:数据明明是中国境内路网,却被默认指定成了 WGS84 经纬度,叠加在别的底图上差了十几公里,看起来像路网长歪了。
3.2 将路网数据统一到 CGCS2000 投影:用 pyproj 做精确转换
国内的路网数据,常见做法是统一到 CGCS2000 坐标系。为什么要换投影?因为地理坐标的度不是等距单位,直接算距离会得出荒谬的结果。比如在同一经度差下,北纬 45 度的实际距离比赤道附近小得多。做路网分析,距离是关键参数,所以必须把它换到等距的投影坐标下。
用 Python 的 pyproj 库做转换是成熟的做法。以下代码实现从 WGS84 经纬度转换到 CGCS2000 高斯-克吕格投影(按某自治区位置选择对应的中央经线分带):
import pyproj from shapely.geometry import shape import fiona # 定义源坐标系:WGS84 地理坐标 src_crs = pyproj.CRS.from_epsg(4326) # 定义目标坐标系:CGCS2000 / 高斯-克吕格投影,中央经线 111°E # 根据某自治区中东部区域位置,选 111°E 分带;若数据偏西/东,改 central_longitude 即可 dst_crs = pyproj.CRS.from_proj4( "+proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs" ) transformer = pyproj.Transformer.from_crs(src_crs, dst_crs, always_xy=True) with fiona.open('某自治区路网.shp', 'r') as src: # 创建输出文件,坐标系用目标 CRS 的 WKT meta = src.meta meta['crs'] = dst_crs.to_wkt() with fiona.open('某自治区路网_cgcs2000.shp', 'w', **meta) as dst: for feat in src: geom = shape(feat['geometry']) # 逐坐标点转换 new_geom = transform_geom(geom, transformer) feat['geometry'] = new_geom dst.write(feat)central_longitude的参数要根据数据所在经度范围调整。某自治区东西跨度大,如果数据覆盖全区,需要判断是按 105°E、111°E 还是 117°E 分带。一个简单的经验:查看原始数据的 Extent 中心经度,选择离它最近的标准分带中央经线。如果覆盖范围横跨两个分带,建议把数据按经度切片分别投影,或者改用兰勃特等角圆锥投影——它特别适合东西狭长、跨分带的区域。
注意:如果数据源已经是 CGCS2000 地理坐标(经纬度),不要再走 WGS84 转 CGCS2000 的路径,直接做高斯-克吕格投影即可。中间多一次转换等于多引入一次误差。
3.3 QGIS 里批量重投影:不想写代码时的图形化方案
如果你不想碰代码,QGIS 的「导出」功能就能完成重投影。右键图层 → 导出 → 要素另存为,在「CRS」选项里选择目标坐标系。要点是勾选「将选中的要素保存为」旁边的「几何图形」选项,并确保输出文件名不含中文字符或空格——这个问题后面会在避坑章节细说。
QGIS 还有一个好处:处理完后你可以把转换结果和原始数据同时加载,用「坐标窗口」里的数值对比。选中同一个路口,转换前后经纬度应该能对应上;如果差了十万八千里,说明中央经线选错了,重来。
4. 路网拓扑修复:断头路、重复线、相交未打断,分析前必须清干净
坐标系搞定之后,路网数据能正确显示了,但还不能直接用来做网络分析。真实路网数据在数字化过程中会产生大量拓扑错误:两条路相交处没有打断、同一条路被重复数字化、本该相连的断口莫名断开。这些错误在平面视图上看不出来,一跑网络分析就全部暴露:路径计算抄近路、可达性范围严重偏离、距离统计翻倍。
4.1 用 Python + shapely 做相交打断:把每条线在交点处切开
路网分析的基本要求是:两条路相交的位置,必须有同一坐标的节点。如果各自穿过而没有交点,路径规划算法会认为它们是两条不相干的路,无法转弯。检查方法很直观:把路网叠加在高分辨率影像上,看交叉路口是否存在「十字线中间没有节点」的情况。手动修复不现实,用 shapely 的union操作可以批量打断:
import geopandas as gpd gdf = gpd.read_file('某自治区路网_cgcs2000.shp') # 用 unary_union 把所有线在相交处打断成小段 # 注意:这会消除原始线的属性关联,所以先在打断前记录原线索引 gdf['seg_id'] = range(len(gdf)) lines = gdf.geometry.values # 逐线打散,并保留原线 ID 字段 segments = [] for idx, line in enumerate(lines): # 如果几何是 MultiLineString 或 LineString,统一处理 if line.geom_type == 'LineString': segs = [line] else: segs = list(line.geoms) for seg in segs: segments.append({'geometry': seg, 'orig_idx': idx}) seg_gdf = gpd.GeoDataFrame(segments, crs=gdf.crs) # 执行几何合并打断 from shapely.ops import unary_union, linemerge merged = unary_union(seg_gdf.geometry.values) # 将打断后的结果拆成 LineString 段 result_lines = [] if merged.geom_type == 'MultiLineString': result_lines = list(merged.geoms) else: result_lines = [merged]unary_union会把所有线的相交处自动切开。但要注意,这个操作会丢掉属性表——每条打断后的线段和原始道路的从属关系需要你自己记录。上面的代码用orig_idx保留原线索引,但还不足以精确对应:一条路可能被切成 5 段,这 5 段都应继承原路属性。实操中我一般用空间连接(spatial join)来做属性重关联,或者用 QGIS 的「线相交」工具处理体积较小的数据。如果数据量在几万条以内,直接在 QGIS 里跑「处理工具箱 → 矢量几何 → 线相交」更快。
4.2 修复悬挂点与断头:识别真正的断点和伪断点
打断之后,下一个问题是悬挂点(dangling node)。一条路的端点没有与其他任何路相连,就是一个悬挂点。在路网中,有的悬挂点是正常的——路的尽头、边界不完整、未连接的匝道。但绝大多数的悬挂点意味着数据缺失或数字化错误。
用 shapely 找出所有线端点,再判断每个端点是否落在其他线上:
from shapely.geometry import Point # 收集所有线的端点 endpoints = [] for line in result_lines: coords = list(line.coords) endpoints.append(Point(coords[0])) endpoints.append(Point(coords[-1])) # 构建空间索引,判断每个端点是否被其他线覆盖 from shapely.strtree import STRtree tree = STRtree(result_lines) dangling = [] for pt in endpoints: # 查询与端点距离小于 1 米(容差)的线 nearby = tree.query(pt) matches = [l for l in nearby if l.distance(pt) < 1.0] if len(matches) == 0: dangling.append(pt)容差设置很关键。路网数据在数字化时不可能精确到毫米级,端点离相邻线有 0.5 米的间隙很常见,所以容差至少要设在 1 米。但这个值不是随便拍的:容差太小会把所有半连接的点都当成断点,容差太大又会把真正的断路误判为连接。建议先用 1 米跑一遍,统计悬挂点数量和分布,再按数据密集程度调整到 0.5 米或 2 米。我通常会把悬挂点导出成点图层,叠在影像上看一遍,确认哪些是真实断头路。
4.3 删除重复和冗余节点:别让一条路有几百个坐标点
还有一种常见问题是重复几何。同一条路被数字化了两遍,两条线形状完全相同但属性表里是两条记录,查询时会出现大量重复结果。用geopandas的drop_duplicates可以处理严格重复的几何,但对于部分重叠的线,需要手动判断。
冗余节点是另一个容易忽略的点:一条几公里的路,节点密度高的地方可能每隔几米一个点,这通常是从高精度 GPS 轨迹生成的,但会造成文件体积膨胀和分析变慢。QGIS 的「简化几何」工具可以按道格拉斯-普克算法简化,容差设 1~5 米即可,这个操作能去掉 30%~60% 的节点而不影响路网形状。
ogr2ogr -simplify 3 某自治区路网_简化.shp 某自治区路网_cgcs2000.shp-simplify 3表示简化容差为 3 米。这个参数不是越大越好——城市道路密集区域,3 米足够;在乡村道路弯道多的地方,3 米可能把弯道拉直,影响距离精度。我会先做一版 1 米简化对比一下,再决定最终容差。
5. 路网数据避坑指南:坐标系、编码、字段名,这些坑我全踩过
这一章是血泪经验的汇总。路网矢量数据处理翻车的概率非常高,但翻车点也高度集中。以下是我处理各类路网数据时反复遇到的 5 类问题,每条都有现象、原因和解决办法。
5.1 打开属性表全是乱码:编码声明缺失或错误
现象:在 QGIS 里打开某自治区路网.shp,属性表里的中文道路名称变成「锟斤拷」或「æ°å»ºè·¯」这类乱码。
原因:shp 的属性表 .dbf 文件使用 DBF 格式,编码信息不在文件内部,而是靠系统猜测。有的数据是 GBK 编码,有的是 UTF-8 编码,导出时没有写明 .cpg 文件,读取方默认按本地系统编码读取,一旦猜错就是满屏乱码。
解决:在 QGIS 中右键图层 → 图层属性 → 源 → 数据源编码,手动切换 GBK / UTF-8,能解决大多数情况。另一种通用方法是把 .dbf 用 Python 的 pandas 读取并重写编码:
import pandas as pd # 按 GBK 读取,再另存为 UTF-8 的 CSV df = pd.read_csv('某自治区路网.dbf', encoding='gbk', dtype=str) df.to_csv('某自治区路网_属性表.csv', index=False, encoding='utf-8-sig')更彻底的做法是写一个脚本批量修改 .dbf 并重新生成 .cpg 文件。如果你多次处理来自不同单位的数据,建议做一个统一入口:进来先用 ogrinfo 检查编码,再决定读入方式。
5.2 数据打开后飞到地图外:坐标系定义错误导致偏移
现象:叠加底图后,路网出现在非洲海岸或太平洋上,或者偏了好几公里。
原因:.prj 文件缺失或写错。最典型的场景是数据本来就是 CGCS2000 坐标,但系统默认加载成 WGS84 经纬度,导致显示在完全错误的位置。另一种情况是数据是投影坐标(米制单位),但被当作经纬度读取——数值不能超过 180,当然直接定位错误。
解决:加载数据后,先看 Extent。如果范围值在几十万分米级别,说明是投影坐标被当成经纬度读了。给图层手动指定正确的坐标系,再执行重投影。还有一个排查习惯:同时加载一份已知正确位置的点数据,检查同一地物在两个图层中的偏移量是否一致。偏移量一致且数值很大,基本就是坐标系定义问题,而不是数据本身错了。
5.3 网络分析结果离谱:拓扑错误没修完
现象:跑 OD 成本矩阵或者最短路径,路线从地图上凭空横穿大片区域,无视道路边界。
原因:这一般发生在数据清洗阶段没做相交打断。两条路在空间上交叉,但属性上没有交点,网络分析算法会认为它们无法互通,路径只能绕路或者直接跳跃到最近节点。还有一种情况是节点捕捉失败——两条线的端点相距不到 1 米,在可视化时看似相连,但分析时节点不重合。
解决:回到第 4 章的打断流程,先把所有相交线打断,再检查随后产生的悬挂点。最后一个实用技巧:把容差设为 1 米再做一次「线转点 + 空间连接」,把 1 米内的端点吸附到同一点。这一步虽然会损失一点点几何精度,但换来的是网络分析结果的可靠性。
5.4 边界区域的路突然断了:裁剪或合并时丢几何
现象:某自治区与相邻区域交界处的道路,在半途突然中断,没有延伸到边界。
原因:数据是按行政区域分幅裁剪的,边界处本应与相邻区域数据拼接。如果只拿到了其中一块,路网自然在边界处截断。这不是数据错误,而是覆盖范围问题——没有邻区数据,谁都补不上这个缺口。
解决:确认数据来源和覆盖范围。如果只是做某自治区内部的分析,边界上的断头路可以接受,但要意识到这会影响边缘区域的可达性计算结果。如果分析目标是跨边界的路网联通,就必须拼接邻区数据,或者从开放来源补齐边界区域路网。这类错误冒充数据质量问题去修,是修不出来的。
5.5 shp 文件超过 2GB 跑不动
现象:QGIS 打开数据后内存占用飙升,经常崩溃。检查发现文件在 1.8GB 附近。
原因:shp 格式本身有 2GB 的硬上限,但接近上限时读取效率已经急剧下降,纯几何存储效率不高。
解决:转成 GeoPackage 格式(.gpkg),它是基于 SQLite 的开放格式,没有 2GB 限制,读取速度更快。转换命令:
ogr2ogr -f GPKG 某自治区路网.gpkg 某自治区路网.shp如果只是为了分析,还可以把数据导入 PostGIS,用空间索引加速查询。这个转换在数据量超过 1GB 时基本是必须的操作,别等到崩溃再后悔。
6. 进阶:基于清洗后路网的分析与可视化实操
数据修干净之后,我通常用三种方式验证效果,并产出实际分析结果。第一步是出图检查:把路网按道路等级分层配色,城市道路用深色粗线,乡道和高速用不同颜色区分,一眼就能看出断头路和乱线是否清干净。第二步是跑一个最简单的最短路径测试,从 A 城到 B 旗,看路线是否合理、里程是否符合常识。第三步才是正式分析。
6.1 按道路等级做路网密度专题图
用 QGIS 的「热力图」插件或者 Python 生成密度网格,能快速看出路网的分布特征。密度网格的做法是把研究区域划分成 1km × 1km 的格子,统计每个格子内道路总长度:
import geopandas as gpd import numpy as np from shapely.geometry import box gdf = gpd.read_file('某自治区路网_cgcs2000.shp') # 获取数据范围 minx, miny, maxx, maxy = gdf.total_bounds # 生成 1km 网格 cell = 1000 # 单位与投影坐标一致,这里是米 cols = int(np.ceil((maxx - minx) / cell)) rows = int(np.ceil((maxy - miny) / cell)) grid_cells = [] for i in range(cols): for j in range(rows): grid_cells.append(box(minx + i*cell, miny + j*cell, minx + (i+1)*cell, miny + (j+1)*cell)) grid = gpd.GeoDataFrame({'geometry': grid_cells}, crs=gdf.crs) # 空间连接统计每个网格内的道路长度 # 更高效的方式是先裁剪再按网格分组聚合 intersected = gpd.overlay(gdf, grid, how='intersection') intersected['len'] = intersected.geometry.length density = intersected.groupby('grid_id')['len'].sum().reset_index() # 结果转成面图层,字段 len 即该网格内道路总长度(单位:米)网格尺寸的选择有讲究。城市区域用 500m 网格能看到街区尺度差异,乡村区域用 5km 网格更合适。如果你做的是全自治区尺度的对比,1km 网格是折中方案。生成的密度图层可以做分级着色,直观展示路网的空间分布不均——这东西放在分析报告里比任何文字都好用。
6.2 计算任意两点的实际路网距离:替代直线距离的可靠方案
直线距离(欧氏距离)在某些场景下严重失真,山区尤为明显。有了拓扑正确的路网,就可以用网络分析算实际交通距离:
import networkx as nx # 构建图:每条线转为边,长度作为权重 G = nx.Graph() for _, row in gdf.iterrows(): line = row.geometry length = line.length start = line.coords[0] end = line.coords[-1] G.add_edge(start, end, weight=length) # 查找最近节点 from shapely.geometry import Point orig = Point(112.3, 41.5) # 注意这是经纬度,要先投影 orig_proj = transformer.transform(orig.x, orig.y) # 找到离起点最近的节点 nearest_node = min(G.nodes, key=lambda n: ((n[0]-orig_proj.x)**2 + (n[1]-orig_proj.y)**2)**0.5) # 同样找到终点最近节点,然后算最短路径 path = nx.shortest_path(G, nearest_node, target_node, weight='weight') distance = nx.shortest_path_length(G, nearest_node, target_node, weight='weight')这段代码把路网转成了 NetworkX 图,边权重是实际长度。有一个细节:G.add_edge时用的是每个 LineString 的起终点坐标,如果一条路有多个中间点(弯道),路网会被简化成直连线段,长度可能偏短。更精确的做法是用中间点建节点、把相邻中间点连成边,或者直接在add_edge时把length设为整条线总长,两个端点的度数小于 2 也能凑合。实际项目中我一般选用后者,数据量大时效率更高。
6.3 我的收尾习惯:出一张质检图,留一份问题清单
每次做完路网清洗,最后一步不是跑分析,而是存一个「质检记录」。我用 QGIS 的 Atlas 功能按旗县分页导出路网图,叠加原始数据的背景透明度,人工抽查几个疑难路口;同时在项目文件夹里留一份 txt,记录这次处理用了哪些参数:坐标系转换用的中央经线、简化容差 3 米、悬挂点容差 1 米、编码是 UTF-8。为什么要留这个?因为某自治区路网这种数据大概率不是一次性的,下个月新数据进来,你还要用同样的参数处理,不然两次结果对不上。
这个过程我前前后后跑过好几轮,每次踩的坑都集中在坐标系和拓扑这两件事上。先把这两件事做扎实,后面的分析怎么跑都稳。希望帮到你。
本文还有配套的精品资源,点击获取