简介:这份2000年全国道路交通网矢量图SHP数据包,面向从事交通地理研究、城市规划与GIS空间分析的科研人员及学生,用于还原2000年前后中国公路与铁路网络的分布格局。压缩包共48个文件,约7.08MB,以shp、shx、dbf、prj等Esri Shapefile核心格式为主,分别承载道路线段的几何坐标、索引、属性表与投影信息,并附带xml元数据及sbx、sbn空间索引文件,便于在ArcGIS、QGIS中直接加载与查询。数据按国道、铁路、高速三类要素分层组织,以polyline线要素表达路网走向,可支撑交通流分析、可达性测算与历史路网演变对比等研究。目前已有9333人学习下载,使用者需具备基础GIS操作能力,并注意数据源自北京大学城市与环境学院地理数据平台,公开发布时须规范引用。
1. 拿到一份 2000 年全国道路交通网 SHP,先别急着往 GIS 里拖
很多人第一次接触“2000 年全国道路交通网矢量图 SHP”这类数据,第一反应是双击打开、拖进软件、截图出图,结果要么属性表全是乱码,要么路网断成一截一截,要么坐标系飘到几千公里外。这份数据本质是一套按行政或图幅切分的 Shapefile 集合,核心价值在于它把 2000 年前后的国道、省道、县乡道以线要素形式固化下来,适合做历史路网密度对比、可达性建模、老地图底图叠加。它适合做区域规划、交通史研究、GIS 教学和时空数据可视化的人,不适合只想找一张“好看的路网图”当壁纸的人。真正要落地,先得把数据组织、坐标系、属性字段和拓扑质量这四件事摸清楚,否则后面所有分析都是沙上建塔。
2. 拆开压缩包先看什么:目录结构、编码与坐标系的三个确认动作
2.1 解压后先数文件:SHP 不是单个文件,而是一组同名兄弟
Shapefile 是 ESRI 早年定下的开放格式,一个完整的线要素图层至少包含.shp、.shx、.dbf三个文件,缺一不可。.shp存几何,.shx存索引,.dbf存属性表。如果还带.prj,那说明坐标系定义还在;如果带.cpg,说明属性表的字符编码有声明。很多流传的压缩包里.prj和.cpg经常丢失,打开后坐标是“无投影”状态,中文属性也会变成问号。
我一般拿到包先做三件事:列目录、看文件大小、检查同名文件是否齐全。下面这段 bash 命令可以直接在解压后的目录里跑,把每个图层的文件配套情况打出来。
# 进入解压后的目录,按文件名前缀分组统计配套文件 for prefix in $(ls *.shp 2>/dev/null | sed 's/\.shp$//'); do echo "=== 图层: $prefix ===" for ext in shp shx dbf prj cpg; do if [ -f "${prefix}.${ext}" ]; then size=$(du -h "${prefix}.${ext}" | cut -f1) echo " ${ext}: 存在 (${size})" else echo " ${ext}: 缺失" fi done done这段脚本的逻辑是按.shp文件反推图层名,再逐个检查五个关键扩展名。参数上不需要额外输入,但要注意:如果目录里混有.shp.xml这种元数据文件,sed替换后不会误伤,因为只匹配.shp结尾。缺失.prj时不要慌,后面可以手动补坐标系;缺失.dbf就麻烦了,几何还在但属性全丢,只能当纯线画图用。
2.2 用 ogrinfo 读元数据:字段名、要素数、几何类型一次看清
确认文件齐全后,下一步是读元数据。GDAL 套件里的ogrinfo是命令行里最稳的工具,不依赖桌面软件,输出也干净。下面这条命令会打印图层摘要和字段定义。
# 查看单个图层的摘要信息 ogrinfo -so -al road_2000.shp # 如果目录下有多个图层,先列出所有图层名 ogrinfo -ro -q /path/to/folder-so表示 summary only,只输出摘要不遍历要素;-al表示 all layers,对目录下所有图层生效。输出里重点看三行:Geometry: Line String确认是线要素;Feature Count看要素总数,全国路网通常在几十万到百万级;Extent看经纬度范围,如果出现-180, -90, 180, 90这种全球范围,说明坐标系没定义,软件按 WGS84 经纬度硬解了。
字段部分要留意中文属性是否正常显示。如果ogrinfo输出里字段值是乱码,但字段名是英文,那大概率是.dbf的代码页问题。常见做法是用ogr2ogr转一次,强制指定编码:
# 将 DBF 编码从 GBK 转为 UTF-8,输出为新文件 ogr2ogr -f "ESRI Shapefile" -lco ENCODING=UTF-8 road_2000_utf8.shp road_2000.shp-lco ENCODING=UTF-8是图层创建选项,告诉驱动写出的.dbf用 UTF-8。如果原始数据是 GBK 编码但没带.cpg,GDAL 可能按 Latin-1 解读,转出来还是乱码,这时需要加-oo ENCODING=GBK指定输入编码。参数顺序不能反,-oo管输入,-lco管输出。
2.3 坐标系判定:没有 .prj 时怎么反推是地理坐标还是投影坐标
2000 年全国路网数据常见的坐标系有两种:一种是地理坐标系,单位是度,比如GCS_WGS_1984或GCS_Beijing_1954;另一种是投影坐标系,单位是米,比如Beijing_1954_GK_Zone_*或Xian_1980_GK_Zone_*。如果.prj丢了,怎么判断?
看坐标数值量级。用ogrinfo输出 Extent,如果经度在 73 到 135 之间、纬度在 3 到 54 之间,那是地理坐标,单位度。如果 X 坐标在几百万、Y 坐标在几百万到几千万,那是投影坐标,单位米。下面这段 Python 用fiona读第一个要素的坐标,直接打印量级。
import fiona with fiona.open("road_2000.shp") as src: # 读第一个要素的几何边界 first = next(iter(src)) print("几何类型:", first["geometry"]["type"]) print("坐标示例:", first["geometry"]["coordinates"][0][:2]) print("CRS 定义:", src.crs)如果src.crs是空字典,说明没有.prj。此时根据坐标量级判断:度级坐标补EPSG:4326或EPSG:4214(北京54地理坐标),米级坐标补对应的高斯克吕格投影。补坐标系不要直接改.prj文本,用ogr2ogr重新投影或qgis里“另存为”指定 CRS 更稳。
提示:坐标系补错比不补更危险。不补只是显示位置不对,补错会导致面积、长度计算全部失真,而且很难从图上肉眼发现。
3. 把 SHP 跑进分析流程:从裁剪、投影到路网密度的最小可复现链路
3.1 按研究区裁剪:用 ogr2ogr 的 -clipsrc 做边界裁切
全国路网数据动辄几十万条线,直接做空间分析会拖慢每一步。常见做法是先按研究区边界裁剪。假设你有一个研究区的面文件study_area.shp,下面这条命令把路网裁到该范围内。
# 用面边界裁剪路网,输出为新的 Shapefile ogr2ogr -f "ESRI Shapefile" \ -clipsrc study_area.shp \ -clipsrclayer study_area \ road_clipped.shp \ road_2000.shp-clipsrc指定裁剪源,可以是面文件也可以是经纬度范围。-clipsrclayer在裁剪源是多图层时指定具体图层名。输出文件只保留与裁剪面相交的线要素,属性表原样保留。注意裁剪不会打断线,跨边界的线会被整条保留或整条丢弃,取决于相交判断。如果要做精确的边界截断,得用-clipsrc配合-nlt MULTILINESTRING或者转到 PostGIS 里用ST_Intersection。
裁剪后要素数会大幅下降,可以用ogrinfo -so对比前后数量,确认裁剪生效。
3.2 投影转换:把地理坐标转成等面积投影再做密度计算
路网密度计算必须用投影坐标,因为“度”不是长度单位。全国范围常用Albers等面积投影,参数如下:
# 将 WGS84 地理坐标转为 Albers 等面积投影 ogr2ogr -f "ESRI Shapefile" \ -t_srs "+proj=aea +lat_1=25 +lat_2=47 +lat_0=0 +lon_0=105 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs" \ road_albers.shp \ road_clipped.shp+lat_1和+lat_2是标准纬线,取 25 和 47 能覆盖中国大部分区域,变形较小。+lon_0=105是中央经线。+units=m确保输出单位是米。转换后可以用ogrinfo -so看 Extent,X 和 Y 应该变成百万级数值。
如果原始数据是北京54或西安80,+datum要相应改成Beijing_1954或Xian_1980,并且需要七参数或三参数转换,不能只改+datum了事。没有转换参数时,近似处理会带来几十到上百米偏移,做小区域分析时要注意。
3.3 路网密度计算:用 QGIS 字段计算器或 Python 脚本算线密度
路网密度通常定义为单位面积内的道路长度,单位 km/km²。在 QGIS 里可以用“字段计算器”给每条线算长度,再按格网汇总。命令行方式用geopandas更可控:
import geopandas as gpd import numpy as np # 读投影后的路网 roads = gpd.read_file("road_albers.shp") # 计算每条线的长度,单位米 roads["length_m"] = roads.geometry.length # 按道路等级分组统计总长度 summary = roads.groupby("road_class")["length_m"].sum() / 1000.0 print(summary)roads.geometry.length在投影坐标下返回米,除以 1000 得公里。road_class是属性表里的道路等级字段,不同数据里可能叫CLASS、TYPE、等级,先用ogrinfo确认字段名。如果要做格网密度,用geopandas.overlay把路网与格网相交,再按格网 ID 汇总长度除以格网面积。
注意:线密度对格网大小很敏感。格网太大,密度被平均掉;格网太小,很多格网没有路,密度为零。常见做法是取 1km×1km 或 5km×5km,根据研究尺度调整。
4. 避坑与排查:路网数据最常见的五类翻车现场
4.1 属性表中文乱码,字段值全是问号或方块
现象:用 QGIS 打开.dbf,中文路名显示为????或\xca\xa1这类转义字符。原因:.dbf文件头里的代码页标记丢失或错误,GDAL 按默认 Latin-1 解码。解决:先用ogrinfo确认字段名是否正常,如果字段名正常只有值乱码,用ogr2ogr加-oo ENCODING=GBK转一次;如果字段名也乱,说明整个 DBF 头有问题,用 LibreOffice 打开.dbf另存为 CSV,再重新关联几何。
4.2 路网断线严重,交叉口不连通
现象:做网络分析时,明明两条路在图上相交,但拓扑上不连通,最短路径绕远。原因:原始数据采集时按路段独立数字化,交叉口没有打断,或者打断后节点坐标有微小偏差。解决:用GRASS GIS的v.clean工具做break和snap,容差设 0.001 米到 0.01 米之间。QGIS 里可以用“处理工具箱”里的“打断相交线”和“吸附几何”。
4.3 坐标系缺失导致面积和长度计算全错
现象:在 QGIS 里量一条国道长度,显示 0.5 度,而不是 500 公里。原因:图层没有.prj,QGIS 按地理坐标算长度,单位是度。解决:先判断坐标量级,补上正确 CRS,再用“另存为”投影到等面积坐标系。不要直接在原图层上改 CRS,那只是改标签,不改坐标值。
4.4 裁剪后属性表字段丢失或类型变化
现象:用ogr2ogr -clipsrc裁剪后,原本的整型字段变成字符串,或者某些字段消失。原因:Shapefile 的 DBF 字段名最长 10 个字符,裁剪时如果字段名冲突会被截断或重命名。解决:裁剪前先用ogr2ogr把数据转成 GeoPackage 或 GeoJSON,这些格式字段名限制少,裁剪后再转回 Shapefile。或者裁剪时加-sql显式选择字段。
4.5 大数据量下内存溢出或处理超时
现象:用 Python 读全国路网时进程被系统杀掉,或者 QGIS 卡死。原因:Shapefile 驱动一次性把全部要素读进内存,几十万条线加上属性表很容易超过 2GB。解决:分块处理,用fiona的filter按边界框逐块读,或者先按省裁剪成小文件再逐个处理。QGIS 里可以开启“渲染缓存”并降低缩放级别,避免全量渲染。
5. 进阶用法:把 2000 年路网和后续年份做叠加,验证变化检测的精度边界
5.1 多期路网叠加的字段对齐与几何匹配
手头如果有 2010 年或 2020 年的路网数据,想算新增道路里程,不能直接相减。不同年份的数据可能来自不同采集标准,道路等级分类、字段名、坐标系都不一样。我一般先做三件事:统一投影到同一 Albers 坐标系;把道路等级字段映射到一套自定义分类;用geopandas.sjoin做空间连接,找出 2000 年没有而后续年份有的路段。
import geopandas as gpd # 读两期数据,统一投影 roads_2000 = gpd.read_file("road_2000_albers.shp").to_crs("EPSG:3857") roads_2020 = gpd.read_file("road_2020_albers.shp").to_crs("EPSG:3857") # 用 100 米缓冲区做空间连接,判断 2020 年路段是否在 2000 年路网附近 buf_2000 = roads_2000.copy() buf_2000["geometry"] = buf_2000.geometry.buffer(100) joined = gpd.sjoin(roads_2020, buf_2000, how="left", predicate="intersects") # 没有匹配上的就是新增路段 new_roads = joined[joined["index_right"].isna()] print("新增路段数:", len(new_roads))buffer(100)的 100 米是经验值,用于容忍两期数据采集时的位置偏差。太小会误判新增,太大会漏判。predicate="intersects"表示只要相交就算匹配。index_right为空说明该 2020 年路段在 2000 年路网 100 米范围内没有对应线。
5.2 变化检测的精度验证:抽样人工核对与混淆矩阵
自动算出来的新增路段数不能直接信。我习惯抽 5% 的样本,叠加到影像或老地图上人工看。如果新增路段里混有大量因位置偏移导致的假新增,就要调整缓冲距离或改用 Hausdorff 距离做匹配。下面这个表是我在某次模拟项目里用的验证记录格式,读者可以照着建自己的核对表。
| 样本编号 | 自动判定 | 人工核对 | 偏差原因 |
|---|---|---|---|
| S001 | 新增 | 新增 | 无 |
| S002 | 新增 | 原有 | 位置偏移 120 米 |
| S003 | 原有 | 原有 | 无 |
| S004 | 新增 | 新增 | 无 |
| S005 | 原有 | 新增 | 缓冲区过大漏判 |
偏差原因里“位置偏移”最常见,尤其是早期数据数字化精度不高时。如果偏移导致的假新增超过 20%,缓冲距离要重新调,或者改用拓扑匹配算法。
5.3 一个具体技巧:用道路等级加权算可达性,而不是只看密度
路网密度只反映“有多少路”,不反映“路好不好走”。做可达性分析时,我一般给不同等级道路赋不同速度,再算从某点到最近医院或学校的通行时间。2000 年数据里如果属性表有道路等级字段,可以直接映射:国道 80 km/h,省道 60 km/h,县道 40 km/h,乡道 20 km/h。没有等级字段时,用道路长度和弯曲度做粗略推断,但精度会打折扣。
# 按道路等级赋速度,计算通行时间 speed_map = {"国道": 80, "省道": 60, "县道": 40, "乡道": 20} roads["speed_kmh"] = roads["road_class"].map(speed_map).fillna(30) roads["travel_time_h"] = roads["length_m"] / 1000.0 / roads["speed_kmh"] print(roads[["road_class", "length_m", "travel_time_h"]].head())fillna(30)是给没有等级的路段一个默认速度,避免空值导致计算中断。travel_time_h是小时数,后续可以用网络分析工具做最短时间路径。
这套流程跑下来,最大的教训是:历史路网数据的价值不在“图好看”,而在“属性全、拓扑通、坐标对”。我见过太多人把 SHP 拖进软件截个图就完事,结果真要算指标时发现字段是空的、线是断的、坐标是飘的。如果你打算认真用这份 2000 年路网数据,先把第 2 章的三个确认动作做一遍,再按第 3 章的链路跑通一次裁剪和投影,后面所有分析都会顺很多。希望帮到你。
本文还有配套的精品资源,点击获取