☰
北京城区道路矢量数据包实战:从Shapefile读取到路网分析与可视化
2026/10/14 7:47:09 网站建设 项目流程

简介:这份北京城区道路矢量数据包面向交通规划、GIS制图与城市建模方向的从业者和学生,提供主城区范围内精细化路网数据,可直接用于路径分析、地图叠加与空间可视化。压缩包共22个文件、约8.08MB,以Shapefile全套组件为主,包含shp、shx、dbf、prj、cpg、sbn、sbx及shp.xml等,另附png预览图、txt说明与Python脚本,覆盖主干道、次干道、支路及部分街巷,坐标系以prj文件为准,属性字段含道路名称、等级、方向、车道数等基础信息。文件命名统一、结构清晰、无冗余,可开箱导入ArcGIS、QGIS、SuperMap等平台编辑与叠加。目前已有71人学习下载,适合需要快速获取北京路网底图、搭建分析实验或制作专题地图的读者参考使用。

1. 北京城区道路矢量数据包到底装了什么:从拿到 Shapefile 到能画出一张路网图

很多人第一次拿到「北京城区道路矢量数据包(含 Shapefile 全套文件)」时,以为解压完就是一个.shp,双击打开就能看到路。结果打开一看:要么属性表里字段名全是拼音缩写,要么道路断成一段一段,要么坐标系对不上、叠到地图上偏出去几百米。这个数据包真正解决的不是「有没有路」的问题,而是「能不能把路网拿来做分析、做渲染、做空间查询」的问题。它适合三类人:做城市交通分析的研究者、做地图可视化的前端或 GIS 工程师、以及需要路网底图做选址或路径规划的产品技术。Shapefile 本身不是一个文件,而是一组同名文件的集合,缺一个都可能导致读取失败或属性丢失。下面按「先认清数据、再动手读、最后避坑」的顺序讲透。

2. Shapefile 全套文件拆解:哪几个必须留,哪几个丢了会翻车

Shapefile 是 Esri 提出的一种矢量数据交换格式,虽然名字叫「file」,实际是一组文件的统称。北京城区道路矢量数据包解压后,通常会看到.shp、.shx、.dbf、.prj、.cpg这几个核心文件,有时还会带.sbn、.sbx、.xml、.qix等辅助文件。很多人只拷贝.shp发给别人,对方打开后属性表空白或者直接报错,这就是典型的「只拿了一半数据」。

2.1 五个核心文件的职责与缺失后果

.shp存几何形状,也就是道路的折线坐标;.shx是索引文件,让软件能快速定位到某条道路的几何记录;.dbf是属性表,存道路名称、等级、长度等字段;.prj存坐标系定义,决定这条道路在地球上的真实位置;.cpg存字符编码,决定中文道路名会不会变成乱码。这五个文件必须同名同目录,缺一不可。

文件后缀作用缺失后果
.shp几何坐标完全打不开
.shx几何索引能打开但查询极慢或报错
.dbf属性字段有路形但无名称无等级
.prj坐标系叠加到地图上位置偏移
.cpg字符编码中文路名乱码

注意:.prj文件如果内容是GEOGCS["GCS_WGS_1984"...],说明是地理坐标系(经纬度);如果是PROJCS["...Beijing..."...],说明是投影坐标系(米为单位)。两者混用会直接导致面积、长度计算错误。

2.2 用 Python 读取并检查数据完整性

拿到数据包后,我一般先用geopandas做一次体检,确认几何类型、坐标系、字段和记录数。下面这段代码可以直接抄。

import geopandas as gpd # 读取 shapefile,注意路径不要带中文空格 gdf = gpd.read_file("./beijing_roads/beijing_roads.shp") # 1. 看坐标系 print("CRS:", gdf.crs) # 2. 看几何类型和记录数 print("几何类型:", gdf.geom_type.unique()) print("道路条数:", len(gdf)) # 3. 看字段名和前 5 行 print("字段:", gdf.columns.tolist()) print(gdf.head()) # 4. 检查是否有空几何 print("空几何数量:", gdf.geometry.is_empty.sum()) # 5. 检查坐标系是否为经纬度 if gdf.crs and gdf.crs.is_geographic: print("当前是地理坐标系,计算长度前需投影")

逻辑说明:read_file会自动关联同名的.shx、.dbf、.prj,所以只要这五个文件在同一目录且同名,就能一次性读全。gdf.crs返回坐标系对象,is_geographic为 True 表示经纬度。geometry.is_empty用来排查有没有空几何,空几何在后续做缓冲区或相交分析时会直接报错。

参数说明:路径建议用相对路径,避免 Windows 反斜杠转义问题。如果.dbf里的中文乱码,可以在read_file里加encoding="utf-8"或encoding="gbk"试,具体看.cpg文件里写的编码。

2.3 道路等级字段的常见命名与含义

北京城区道路数据包的属性表里,道路等级字段常见命名有fclass、type、class、road_type等。不同来源的数据包字段名不一样,但含义大致对应:高速/快速路、主干路、次干路、支路、辅路、步行道等。做交通分析时,通常只保留主干路以上;做底图渲染时,支路也要保留但线宽调细。

# 假设等级字段叫 fclass,统计各类道路数量 print(gdf["fclass"].value_counts()) # 只保留主干路及以上(按实际字段值调整) main_roads = gdf[gdf["fclass"].isin(["motorway", "trunk", "primary"])] print("主干路条数:", len(main_roads))

逻辑说明:value_counts()先摸清字段里到底有哪些值,避免凭猜测过滤。isin做多值筛选,比写多个==更简洁。过滤后建议单独存一份,方便后续渲染分层。

参数说明:如果字段值是中文,比如「主干路」「次干路」,直接把列表里的英文替换成中文即可。注意有些数据包用数字编码,比如 1 代表高速、2 代表主干,这时要先查数据说明或看.dbf里的字段定义。

3. 从 Shapefile 到可交互路网图:投影、简化与导出三步走

读到数据只是第一步,真正要拿来做可视化或分析,还得处理坐标系、几何简化和格式转换。这一步做不好,要么地图卡顿,要么长度算错,要么导出后前端加载失败。

3.1 投影转换:为什么经纬度不能直接算长度

经纬度是球面坐标,单位是度,不是米。直接对经纬度做length计算,得到的是「度」而不是「米」,数值没有实际意义。北京城区大约在东经 116.4 度、北纬 39.9 度附近,常用投影是 UTM Zone 50N 或 Web Mercator(EPSG:3857)。做长度、面积、缓冲区分析时,必须转到投影坐标系。

# 转到 UTM Zone 50N(北京适用),单位米 gdf_utm = gdf.to_crs(epsg=32650) # 计算每条道路长度(米) gdf_utm["length_m"] = gdf_utm.geometry.length print(gdf_utm["length_m"].describe()) # 如果要做 Web 地图叠加,转到 EPSG:3857 gdf_web = gdf.to_crs(epsg=3857)

逻辑说明:to_crs做坐标系转换,epsg=32650对应 UTM Zone 50N,覆盖北京及周边。geometry.length在投影坐标系下返回米。describe()看长度分布,能快速发现异常值,比如某条路长度几万米,可能是几何错误。

参数说明:如果数据包原始坐标系不是 WGS84,而是北京地方坐标系(如 BJ54 或 CGCS2000 的地方投影),需要先确认.prj内容,再用对应的 EPSG 码转换。不确定时,用gdf.crs打印出来,去 EPSG 官网查对应编码。

3.2 几何简化:让前端加载不卡顿

北京城区道路数据包如果包含支路和步行道,几何点数量可能非常大,直接导出 GeoJSON 给前端,文件几十兆,浏览器加载卡死。常见做法是用simplify做几何简化,容差根据显示层级调整。

# 简化几何,容差 10 米(投影坐标系下) gdf_utm["geometry"] = gdf_utm.geometry.simplify(tolerance=10, preserve_topology=True) # 导出 GeoJSON 给前端 gdf_utm.to_file("./beijing_roads_simplified.geojson", driver="GeoJSON")

逻辑说明:simplify用 Douglas-Peucker 算法减少折线点数,tolerance=10表示 10 米以内的弯曲被拉直。preserve_topology=True防止简化后出现自相交。导出 GeoJSON 时,坐标系会保留当前投影,前端用 Leaflet 或 Mapbox 加载时注意再做一次转换。

参数说明:容差不是越大越好。做全市概览图,容差可以到 50 米;做街区级分析,容差建议 1 到 5 米。简化前先备份原始数据,避免不可逆。

3.3 按道路等级分层导出与渲染参数

前端渲染时,不同等级道路用不同线宽和颜色,视觉层次才清晰。我一般按等级拆成多个 GeoJSON,或者在一个文件里加level字段,前端按字段过滤。

# 按等级分层导出 for level in gdf_utm["fclass"].unique(): subset = gdf_utm[gdf_utm["fclass"] == level] subset.to_file(f"./roads_{level}.geojson", driver="GeoJSON") print(level, len(subset))

逻辑说明:循环按等级拆分,每个等级一个文件,前端按需加载。文件名用等级值,避免中文路径问题。如果等级值有特殊字符,先做替换。

参数说明:导出 GeoJSON 时,如果字段里有中文,确保encoding="utf-8"。前端渲染线宽建议:高速 4px、主干 3px、次干 2px、支路 1px,颜色从深到浅。

4. 避坑与排查:道路数据包最常见的五个翻车现场

这一章是我和身边同行踩过的真实坑,按「现象 → 原因 → 解决」写,遇到问题直接对号入座。

4.1 打开后地图偏移几百米

现象:把道路数据叠加到在线底图上,路网整体偏移,不是个别路段。原因:.prj定义的坐标系和底图坐标系不一致,常见是数据包用地方投影,底图用 WGS84 或 Web Mercator。解决:先用gdf.crs确认原始坐标系,再用to_crs转到与底图一致的坐标系。如果.prj缺失,需要根据数据来源判断,北京城区数据常见是 CGCS2000 或 WGS84。

4.2 中文路名全部乱码

现象:属性表里道路名称显示为「???」或乱码字符。原因:.dbf文件的字符编码和读取时指定的编码不一致,常见是数据包用 GBK,读取时默认 UTF-8。解决:先看.cpg文件内容,如果写的是GBK或GB2312,读取时加encoding="gbk";如果.cpg缺失,用encoding="gbk"和encoding="utf-8"各试一次,看哪个正常。

4.3 道路断成碎段,无法做路径分析

现象:一条完整道路被拆成很多小段,拓扑不连续。原因:原始数据按路口切分,或者数据包本身就是分段存储。解决:做路径分析前,先用unary_union或linemerge合并同名字段的路段,再构建拓扑。如果字段里没有唯一道路 ID,需要按名称和空间邻接关系合并。

# 按道路名称合并碎段 merged = gdf_utm.dissolve(by="name", aggfunc="first") print("合并后条数:", len(merged))

逻辑说明:dissolve按指定字段分组,把同组几何合并成一个。aggfunc="first"保留每组第一条记录的属性。合并后几何类型可能变成 MultiLineString,做路径分析前再转回 LineString。

参数说明:by字段必须是道路唯一标识,如果名称有重复或空值,先清洗。空名称的路段建议单独处理,不要直接合并。

4.4 导出 GeoJSON 后文件过大

现象:导出的 GeoJSON 几十兆,前端加载超时。原因:几何点未简化,或者包含了过多属性字段。解决:先simplify简化几何,再只保留必要字段导出。GeoJSON 默认保留所有字段,用gdf[["name", "fclass", "geometry"]]筛选后再导出。

4.5 坐标系转换后长度反而错了

现象:转到投影坐标系后,计算的道路长度和实际差距很大。原因:转错了投影带,比如北京用 UTM Zone 50N,误用了 Zone 49N 或 51N。解决:确认北京经度范围约 115.7 到 117.4 度,UTM Zone 50N 覆盖 114 到 120 度,正确。如果转到 Web Mercator,高纬度地区长度会失真,不适合做精确长度计算。

注意:Web Mercator 在纬度 40 度附近长度变形约 1.3 倍,做长度统计时不要用 EPSG:3857,用 UTM 或地方投影。

5. 进阶技巧:用道路数据包做一次真实的服务区覆盖分析

前面讲了读取、转换、导出和避坑,这一章落到一个具体分析场景:给定几个候选点,分析每个点 500 米范围内覆盖的道路总长度。这个分析在选址、应急响应、商业布点里都很常见。我用北京城区道路数据包走一遍完整流程,代码可以直接改参数复用。

5.1 构建缓冲区并做空间相交

思路是:把候选点转成投影坐标系,做 500 米缓冲区,再和道路数据做相交,统计相交后的道路长度。

import geopandas as gpd from shapely.geometry import Point # 读取道路数据并投影 roads = gpd.read_file("./beijing_roads/beijing_roads.shp").to_crs(epsg=32650) # 定义候选点(经纬度转投影) candidates = gpd.GeoDataFrame( {"name": ["A点", "B点", "C点"]}, geometry=[Point(116.40, 39.90), Point(116.45, 39.92), Point(116.35, 39.88)], crs="EPSG:4326" ).to_crs(epsg=32650) # 做 500 米缓冲区 candidates["buffer"] = candidates.geometry.buffer(500) # 用缓冲区去裁剪道路 results = [] for _, row in candidates.iterrows(): clipped = gpd.clip(roads, row["buffer"]) total_length = clipped.geometry.length.sum() results.append({"name": row["name"], "road_length_m": round(total_length, 2)}) print(results)

逻辑说明:buffer(500)生成 500 米圆缓冲区。gpd.clip用缓冲区几何裁剪道路,保留落在缓冲区内的部分。geometry.length.sum()统计裁剪后道路总长度。结果是一个列表,每个候选点对应一个覆盖长度。

参数说明:缓冲区半径按业务定,500 米适合步行覆盖,1000 米适合骑行。clip要求两个 GeoDataFrame 坐标系一致,所以候选点先转 EPSG:32650。如果候选点很多,循环会慢,可以用sjoin先做空间连接再统计。

5.2 用 sjoin 加速批量分析

候选点超过几十个时,逐个clip效率低。改用sjoin做空间连接,再按点分组统计。

# 空间连接:道路落在哪个缓冲区里 joined = gpd.sjoin(roads, candidates.set_geometry("buffer"), predicate="intersects") # 按候选点分组统计道路长度 summary = joined.groupby("name")["length_m"].sum().reset_index() print(summary)

逻辑说明:sjoin返回所有相交的道路和候选点组合,predicate="intersects"表示只要相交就保留。groupby按候选点名称分组,sum统计长度。注意这里用的是道路数据里已有的length_m字段,如果没有,先算好再连接。

参数说明:sjoin的how参数默认inner,只保留相交记录。如果候选点没有覆盖任何道路,结果里不会出现,需要how="left"保留所有候选点。

5.3 结果可视化与验证

分析结果最好落到地图上验证,避免缓冲区画错或坐标系搞混。

import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(10, 10)) roads.plot(ax=ax, linewidth=0.5, color="gray") candidates.buffer.plot(ax=ax, alpha=0.3, color="blue") candidates.plot(ax=ax, color="red", markersize=20) plt.title("候选点 500 米道路覆盖") plt.show()

逻辑说明:先画道路底图,再叠缓冲区,最后画候选点。颜色区分明显,一眼能看出缓冲区是否落在路网密集区。如果缓冲区位置明显偏了,回去检查坐标系转换。

参数说明:alpha控制透明度,0.3 能看清底下的路网。markersize调大点,避免被缓冲区盖住。

我自己的习惯是:每次拿到新的道路数据包,先跑一遍第 2 章的体检代码,确认坐标系和字段,再动手做分析。这个习惯帮我省过至少三次返工。希望帮到你。

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

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

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

立即咨询