ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

省级道路SHP数据包处理指南:从坐标转换到货车限行路径预筛选

省级道路SHP数据包处理指南:从坐标转换到货车限行路径预筛选

简介:这份2024年广东省道路矢量数据包面向交通规划、物流优化、应急建模与区域经济研究等场景的GIS从业者与研究人员,提供开箱即用的全省路网底图。数据以标准Shapefile格式组织,覆盖全省各地级市及下辖区县,包含高速公路、国道、省道、县道、乡道与城市主干道等多级分类,每条道路附带名称、等级、编号等属性字段,总计超三十万条线要素,坐标系统一采用WGS1984地理坐标系,兼容ArcGIS、QGIS、SuperMap等主流平台。资源包共10个文件,以shp、shx、dbf、prj、cpg等Shapefile核心组件为主,另附py转换脚本、geojson示例与txt说明,压缩包约202.29MB。包内shp2.py脚本可将数据快速转为GeoJSON,便于Web地图开发与前端调用。目前已有53人学习下载,适合需要高质量路网底图进行空间分析、路径计算与可视化呈现的读者直接取用。

1. 拿到一份省级道路 SHP 数据包,先别急着往 GIS 里拖

上周有个做物流路径规划的朋友甩过来一个压缩包,说“广东全省的路网,高速国道省道县乡道全有,WGS84 的,你帮我看看能不能直接算货车限行”。我解压一看,目录里躺着四个 SHP 文件,外加 .dbf、.shx、.prj 一整套。这种“省级全量道路矢量数据包”在路网分析、导航电子地图底图、城乡配送范围测算里属于硬通货——它把高速、国道、省道、县乡道按行政等级拆成独立图层,坐标系统一为 WGS84 地理坐标系,意味着你拿到手就能跟 GPS 轨迹、POI 点位、行政区划面做空间叠加,不用先折腾坐标转换。适合谁?做交通流量模拟的、搞配送时效估算的、给乡镇网点做选址的,以及需要一份干净路网底图做可视化的。但先别急着往 QGIS 或 ArcGIS 里拖,SHP 这东西看着简单,属性表字段、拓扑连通性、坐标系声明这三处但凡有一处没对齐,后面空间连接出来的结果就是玄学。

2. 拆开数据包:四个 SHP 图层到底怎么分工

2.1 高速、国道、省道、县乡道的分层逻辑

拿到压缩包先别双击打开,用命令行列一下文件清单,确认每个图层的要素数量和字段结构。常见做法是高速和国道合并成一个“干线层”,省道单独一层,县乡道因为碎、断头路多,单独处理。为什么要分层?因为不同等级道路在路径分析里的通行成本、限速、单双向规则完全不同。你把高速和县乡道混在一个图层里做最短路径,算法会默认它们权重一样,结果就是导航让你从村道穿过去上高速匝道——血泪经验。

# 列出目录下所有 shp 及其配套文件,确认没有缺 .prj 或 .shx ls -lh *.shp *.dbf *.shx *.prj # 用 ogrinfo 快速看每个图层的要素数和字段(GDAL 自带工具) ogrinfo -so -al gd_highway.shp ogrinfo -so -al gd_national.shp ogrinfo -so -al gd_provincial.shp ogrinfo -so -al gd_county_town.shp

ogrinfo -so输出的是摘要信息,重点看Feature Count和Geometry类型。如果县乡道图层要素数明显偏少(比如只有几千条),大概率是只保留了主要乡道,村道没进来,做末端配送覆盖分析时要注意这个边界。-al会把所有要素的字段列出来,重点确认有没有name、grade、length这类字段,后面做筛选和统计全靠它们。

2.2 WGS84 坐标在 .prj 文件里怎么确认

WGS84 是个统称,实际工程里至少有两种常见写法:地理坐标系(经纬度,单位度)和投影坐标系(平面米,比如 UTM 或 Web Mercator)。这份数据包标题写的是 WGS84 坐标,默认理解为地理坐标系 EPSG:4326。但别信标题,直接看 .prj 文件内容。

# 查看 .prj 文件,确认坐标系定义 cat gd_highway.prj # 用 ogrinfo 直接读 SRS 信息更直观 ogrinfo -so gd_highway.shp gd_highway | grep -A 5 "SRS"

如果 .prj 里出现GCS_WGS_1984且UNIT["Degree"],那就是经纬度。如果出现PROJCS和UNIT["Meter"],说明是投影后的平面坐标,虽然基准还是 WGS84,但单位是米。这两种在计算长度、面积时差别巨大:经纬度下直接算长度得到的是度,不是米,必须用测地线算法或先投影。我一般会先跑一个字段计算,看length字段的数值量级——如果一条高速的长度是 0.5 左右,那单位是度;如果是 50000 左右,单位是米。

提示:如果 .prj 文件缺失,SHP 会变成“无坐标系”状态,拖进 GIS 后可能被默认当成 WGS84 或投影坐标,后续所有空间操作都不可信。拿到数据先检查 .prj 是否存在。

2.3 属性表字段与道路等级编码

四个图层的属性表结构通常一致,但字段名可能因生产单位不同而有差异。常见字段包括NAME(道路名称)、GRADE(等级编码)、TYPE(类型)、LENGTH(长度)、ONEWAY(单双向)。等级编码没有国标强制,有的用 1/2/3/4 对应高速/国道/省道/县乡道,有的用文字“高速”“国道”。拿到后先跑个频数统计。

import geopandas as gpd # 读取县乡道图层,查看等级字段的取值分布 gdf = gpd.read_file("gd_county_town.shp") print(gdf.columns.tolist()) print(gdf['GRADE'].value_counts() if 'GRADE' in gdf.columns else '无 GRADE 字段') print(gdf.crs) # 确认坐标系

gdf.crs输出EPSG:4326就说明 GeoPandas 正确识别了坐标系。如果输出None,说明 .prj 没被读到,需要手动指定gdf.set_crs(epsg=4326, inplace=True)。value_counts()用来确认等级字段的编码方式,后面做筛选时直接按这个编码过滤。如果字段名不是GRADE而是LEVEL或ROAD_TYPE,改一下列名即可,逻辑一样。

3. 把 SHP 用起来:从坐标转换到空间筛选的完整链路

3.1 经纬度转投影坐标做长度和缓冲区

WGS84 经纬度直接算长度得到的是度,没有工程意义。正确做法是先投影到适合广东的平面坐标系。广东跨了 UTM 49N 和 50N 两个带,全省范围建议用 Albers 等面积投影或 Web Mercator(EPSG:3857)。如果只是做长度统计和缓冲区,Web Mercator 够用,但高纬度会变形,广东纬度低,误差可接受。

import geopandas as gpd # 读取高速图层 highway = gpd.read_file("gd_highway.shp") # 确认原始坐标系为 WGS84 经纬度 assert highway.crs.to_epsg() == 4326, "坐标系不是 EPSG:4326,先检查 .prj" # 投影到 Web Mercator,单位变为米 highway_m = highway.to_crs(epsg=3857) # 计算每条高速的实际长度(米),并转成公里 highway_m['length_km'] = highway_m.geometry.length / 1000 # 按道路名称分组统计总里程 total_by_name = highway_m.groupby('NAME')['length_km'].sum().sort_values(ascending=False) print(total_by_name.head(10))

to_crs(epsg=3857)是原地投影转换,不改变几何形状,只改变坐标单位。geometry.length在投影坐标系下返回米,除以 1000 得到公里。groupby('NAME')按道路名称聚合,能快速看出哪条高速最长。如果NAME字段有重复或空值,先做清洗:highway_m = highway_m[highway_m['NAME'].notna()]。

3.2 按等级筛选并导出独立图层

四个图层分开存有好处,但做全省路网分析时经常需要合并成一个带等级字段的总图层。合并前先给每个图层加一个road_class字段,再pd.concat。

import pandas as pd import geopandas as gpd # 分别读取四个图层,统一加等级字段 highway = gpd.read_file("gd_highway.shp").assign(road_class='高速') national = gpd.read_file("gd_national.shp").assign(road_class='国道') provincial = gpd.read_file("gd_provincial.shp").assign(road_class='省道') county = gpd.read_file("gd_county_town.shp").assign(road_class='县乡道') # 合并为一个 GeoDataFrame all_roads = pd.concat([highway, national, provincial, county], ignore_index=True) # 确认合并后坐标系一致 print(all_roads.crs) # 按等级筛选出高速和国道,导出为新的 SHP trunk = all_roads[all_roads['road_class'].isin(['高速', '国道'])] trunk.to_file("gd_trunk_roads.shp", encoding='utf-8')

assign是 pandas 的链式赋值,给每个子图层加一列常量。pd.concat合并时ignore_index=True重置索引,避免重复索引导致后续筛选出错。to_file导出 SHP 时指定encoding='utf-8',防止中文道路名称乱码。注意 SHP 对字段名长度有限制(最多 10 个字符),road_class刚好 10 个字符,再长会被截断。

3.3 用空间连接把路网挂到行政区上

做区域统计时,需要知道每个区县内各等级道路的里程。思路是用行政区划面去跟路网线做空间连接,再按区县和等级分组汇总。

import geopandas as gpd # 读取行政区划面(假设已有该数据,坐标系需为 WGS84) districts = gpd.read_file("gd_districts.shp").to_crs(epsg=3857) # 路网也投影到同一坐标系 roads_m = all_roads.to_crs(epsg=3857) # 空间连接:路网线与行政区面相交 joined = gpd.sjoin(roads_m, districts, how='inner', predicate='intersects') # 按区县和道路等级统计里程 summary = joined.groupby(['district_name', 'road_class']).apply( lambda x: x.geometry.length.sum() / 1000 ).unstack(fill_value=0) print(summary.head())

sjoin的predicate='intersects'表示只要线与面有交集就关联。how='inner'只保留能匹配上的记录。groupby后apply对每组几何求长度和,unstack把等级变成列,方便看每个区县各等级道路的里程分布。如果某条路跨了两个区县,会被拆成两段分别计入,这是正确的——实际统计中跨区道路本来就要分段归属。

注意:空间连接前务必确认两个图层的坐标系一致。一个 WGS84 经纬度、一个 Web Mercator 米,sjoin不会报错,但结果全错,这是最常见的翻车点。

4. 避坑与排查:SHP 处理里最容易翻车的五件事

4.1 中文属性乱码:现象是道路名称显示为问号或方块

原因:SHP 的 .dbf 文件默认编码是 Latin-1 或 GBK,不同生产工具导出的编码不统一。用 GeoPandas 读取时如果没指定编码,中文会乱码。解决:读取时加encoding='gbk'或encoding='utf-8'试,导出时统一用encoding='utf-8'。如果已经乱码,用gdf['NAME'].str.encode('latin1').str.decode('gbk')尝试修复。

4.2 坐标系声明缺失:现象是拖进 GIS 后位置飘到非洲

原因:压缩包里 .prj 文件丢失或被误删。SHP 本身不存储坐标系,全靠 .prj 声明。解决:如果确认数据是 WGS84 经纬度,用gdf.set_crs(epsg=4326, inplace=True)手动指定,再投影。如果不知道原始坐标系,看坐标数值范围——经纬度在 -180 到 180 之间,投影坐标通常是几十万到几百万。

4.3 拓扑断头路:现象是最短路径算出来绕远路或走不通

原因:县乡道图层里很多路段首尾节点没有严格对齐,存在微小缝隙。路径分析算法要求网络拓扑连通,缝隙会导致断连。解决:用shapely的snap做节点捕捉,容差设 0.0001 度(约 10 米),或者用 PostGIS 的ST_Snap和ST_Node做拓扑重建。常见做法是先把路网导入 PostGIS,用pgr_createTopology建拓扑。

4.4 字段名截断:现象是导出的 SHP 里字段名变成 NAME_1、GRADE_1

原因:SHP 格式对字段名限制 10 个字符,超出部分被截断,重名时自动加后缀。解决:导出前把字段名改短,比如road_class改成rclass,district_name改成dname。或者改用 GeoPackage(.gpkg)格式,没有字段名长度限制。

4.5 高速与匝道混淆:现象是高速里程统计偏大

原因:高速图层里包含了匝道、服务区道路等附属线,这些在属性表里可能没有单独标记。解决:先按NAME或TYPE字段过滤,排除名称含“匝道”“连接线”的要素,再做里程统计。如果属性表没有区分字段,只能靠长度阈值粗筛——匝道通常很短,设个 500 米以下的过滤掉。

5. 进阶用法:用路网数据做货车限行路径的预筛选

前面都是基础操作,真正体现这份数据价值的是结合限行规则做路径预筛选。假设你要给某市做货车配送路径规划,高速和国道大部分允许货车通行,但县乡道有限重、限宽,省道部分路段有城区限行。思路是给每条路打上“货车可通行”标签,再在路径分析时过滤。

import geopandas as gpd # 读取合并后的全路网 roads = gpd.read_file("gd_all_roads.shp") # 初始化通行标签:默认允许 roads['truck_ok'] = 1 # 县乡道默认不允许货车通行(假设规则) roads.loc[roads['road_class'] == '县乡道', 'truck_ok'] = 0 # 省道中名称含“城区”或“街道”的,标记为限行 roads.loc[ (roads['road_class'] == '省道') & (roads['NAME'].str.contains('城区|街道', na=False)), 'truck_ok' ] = 0 # 导出可通行货车路网 truck_network = roads[roads['truck_ok'] == 1] truck_network.to_file("gd_truck_network.shp", encoding='utf-8') # 统计可通行里程占比 total_km = roads.to_crs(epsg=3857).geometry.length.sum() / 1000 truck_km = truck_network.to_crs(epsg=3857).geometry.length.sum() / 1000 print(f"货车可通行里程占比:{truck_km / total_km:.2%}")

str.contains('城区|街道', na=False)用正则匹配名称中的关键词,na=False把空值当不匹配处理,避免报错。truck_ok字段导出后可以直接被路径规划引擎读取,作为边权重过滤条件。这个预筛选能把路网规模砍掉三到四成,后续做大规模路径计算时速度提升明显。

验证方法:随机抽几条被标记为不可通行的省道,用 QGIS 叠加卫星底图看一眼,确认确实穿过城区。如果发现误判,调整关键词列表。我一般会把这个筛选逻辑封装成一个函数,参数化关键词和等级规则,换一个城市只改配置不改代码。

从那以后我每次拿到新的路网数据包,都强制走一遍“查 .prj → 看字段 → 投影 → 拓扑检查”这四步,不再直接拖进 GIS 就开算。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进