☰
杭州乡镇街道矢量边界SHP数据包实战:从坐标拓扑到空间分析
2026/10/14 3:44:45 网站建设 项目流程

简介:这份杭州乡镇街道矢量边界数据包面向GIS制图、空间分析与基层治理可视化从业者,提供杭州市全域最新版乡镇街道级矢量地理信息。数据覆盖上城区、拱墅区、西湖区、滨江区、萧山区、余杭区、临平区、钱塘区、富阳区、临安区、桐庐县、淳安县、建德市等全部行政区划,坐标系已统一,可直接导入ArcGIS、QGIS、SuperMap等主流平台,用于区域统计、人口热力叠加与专题制图。压缩包共27个文件,约598KB,以shp、shx、dbf、prj等SHP核心与空间参考文件为主,另含sbn、sbx索引及shp.xml元数据,并附Python脚本与预览图,兼顾数据调用与快速查看。文件命名规范、结构清晰,既提供各区县独立SHP,也提供乡镇街道合并版,支持按需提取单个区县或全市整体使用,无加密、无水印、无需授权,下载即用。目前已有180人学习下载,适合需要快速获取杭州基层行政区划底图的中高级GIS用户。

1. 杭州乡镇街道矢量边界数据包:从区县到街道的 SHP 落地实录

做杭州本地的空间分析,绕不开乡镇街道这一级。区县级边界网上随手能搜到,但再往下切到街道、乡镇,要么缺属性、要么拓扑断裂、要么坐标系对不上,最后只能自己描。这份杭州最新乡镇街道矢量边界数据包,按区县拆成独立 SHP 文件,覆盖上城、拱墅、西湖、滨江、萧山、余杭、临平、钱塘、富阳、临安、桐庐、淳安、建德等区县,每个文件里是乡镇街道面要素,带名称、代码等字段。它解决的是「底图从哪来」的问题,适合做人口格网、POI 聚合、行政区划统计、选址分析的人。下面按我实际拆包、入库、跑分析的顺序讲一遍,参数和坑都摊开。

2. 数据包结构拆解:SHP 文件里到底装了什么

拿到一个压缩包,先别急着往 GIS 里拖。SHP 是 ESRI Shapefile 的简称,它从来不是单个文件,而是一组同名文件的集合。很多人第一次用会只拷.shp,结果打开是空图层或者属性全丢,这就是没搞懂它的组织方式。

2.1 Shapefile 的组成与最小可用集

一个完整的 Shapefile 至少包含三个文件,缺一不可:

文件后缀作用缺失后果
.shp存储几何形状(点/线/面坐标)无法显示图形
.shx几何索引,记录每个要素在 shp 中的偏移部分软件报错或加载慢
.dbf属性表,存名称、代码等字段图形在但属性为空
.prj坐标系定义(WKT 文本)坐标未知,叠加错位
.cpg属性表字符编码中文名乱码

这份数据包按区县分目录,每个目录下是一套完整的同名文件。常见做法是保持文件夹整体不动,用的时候整个目录读进去,而不是只挑.shp。

2.2 用 Python 批量读取并核对字段

我一般先用geopandas把每个区县文件过一遍,确认几何类型、坐标系、字段名和要素数量,避免后面分析到一半才发现某个区县是空的。

import geopandas as gpd import os # 数据包解压后的根目录,按区县分子目录 root = "./hangzhou_township" for county in sorted(os.listdir(root)): path = os.path.join(root, county) if not os.path.isdir(path): continue # 找到目录下的 .shp 文件 shp_files = [f for f in os.listdir(path) if f.endswith(".shp")] for shp in shp_files: gdf = gpd.read_file(os.path.join(path, shp)) # 打印区县、几何类型、坐标系、要素数、字段 print(county, shp, gdf.geom_type.unique(), gdf.crs, len(gdf), list(gdf.columns))

这段代码的逻辑是遍历每个区县目录,读取其中的.shp,输出几何类型、CRS、要素数量和字段列表。参数上,gdf.crs会显示坐标系,如果输出是None,说明.prj缺失或没被识别,需要手动指定。gdf.geom_type.unique()正常应该只返回Polygon或MultiPolygon,如果混进LineString,说明数据里混了边界线要素,得先筛掉。

2.3 字段含义与命名规范

跑完上面那段,你会看到属性表里通常有这几类字段:名称字段(如NAME、TOWN、XZQMC)、代码字段(如CODE、XZQDM)、可能还有上级区县字段。字段名不统一是这类数据的通病,有的区县用拼音缩写,有的用中文全称。我的处理习惯是统一重命名成town_name和town_code,后面做关联统计时不用记两套名字。

# 统一字段名,兼容不同区县的命名差异 rename_map = {} for col in gdf.columns: low = col.lower() if low in ("name", "town", "xzqmc", "town_name"): rename_map[col] = "town_name" elif low in ("code", "xzqdm", "town_code"): rename_map[col] = "town_code" gdf = gdf.rename(columns=rename_map)

这里用lower()做大小写归一,避免Name和name被当成两个字段。重命名后建议把每个区县统一存成 GeoPackage 或 Parquet,方便后续批量处理,也避免 Shapefile 字段名长度限制(10 字符)带来的截断问题。

3. 坐标系与拓扑处理:让边界能叠到地图上

数据能用和用得准是两回事。乡镇街道这一级,坐标系选错会让面积算错、叠加偏移,拓扑不干净会让空间连接出现重复匹配。这一章讲怎么把坐标和拓扑收拾干净。

3.1 地理坐标系与投影坐标系的选择

杭州位于东经 118° 到 120° 之间,常用的投影坐标系是 CGCS2000 的 3 度带,中央经线 120°E,带号 40,对应 EPSG:4547(CGCS2000 / 3-degree Gauss-Kruger CM 120E)。如果数据包里的.prj是地理坐标系(如 EPSG:4326,WGS84 经纬度),直接算面积会得到平方度,没有物理意义。

# 判断当前坐标系,必要时投影到 EPSG:4547 if gdf.crs is None: # 数据包若未带 .prj,按常见情况指定为 CGCS2000 地理坐标 gdf = gdf.set_crs(epsg=4490) # 投影到 3 度带,单位变为米 gdf_proj = gdf.to_crs(epsg=4547) # 此时算面积才是平方米 gdf_proj["area_m2"] = gdf_proj.geometry.area

参数说明:set_crs是「声明」坐标系,不改变坐标值,用于.prj缺失时补上;to_crs是「转换」坐标系,会重算坐标。两者别搞反,否则坐标会被二次投影,位置直接飞到境外。EPSG:4490 是 CGCS2000 地理坐标,和 WGS84 在米级精度上差异很小,但正式成果建议用 4490 而非 4326。

3.2 拓扑检查与常见几何错误修复

乡镇街道面数据常见的几何问题有三类:自相交(一个面自己拧麻花)、重叠(两个街道压在一起)、缝隙(相邻街道之间有细缝)。这些在叠加分析时会变成玄学 bug,比如同一个点被两个街道同时包含。

from shapely.validation import explain_validity # 逐要素检查几何有效性 invalid = gdf_proj[~gdf_proj.geometry.is_valid] for idx, row in invalid.iterrows(): print(idx, row["town_name"], explain_validity(row.geometry)) # 用 buffer(0) 修复大部分自相交和重复点问题 gdf_proj["geometry"] = gdf_proj.geometry.buffer(0)

is_valid返回布尔序列,explain_validity会告诉你具体哪里有问题,比如Self-intersection或Ring Self-intersection。buffer(0)是常用的修复手段,对自相交和部分重叠有效,但它不解决缝隙。缝隙要用snap或者先做unary_union再切分。我一般会先跑一遍有效性检查,把无效要素单独导出看一眼,确认是数据问题还是读取问题。

3.3 区县合并与跨区拓扑一致性

如果你要做全市分析,需要把各区县文件合并成一个图层。合并时要注意相邻区县交界处的街道边界是否对齐,否则会出现细缝或重叠。

import pandas as pd gdfs = [] for county in sorted(os.listdir(root)): path = os.path.join(root, county) if not os.path.isdir(path): continue for shp in [f for f in os.listdir(path) if f.endswith(".shp")]: g = gpd.read_file(os.path.join(path, shp)).to_crs(epsg=4547) g["county"] = county gdfs.append(g) hz = gpd.GeoDataFrame(pd.concat(gdfs, ignore_index=True), crs="EPSG:4547") # 检查全市范围内是否有重叠 hz["geometry"] = hz.geometry.buffer(0)

合并后建议做一次全市范围的重叠检测,用gpd.overlay或者两两求交,看有没有面积不为零的交集。如果有,说明两个区县的街道边界定义不一致,需要人工确认以哪个为准。这一步在跨区分析里不能省,否则统计口径会对不上。

4. 避坑与排查:乡镇边界数据最容易翻车的五个地方

这一章是我自己踩过的坑,按「现象 → 原因 → 解决」写,你遇到类似情况可以直接对号入座。

4.1 中文属性乱码

现象:打开属性表,街道名称显示成????或é•¿æ²™。 原因:.dbf的字符编码没被正确识别,常见于.cpg文件缺失或内容写错。 解决:先看目录下有没有.cpg,没有就手动建一个,内容写UTF-8。如果已经是 UTF-8 还乱码,用geopandas读取时指定encoding="gbk"试一次,老数据很多是 GBK。

4.2 坐标系缺失导致叠加偏移

现象:把街道边界叠到在线底图上,整体偏移几百米。 原因:.prj缺失,软件默认按 WGS84 处理,但数据实际是 CGCS2000 或地方坐标。 解决:用set_crs声明正确坐标系后再to_crs转换。判断方法是对比几个已知地标的坐标,比如某区政府大楼,看偏移量是否在合理范围。

4.3 街道名称重复导致关联错误

现象:用街道名称做表关联,结果一个区县的记录匹配到了另一个区县的同名街道。 原因:不同区县存在同名街道,比如多个区都有「XX 街道」。 解决:关联时用「区县 + 街道」组合键,或者直接用行政区划代码。代码字段如果有前导零,读进来可能变成整数丢零,要用dtype={"town_code": str}指定字符串类型。

4.4 几何无效导致空间连接丢要素

现象:做空间连接(sjoin)时,某些点明明在街道内却没被匹配上。 原因:街道面几何无效,比如自相交,导致contains判断失败。 解决:先跑is_valid检查,用buffer(0)修复。修复后重新做空间连接,对比修复前后的匹配数量,差异大说明原数据问题严重。

4.5 面积单位算错

现象:算出来的街道面积是零点几,明显不对。 原因:数据还是地理坐标系,直接算面积得到的是平方度。 解决:先to_crs投影到 EPSG:4547 再算面积。投影后面积单位是平方米,除以 1e6 得平方公里。杭州一个乡镇街道的面积通常在几到几百平方公里,如果算出来是 0.00x,基本就是单位问题。

5. 从边界到分析:三个能直接跑的场景

数据收拾干净后,价值在于用它跑分析。这一章给三个我常用的场景,代码可以直接改参数复用。

5.1 按街道聚合 POI 并出统计表

假设你有一份杭州的 POI 点数据,想统计每个街道的餐饮数量。

import pandas as pd # 读取 POI,假设有 lon/lat 字段 poi = pd.read_csv("hangzhou_poi.csv") poi_gdf = gpd.GeoDataFrame( poi, geometry=gpd.points_from_xy(poi["lon"], poi["lat"]), crs="EPSG:4326" ).to_crs(epsg=4547) # 空间连接,把街道属性挂到每个点上 joined = gpd.sjoin(poi_gdf, hz[["town_name", "county", "geometry"]], how="left", predicate="within") # 按区县和街道统计 stat = joined.groupby(["county", "town_name"]).size().reset_index(name="poi_count") print(stat.sort_values("poi_count", ascending=False).head(10))

逻辑说明:points_from_xy把经纬度转成点几何,to_crs统一到投影坐标后做sjoin。predicate="within"表示点落在面内才算匹配。how="left"保留所有点,落在边界外的点街道字段为空,方便排查。参数上,如果 POI 本身是投影坐标,就不用再转,但两边 CRS 必须一致。

5.2 计算街道面积与人口密度

有了面积,就能算密度。假设你有一份街道级人口数据,用代码字段关联。

# 计算面积(平方公里) hz["area_km2"] = hz.geometry.area / 1e6 # 读取人口数据,代码字段强制为字符串 pop = pd.read_csv("town_pop.csv", dtype={"town_code": str}) # 关联 merged = hz.merge(pop, on="town_code", how="left") merged["density"] = merged["population"] / merged["area_km2"] # 检查关联率 print("未匹配数:", merged["population"].isna().sum())

参数说明:dtype={"town_code": str}防止前导零丢失,这是关联失败最常见的原因。how="left"保留所有街道,未匹配的population为空,用isna().sum()看关联率。如果未匹配数很高,检查两边的代码格式是否一致,比如一边是 12 位一边是 9 位。

5.3 导出为 GeoJSON 供前端使用

如果要把边界给前端做可视化,GeoJSON 比 Shapefile 方便。

# 简化几何,减小文件体积,tolerance 单位是米 hz_simplified = hz.copy() hz_simplified["geometry"] = hz_simplified.geometry.simplify(tolerance=50) # 导出,ensure_ascii=False 保证中文正常 hz_simplified.to_file("hangzhou_township.geojson", driver="GeoJSON", encoding="utf-8")

simplify的tolerance控制简化程度,50 米在市级尺度上肉眼几乎看不出差异,但文件能小一半以上。注意简化后可能出现新的几何无效,导出前再跑一次is_valid检查。encoding="utf-8"保证中文属性不乱码。

6. 进阶技巧:用空间索引把批量查询从分钟压到秒级

前面几个场景数据量不大,直接跑没问题。但如果你要拿几百万个点去匹配街道,或者反复做空间查询,没有空间索引会慢到怀疑人生。这一章讲怎么用 GeoPandas 的空间索引和 Rtree 加速,以及一个我常用的批量匹配写法。

6.1 空间索引的原理与启用方式

空间索引的本质是用一棵 R 树把几何的外包矩形组织起来,查询时先过滤掉不可能相交的要素,再做精确判断。GeoPandas 在sjoin内部会自动建索引,但如果你自己写循环做contains判断,索引就不会生效。

# 手动构建空间索引 sindex = hz.sindex # 查询某个点可能落在哪些街道的外包矩形内 point = poi_gdf.geometry.iloc[0] candidates = list(sindex.intersection(point.bounds)) # candidates 是候选要素的索引,再逐个精确判断 for idx in candidates: if hz.geometry.iloc[idx].contains(point): print(hz.iloc[idx]["town_name"])

sindex.intersection接收一个边界框(minx, miny, maxx, maxy),返回可能相交的要素索引列表。这一步是粗筛,比全量遍历快几个数量级。然后再用contains做精筛。这个模式在自定义空间逻辑时非常有用,比如你要判断点是否在街道内且满足其他属性条件。

6.2 批量点匹配的性能对比

我做过一个测试,10 万个点匹配 200 个街道面,不同写法的耗时差异很明显:

方法耗时(秒)说明
双重循环 + contains约 180每个点遍历所有面,最慢
sjoin(内置索引)约 2.5自动建索引,推荐
手动 sindex 粗筛 + contains约 4灵活但代码多
先按区县分组再 sjoin约 1.8减少单次匹配的面数量

结论很直接:能用sjoin就用sjoin,它内部已经做了索引优化。如果要做自定义判断,用sindex粗筛。按区县分组再匹配是个实用技巧,因为每个区县的面数量少,索引树更浅,匹配更快。

6.3 一个我常用的批量匹配函数

最后给一个封装好的函数,输入点 GeoDataFrame 和面 GeoDataFrame,输出带面属性的点表,内部做了 CRS 对齐和索引优化。

def batch_spatial_join(points, polygons, point_crs="EPSG:4326", poly_crs="EPSG:4547", predicate="within"): """ points: 点 GeoDataFrame 或带 lon/lat 的 DataFrame polygons: 面 GeoDataFrame predicate: within / intersects / contains """ # 统一 CRS 到面数据的坐标系 if points.crs is None: points = points.set_crs(point_crs) points = points.to_crs(polygons.crs) # 用 sjoin 做空间连接,内置空间索引 result = gpd.sjoin(points, polygons, how="left", predicate=predicate) return result

这个函数的关键点是先对齐 CRS 再sjoin,避免坐标系不一致导致的空匹配。predicate参数按需改,within适合点面归属,intersects适合边界也算的情况。返回结果里右边面的属性会带_right后缀,按需重命名即可。

从那以后我每次拿到新的矢量数据,都强制先跑一遍is_valid和 CRS 检查,再开始任何分析。这两个检查花不了两分钟,但能省掉后面几小时的排查。希望帮到你。

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

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

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

立即咨询