☰
全国路网SHP数据实战:从文件解析到路网密度计算与对比分析
2026/10/19 16:29:22 网站建设 项目流程

简介:这份2000年全国道路交通网矢量图SHP数据包,面向从事交通地理研究、城市规划与GIS空间分析的科研人员及学生,提供中国早期公路网络的矢量底图,可用于路网演变对比、可达性建模与历史交通格局复原。压缩包共48个文件,约7.08MB,以shp、shx、dbf、prj等Shapefile核心格式为主,分别承载道路线段的几何坐标、索引、属性表与投影信息,另有xml元数据及sbx、sbn空间索引文件,数据按国道、铁路、高速三类以polyline线要素组织,属性字段可区分不同路段身份标识。资源源自北京大学地理数据平台,权威性较高,已有9333人学习下载。使用者借助ArcGIS或QGIS即可直接加载,快速提取2000年国道、铁路与高速公路的空间分布,为论文制图、交通流分析与城市扩张研究提供基础数据,同时需注意按学术规范标注数据来源。

1. 全国路网 SHP 文件到底装了什么:从一份 2000 年矢量数据说起

前阵子帮一个做区域物流规划的朋友整理底图,他甩过来一个压缩包,文件名就一行字:2000年全国道路交通网矢量图SHP.zip。他问得很直接——这玩意儿能不能直接扔进 QGIS 出图,路网密度算出来靠不靠谱,跟现在网上下载的导航路网是不是一回事。我解压看了一眼,心里大概有数了:这是一份典型的早期全国尺度道路矢量数据,SHP 格式,属于 GIS 圈里最基础也最耐用的那种底图资源。它解决的不是"导航怎么走"的问题,而是"那个年代的路网骨架长什么样"的问题。适合做历史地理研究、区域可达性分析、长时序土地利用对比,以及需要一份轻量全国路网做底图的人。如果你要的是实时路况或车道级精度,这份数据会让你失望;但如果你要的是一份能进 GIS、能算、能叠加的全国路网矢量,它值得你花时间摸清楚。

2. 拆开 SHP 压缩包:文件构成、坐标系与属性表结构

2.1 一个 SHP 从来不是单个文件

很多人第一次拿到 SHP 会懵:解压出来一堆同名不同后缀的文件,删了哪个都怕出错。这不是打包的人偷懒,而是 Shapefile 这种格式本身就是多文件协同的。常见做法是至少保留 .shp、.shx、.dbf 三个,少一个都打不开或者属性丢失。

文件后缀作用能不能删
.shp存几何图形,点线面坐标都在这里不能
.shx几何图形的索引,加速定位不能
.dbf属性表,字段和记录不能
.prj坐标系定义,WKT 文本强烈建议保留
.cpg属性表字符编码说明建议保留
.sbn/.sbx空间索引可删,重建即可

我一般拿到压缩包先做一件事:确认 .prj 在不在。全国尺度的路网数据,坐标系直接决定你后面量出来的长度对不对。2000 年前后的这类数据,常见的是地理坐标系(经纬度,比如基于北京54或西安80),也可能是投影坐标系。如果 .prj 缺失,你得靠坐标数值范围反推——经度在 73 到 135 之间、纬度在 3 到 54 之间,那基本就是经纬度。

2.2 用 Python 快速体检:字段、几何类型、范围

别急着开 QGIS,先用脚本把底细摸清楚,几十行就够。下面这段用 geopandas 读进来,打印关键信息。

import geopandas as gpd # 读取路网 SHP,注意路径按实际解压位置改 gdf = gpd.read_file("./road_network_2000/road.shp") # 1. 看坐标系 print("CRS:", gdf.crs) # 2. 看几何类型:全国路网通常是 LineString 或 MultiLineString print("Geom types:", gdf.geom_type.unique()) # 3. 看属性表字段和前几行 print("Columns:", list(gdf.columns)) print(gdf.head(3)) # 4. 看空间范围,判断坐标系类型 print("Bounds:", gdf.total_bounds) # 5. 看要素数量,评估数据量级 print("Feature count:", len(gdf))

逻辑说明:gdf.crs直接告诉你坐标系,如果返回 None 说明 .prj 丢了,得手动指定。geom_type用来确认是线要素,路网不该出现点或面。total_bounds返回[minx, miny, maxx, maxy],如果 x 在 70~140、y 在 3~55,就是经纬度;如果数值是几十万到几百万,那是投影坐标,单位通常是米。len(gdf)让你对数据量有个预期,全国路网几万到几十万条线都正常。

参数说明:read_file默认按文件里的编码读 .dbf,如果中文属性乱码,加encoding="gbk"或encoding="utf-8"试。这一步不涉及写操作,纯读,安全。

2.3 属性表里通常有什么

早期全国路网数据的属性字段一般不会太花哨,常见的是道路名称、等级、类型、长度这几类。等级字段是关键——它决定了你后面能不能按高速、国道、省道分层渲染。如果属性表里只有几何没有等级,那这份数据只能当"有没有路"的底图用,做不了分级分析。我一般会先统计一下等级字段的取值分布:

# 假设等级字段叫 "CLASS" 或 "等级",先确认列名 col = "CLASS" if "CLASS" in gdf.columns else gdf.columns[1] # 统计各等级要素数量 print(gdf[col].value_counts(dropna=False))

如果输出里出现大量空值或者只有一两种取值,说明属性质量一般,后面做分级渲染时得靠几何长度或人工规则补。这一步的坑在于:不同来源的数据字段名不统一,别硬编码列名,先打印list(gdf.columns)看一眼再写。

3. 把路网用起来:投影转换、长度计算与分级渲染

3.1 先投影再算长度,这是铁律

经纬度坐标下直接算长度,单位是度,没有物理意义。必须转到投影坐标系,单位变成米,算出来的公里数才可信。全国尺度常用等面积或等距投影,我一般用 Albers 等面积投影,兼顾面积和长度误差。

# 从经纬度转到 Albers 等面积投影(中国常用参数) gdf_proj = gdf.to_crs(epsg=9822) # 或自定义 proj 字符串 # 计算每条路的长度,单位米 gdf_proj["length_m"] = gdf_proj.geometry.length # 转成公里并看统计 gdf_proj["length_km"] = gdf_proj["length_m"] / 1000 print(gdf_proj["length_km"].describe())

逻辑说明:to_crs做坐标转换,geometry.length在投影坐标下返回米。describe()给你最小值、最大值、均值,用来判断有没有异常——比如某条路长度是 0 或者几万公里,那多半是几何有问题。

参数说明:epsg=9822是常见的中国 Albers 投影编码,但不同 GIS 平台编码可能不同。稳妥做法是用 proj 字符串显式指定中央经线和标准纬线。如果你不确定,用gdf.estimate_utm_crs()让 geopandas 推荐一个 UTM 分区也行,但全国尺度用 UTM 会有跨带问题,Albers 更合适。

3.2 分级渲染:让路网从一团线变成有层次的地图

全国路网直接画出来就是一团黑,看不出结构。分级渲染的核心是按道路等级或长度分档,给不同粗细和颜色。QGIS 里可以按属性字段做分类符号,代码里也能直接出图。

import matplotlib.pyplot as plt # 按长度分三档,简单演示分级思路 gdf_proj["len_class"] = pd.cut( gdf_proj["length_km"], bins=[0, 1, 10, 1000], labels=["short", "medium", "long"] ) fig, ax = plt.subplots(figsize=(12, 10)) for cls, color, lw in [("short", "#cccccc", 0.3), ("medium", "#888888", 0.6), ("long", "#222222", 1.2)]: subset = gdf_proj[gdf_proj["len_class"] == cls] subset.plot(ax=ax, color=color, linewidth=lw, label=cls) ax.set_title("Road network by length class") ax.legend() plt.savefig("road_classified.png", dpi=200)

逻辑说明:pd.cut按长度把要素分成三档,然后逐档叠加绘制,短路段细而浅,长路段粗而深,视觉上主干道自然凸显。这是最朴素的分级,实际项目里更常见的是按道路等级字段分档。

参数说明:bins的边界要根据你的数据分布调,别照抄。先看describe()的结果再定分档点。linewidth和color是视觉参数,出图前多试几组。

3.3 叠加分析:路网密度怎么算才不糊弄

路网密度是这份数据最常被用来算的指标之一。做法是划网格,统计每个网格内的道路总长度,再除以网格面积。关键是网格大小要合理——太大看不出差异,太小全是噪声。

import numpy as np # 生成覆盖路网范围的网格,这里以 0.5 度为例 minx, miny, maxx, maxy = gdf_proj.total_bounds cell = 50000 # 投影坐标下 50km 网格,按需调 grid_cells = [] x = minx while x < maxx: y = miny while y < maxy: grid_cells.append(box(x, y, x + cell, y + cell)) y += cell x += cell grid = gpd.GeoDataFrame(geometry=grid_cells, crs=gdf_proj.crs) # 空间连接,把路网长度汇总到网格 joined = gpd.overlay(gdf_proj, grid, how="intersection") joined["seg_len"] = joined.geometry.length density = joined.groupby(joined.index_right)["seg_len"].sum() grid["road_len_m"] = density grid["density_km_per_km2"] = grid["road_len_m"] / (cell / 1000) ** 2

逻辑说明:先生成规则网格,再用overlay做相交,把每条路切到网格里并重新算长度,最后按网格汇总。density_km_per_km2就是每平方公里道路公里数,是路网密度的标准表达。

参数说明:cell是网格边长,投影坐标下单位是米。全国尺度用 50km 网格比较合适,城市尺度要降到 1km 甚至更小。overlay计算量大,全国路网加细网格可能跑很久,建议先裁到研究区再算。

提示:overlay之前先确认几何有效,用gdf_proj.is_valid.all()检查,有无效几何先buffer(0)修复,否则相交会报错或丢要素。

4. 避坑与排查:这份路网数据最容易翻车的五个地方

4.1 打开就报错,提示缺少 .shx 或 .dbf

现象:QGIS 或 geopandas 读取时报 "Unable to open datasource" 或字段全空。 原因:压缩包里文件不全,或者解压时只解了 .shp。 解决:回压缩包确认同名文件是否齐全,至少补齐 .shp、.shx、.dbf 三个。如果原始包就缺,用ogr2ogr或 QGIS 的修复几何工具尝试重建索引,但属性丢了就真丢了。

4.2 中文属性乱码

现象:属性表里道路名称显示成问号或方块。 原因:.dbf 的字符编码和读取时指定的编码不一致,早期数据常见 GBK。 解决:读取时显式指定encoding="gbk",如果还乱就试"gb18030"。QGIS 里可以在图层属性里改编码重新加载。别用 UTF-8 硬读 GBK 数据,只会更乱。

4.3 长度算出来大得离谱或小得可怜

现象:某条路长度几万公里,或者全部小于 1 公里。 原因:坐标系没转就量长度,或者转错了投影。 解决:先print(gdf.crs)确认原始坐标系,再to_crs到合适的投影。经纬度下量长度必然错,投影参数选错也会系统性偏差。算完用describe()扫一眼,异常值一眼能看出来。

4.4 叠加分析跑到一半内存爆了

现象:overlay或sjoin执行时卡死或 MemoryError。 原因:全国路网要素多,网格又细,相交后要素数量爆炸。 解决:先按研究区裁剪路网,再生成网格;或者用sjoin代替overlay做近似统计。实在要全国算,分省分批跑再合并。我一般会先拿一个省试跑,确认流程通了再铺开。

4.5 路网和底图对不上,整体偏移

现象:路网叠在影像或在线底图上,位置偏了几百米甚至几公里。 原因:坐标系定义错误,或者数据本身用的是地方坐标系而 .prj 写的是通用坐标系。 解决:先确认 .prj 内容是否合理,再用已知地物(如城市中心、河流交汇)做控制点比对。如果整体平移,可能是基准面差异(北京54 vs WGS84),需要做基准面转换。这类问题没有万能解,只能靠控制点校正。

5. 进阶玩法:把 2000 年路网和现在做对比,以及一个我常走的验证流程

这份数据真正的价值不在单看,而在对比。我最近做的一个小实验,是把 2000 年全国路网和一份较新的路网放在一起,算每个网格内新增道路长度,用来观察哪些区域路网扩张最快。做法不复杂:两份数据都转到同一投影,各自做网格汇总,然后相减。

# 假设 grid_2000 和 grid_now 是同一套网格下的密度结果 grid_2000 = grid[["geometry", "density_km_per_km2"]].rename( columns={"density_km_per_km2": "density_2000"}) grid_now = grid_now[["geometry", "density_km_per_km2"]].rename( columns={"density_km_per_km2": "density_now"}) # 按网格索引对齐后相减 diff = grid_now.copy() diff["density_change"] = grid_now["density_now"] - grid_2000["density_2000"] # 只看增长明显的网格 hot = diff[diff["density_change"] > 0.5] print("Grids with significant growth:", len(hot))

逻辑说明:两份数据必须用同一套网格和同一投影,否则相减没意义。density_change为正表示路网加密,为负可能是数据口径差异或道路废弃,要谨慎解读。hot筛出增长超过阈值的网格,用来定位重点区域。

参数说明:阈值 0.5 是每平方公里新增 0.5 公里道路,按研究尺度调。对比分析最大的坑是两份数据的道路定义和采集标准不一致,差值里混着口径差异,别直接当成真实增长。

验证流程我一般强制走一遍:先拿一个熟悉的小区域(比如某个地级市)单独裁出来,用已知的道路总里程做交叉核对,确认投影、长度计算、网格汇总三个环节都没问题,再铺到全国。这个习惯是被坑出来的——有一次全国跑完才发现投影参数写错,所有长度系统性偏了 8%,返工重跑花了大半天。从那以后我每次拿到新路网数据,都先裁一个小区域验证一遍再全量处理。希望这份拆解帮到你,拿到压缩包不至于对着那堆同名文件发愣。

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

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

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

立即咨询