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

资讯详情

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

辽宁省道路数据处理:shp文件配对、坐标统一与断头路修复

辽宁省道路数据处理:shp文件配对、坐标统一与断头路修复 简介一份面向地理信息系统开发、城乡规划与交通研究者的辽宁省道路矢量数据集分级精细到乡道可直接用于地图制图、空间分析与路网建模。压缩包内含97个文件以12套Shapefile核心数据为主配套属性表、投影文件、空间索引及元数据另附1张预览图总大小约67.77MB。数据覆盖城市一级至四级道路以及高速、国道、省道、县道、乡道等行政等级道路同时整合OSM来源的铁路/地铁/轻轨与各级道路图层既能用于宏观路网展示也能支撑微观道路分类研究。目前已有206人浏览/学习适合需要现成矢量底图进行制图、叠加分析或规划辅助的从业者与学生借助属性表与投影文件可快速拼接、裁剪和符号化提升前期数据处理效率。1. 为什么这套辽宁道路数据比单层 shp 更值得拆拿到辽宁省道路数据压缩包的第一反应是“文件又多又乱”但仔细捋一遍会发现它其实做了三层分类第一层是行政等级道路包含高速、国道、省道、县道、乡道第二层是城市内部道路按一至四级划分第三层是 OSM 来源的道路和铁路地铁轻轨数据。一共 16 种道路类型文件后缀涵盖 shp、dbf、prj、cpg、sbn、sbx、shx 和 xml这意味着一份 shp 实际上由至少 5 个配套文件组成。如果只拖拽 .shp 进软件很容易因为缺少 .prj 或 .dbf 导致坐标系丢失或属性表为空。这套数据的价值在于“分级精确到乡道”对做乡镇级路网规划、物流路径分析和国土空间规划底图的人来说比只给高速国道省道的通用路网要细得多。但反过来分级过细也带来两个问题一是多个图层之间边界重叠、属性字段口径不一致不能直接合并二是城市道路和行政等级道路之间存在重复比如一条省道穿城而过时既出现在省道图层里也出现在城市一级道路图层里。适合用它的人是手里已经有行政区划边界、想做路网密度统计或通达性分析的从业者而不是只要一条全国粗线缆的普通用户。2. 解开压缩包后的文件配对逻辑与坐标系核对2.1 shp 不是一个文件而是一组文件缺一不可Road 数据包里的每个图层例如乡道_辽宁省.shp实际配套了至少 6 个文件.shp存几何要素.dbf存属性表.shx是几何索引.prj记录坐标系.cpg指定字符编码.sbn和.sbx是 ESRI 的空间索引。很多人把.rar解压后只复制.shp到工作目录结果属性表字段全乱码或软件提示“无法打开”。这里最容易被忽略的是.cpg它决定了 dbf 里的中文字段是用 UTF-8 还是 GBK 读取。# 解压后建议先用 ogrinfo 检查完整性直接读取一个图层的元信息 ogrinfo -so 乡道_辽宁省/乡道_辽宁省.shp 乡道_辽宁省 # 输出示例 # INFO: Open of 乡道_辽宁省.shp using driver ESRI Shapefile successful. # Layer name: 乡道_辽宁省 # Geometry: Line String # Feature Count: 15234 # Extent: (119.526575, 38.694588) - (125.788512, 43.481344) # Layer SRS: # WGS 84ogrinfo -so只读概要不遍历全部要素适合解压后快速验证文件是否完整。如果输出里出现Feature Count: 0或直接报错说明.shp和.shx配对出了问题重新解压即可。注意这里Layer SRS显示的是WGS 84说明数据是经纬度坐标系没有做投影后续算长度和面积前必须先转投影坐标系。2.2 用一个脚本批量核对所有图层的坐标系和要素数量这套数据里 16 个图层分散在不同目录手动一个一个看很容易漏。更稳妥的方式是用 Python 的geopandas读取目录下所有 shp输出一个对照表重点看三项要素数量、几何类型、坐标系是否统一。行政等级道路和城市道路理论上都来自同一套基础数据但 OSM 来源的图层则不同OSM 原始数据多为 WGS84 地理坐标系而国内编辑过的数据可能已经被转换为 CGCS2000两者混用会导致叠加时偏移几百米。import geopandas as gpd import glob, os shp_files glob.glob(辽宁省道路数据/**/*.shp, recursiveTrue) rows [] for shp in shp_files: try: gdf gpd.read_file(shp, encodingutf-8) rows.append({ file: os.path.basename(shp), count: len(gdf), geom: gdf.geom_type.unique().tolist(), crs: str(gdf.crs), columns: list(gdf.columns[:8]) }) except Exception as e: rows.append({file: os.path.basename(shp), error: str(e)}) overview gpd.pd.DataFrame(rows) print(overview.to_string())这段代码递归读取辽宁省道路数据目录下的所有 shp并把每个图层的要素数量、几何类型、坐标系、前 8 个字段名汇集给截图做比对。逻辑上glob.glob负责递归找文件gpd.read_file读取时显式指定encodingutf-8确保 dbf 中文属性不乱码。如果某些shp 的 cpg 声明为 GBK则需要把 encoding 改成gbk否则会出现属性字段名为乱码。.cpg文件的存在并不意味着每个图形文件都一定用它geopandas默认优先读取.cpg里写的编码但遇到空.cpg时会回退到系统默认编码这就是为什么同一套数据里有的图层读取正常、有的乱码。2.3 坐标系统一CGCS2000 与 WGS84 混用时的偏移处理核对完会发现铁路地铁轻轨OSM_辽宁省.shp大概率投影信息不完整或字段结构与其他图层不同。OSM 来源数据的属性表字段名通常是name、highway、railway、lanes而行政等级道路的字段名则是道路编码、道路名称、行政等级直接合并图层会字段对不齐。常见做法是先统一坐标系再按需提取字段。# 用 ogr2ogr 将 WGS84 的 OSM 图层转换为 CGCS2000 / 3-degree Gauss-Kruger CM 123E ogr2ogr -t_srs EPSG:4548 -overwrite \ 铁路地铁轻轨OSM_辽宁_4548.shp \ 铁路地铁轻轨OSM_辽宁省.shpEPSG:4548 是 CGCS2000 三度带、中央经线 123E 的投影坐标系辽宁大部分区域用这个带能保证长度变形控制在 0.07 米以内。转换时注意-t_srs是目标坐标系不是源坐标系源坐标系不写默认从 prj 文件读取。如果原始 shp 的 prj 丢失ogr2ogr会默认按 WGS84 解释导致转换出的坐标偏差。所以解压后第一件事永远是检查 prj 是否存在而不是急着转投影。3. 按行政等级与城市道路分类做分层提取与拓扑检查3.1 字段口径差异行政等级道路和城市道路不能直接 union把高速_辽宁省.shp和城市一级道路_辽宁省.shp直接叠加时会发现城市一级道路里包含了部分穿城高速的线段但属性表完全不一样。高速层的字段是道路编号(G15)、技术等级城市道路层则是道路名称(青年大街)、道路等级(主干道)两者没有共同主键无法按字段关联合并。所以处理顺序应该是先按道路功能分层再跨层去重。常见做法是给每个图层加一个来源标记字段然后统一字段结构再按空间位置做去重或融合。import geopandas as gpd # 读取两个图层并统一字段结构 admin gpd.read_file(省道_辽宁省.shp, encodingutf-8) city gpd.read_file(城市一级道路_辽宁省.shp, encodingutf-8) admin admin[[geometry]].copy() city city[[geometry]].copy() admin[source] 省道 city[source] 城市一级 combined gpd.GeoDataFrame(gpd.pd.concat([admin, city], ignore_indexTrue), crsadmin.crs) combined combined.explode() # 拆掉 MultiPart避免一条线包含多个独立的 LineString combined combined.reset_index(dropTrue) print(combined.head())explode()是关键一步。shp 里一条要素既可以是单根 LineString也可以由多个不相连的线段组成 MultiLineString直接做后续的求交、打断、合并会把多段线当一条线处理导致长度计算错误。这里crsadmin.crs保证了合并后的 GeoDataFrame 继承源坐标系而后explode()会把 MultiLineString 拆分并丢弃原来的属性关联。如果要保留属性可以先用assign把 source 传到拆分后的每一段。3.2 用空间关系做重复线段剔除避免同一条路算两遍高速在某一段和国道共线是常见情况行政等级重叠导致统计路网里程时会把同一条路重复计两次。排除重复需要做三步先统一坐标系再用overlay做线线求交最后用长度阈值判断是否为同一段。# 假设 high 和 national 都已转成相同投影坐标 merged gpd.overlay(high, national, howunion) high[len] high.length national[len] national.length # 找出长度几乎相等且几何中心一致的路段视为重复 high[left] high.geometry.centroid national[right] national.geometry.centroid near_pairs gpd.sjoin_nearest( high.reset_index(), national.reset_index(), howinner, max_distance5 )overlay(howunion)会把两个图层的线切割成互相不重叠的线段然后sjoin_nearest通过几何中心距离判断哪些线段属于同一物理路段。这里max_distance5表示两个几何中心距离在 5 米内视为同一条路适合城区道路因为偏移产生小幅不平行的场景。把识别出的重复段从统计表中剔除再用dissolve(by...)合并成完整道路。注意overlay对线要素要求很高如果源数据里有自相交self-intersect的线会在求交时报错。遇到这种数据建议先用gdf gdf.make_valid()修复几何再执行 overlay。make_valid会拆分自相交的环并重组成合法几何但也会改变某些线段的顶点数量所以要在统一坐标系之后做不要在经纬度坐标系下做否则修复结果会基于球面弧度长度和方向都会失真。3.3 分级属性表重构把 16 个图层合并成一层带等级字段的路网这是把这个资源变成可用底图的关键一步。不要把 16 个图层当作 16 个独立要素类管理而要合成一层每条线带road_level字段取值包括高速、国道、省道、县道、乡道、城市一级、城市二级、城市三级、城市四级、铁路、地铁轻轨。字段在合成后做优先级排序当同一条路既在城市图层又在行政图层时按行政等级优先保留。-- 在城市道路属性表中增加 road_level然后 UNION ALL 到总表 ALTER TABLE city_road ADD COLUMN road_level TEXT; UPDATE city_road SET road_level 城市一级 WHERE level_code L1; UPDATE city_road SET road_level 城市二级 WHERE level_code L2; INSERT INTO unified_road (geometry, road_level, source_name) SELECT geometry, 高速, name FROM highway_layer; INSERT INTO unified_road (geometry, road_level, source_name) SELECT geometry, 乡道, name FROM township_road;把 SQL 这段当作参照流程来理解多个 shp 先各自转为 PostGIS 表再按字段映射插入到统一表。字段映射时注意城市道路的等级字段各层不统一有的是level_code有的是road_class入库前先用gpd.rename把字段名拉齐。UNION ALL保留重复行后续用空间去重处理不用UNION是因为几何字段不能简单地 DISTINCT两个图层即使代表同一条路顶点加密方式也可能不同直接 DISTINCT 去不干净。4. 乡道断头路检测与乡镇路网连通性修复4.1 用端点相交判断断头路而不是肉眼找乡镇级路网最大的坑是断头路一条乡道在图层里看起来接上了省道但实际端点有 3~5 米的间隙因为两段路来自不同图层、顶点不一致。用 QGIS 的拓扑检查插件也能做但数据量一旦上万条线段手动修改不现实。我一般用shapely做两步检查第一步找出所有线段的端点第二步判断端点是否落在其他线段上落点距离大于容差就标记为断头。from shapely.geometry import LineString def find_dangling(gdf, tolerance3.0): endpoints [] for idx, row in gdf.iterrows(): geom row.geometry if geom.geom_type LineString: endpoints.append((start, idx, geom.coords[0])) endpoints.append((end, idx, geom.coords[-1])) elif geom.geom_type MultiLineString: for part in geom.geoms: endpoints.append((start, idx, part.coords[0])) endpoints.append((end, idx, part.coords[-1])) other_lines gdf.geometry.unary_union dangling [] # 只取 tolerance单位与坐标系一致范围内没有其他线的端点 for kind, idx, pt in endpoints: buf near(pt, tolerance) if not buf.intersects(other_lines): dangling.append((kind, idx, pt)) return dangling这里的核心逻辑是每根线的首尾点都被拿出来在容差距离内找是否有其他线段存在没有则说明这条线孤悬。tolerance3.0的意义要按坐标系理解——如果数据在 CGCS2000 投影坐标系下3 代表 3 米如果还在 WGS84 经纬度下3 代表 3 度约等于 300 公里完全失真。所以容差检测必须在投影坐标系下进行。实际经验里乡镇路网断头容差设为 2~5 米比较合理城市路网因为道路密集容差可以放到 8 米但要注意 8 米会把并行的两条路误判成一条。4.2 自动生成连接线修复小间隙对断头路做自动修复要非常克制自动补线只适合 10 米以内的小间隙超过 10 米建议人工核对。补线的常见做法是找到断头最近的其他线段端点生成一条直线段连接。这里直线段的问题在于它与道路弧度完全不吻合如果强行连接后处理时会出现折角影响成果美观度和后续网络分析。from shapely.ops import nearest_points def connect_dangling(gdf, dangling_pts): lines gdf.geometry.tolist() for pt in dangling_pts: # 从所有线段几何中提取最近的线 nearest_line nearest_points(pt, gdf.geometry.unary_union)[1] # 取最近点坐标 nearest_p nearest_points(pt, nearest_line)[1] connector LineString([pt, nearest_p]) lines.append(connector) return gpd.GeoDataFrame( {geometry: lines, connector: [1]*len(lines)}, crsgdf.crs )nearest_points函数返回的是两个几何体上最近的一对点注意nearest_line拿到的不一定是 LineString可能是 Point 或 MultiPoint因为unary_union会把所有线合并成 MultiLineString。为了稳定应该从gdf.geometry.tolist()里逐个算距离取最近的线要素而不是直接对 unary_union 操作。生成的 connector 线段要加来源标记方便后续人工审查。修复后重算一次find_dangling看断头数量有没有明显下降。如果断头数量反而变多说明容差设置太小或补线连到了错误位置需要退回重新调整。路网连接性检测的最终结果用网路分析工具做一次从乡镇中心到最近国道的路径计算能通就算闭环。4.3 成果输出注意编码和字段名避免交付后乱码辽宁这套数据交付时经常拷给不同单位用ArcGIS 默认读取 dbf 的字段名是 GBKQGIS 默认用 UTF-8同一个 shp 在两个软件下显示字段名完全不同。所以输出成果前要在属性表里统一字段命名规则字段名全部用拼音或英文如road_level、road_name、source_name字段中文别名写到字段别名表里而不是直接写在 dbf 字段名中。.cpg文件统一写成UTF-8避免 QGIS 识别错误。# 导出最终成果时强制指定编码 ogr2ogr -lco ENCODINGUTF-8 \ -nlt LINESTRING \ -overwrite \ 辽宁省路网_merge.shp \ 辽宁省路网_merge.gpkg-lco ENCODINGUTF-8控制 dbf 内部编码标识-nlt LINESTRING强制所有几何写为单线类型防止 MultiLineString 在部分软件中显示异常。这里输出用 GeoPackage 作为中间格式再用 ogr2ogr 转到 shp好处是中间格式能保留坐标精度避免 shp 固有的 2 字节长度限制导致坐标末位四舍五入。做完这一步这 16 个图层才算真正变成一份干净的可分析路网底图而不是一包原始素材。本文还有配套的精品资源点击获取
返回列表