简介:这份浙江省乡镇街道边界数据包面向GIS开发者、地图可视化工程师及区域规划研究人员,解决乡镇级行政边界获取与地图集成的问题。包内共11个文件,约19.08MB,涵盖Shapefile所需的shp、shx、dbf、prj、cpg等核心组件,以及GeoJSON、SVG、xml等格式,可分别用于ArcGIS、QGIS等桌面软件处理、高德地图API边界叠加和Echarts交互地图渲染。数据以浙江省乡镇街道为粒度,支持面积计算、边界重叠分析与人口经济等多维区域研究。目前已有1184人学习下载,适合需要快速搭建乡镇级地图应用或开展空间数据分析的读者直接取用。
1. 拿到一份乡镇街道边界数据,先别急着往 GIS 里拖
做行政区划相关的项目,绕不开一份基础底图:乡镇街道级别的边界。省级、市级边界网上相对好找,但一落到乡镇街道这一级,数据来源就变得零散,格式也五花八门。最近不少人在找「浙江省乡镇街道边界数据」这类资源,压缩包拿到手之后,第一反应往往是直接拖进 GIS 软件看效果,结果要么坐标系对不上,要么属性表里一堆乱码,要么边界之间留着肉眼可见的缝隙。这篇笔记就围绕这份数据,把从解压、格式识别、坐标系处理、属性清洗到实际叠加分析的完整链路讲清楚。适合做国土空间规划、人口热力、物流分区、门店选址的从业者,也适合刚接触矢量数据处理的新手。读完你应该能独立把一份乡镇街道边界数据整理成能直接用于空间分析的干净底图,并且知道每一步为什么这么做、哪里容易翻车。
2. 先搞清楚这份数据里到底装了什么:格式识别与结构拆解
拿到压缩包,别急着解压到桌面。先看扩展名和体积,这一步能帮你判断数据是矢量还是栅格、是单文件还是多文件组合。乡镇街道边界数据绝大多数是矢量格式,常见的有 Shapefile、GeoJSON、KML、File Geodatabase 几种。Shapefile 是一组文件(.shp、.shx、.dbf、.prj 等),单独一个 .shp 是打不开的;GeoJSON 是单文件,文本可读;KML 多用于展示,属性字段支持较弱。判断清楚格式,后面的工具选择才有依据。
2.1 解压后先做一次文件清单核对
解压到一个纯英文路径下,路径里不要有空格和中文,这是血泪经验。很多 GIS 工具和 Python 库对中文路径的处理并不一致,报错信息还特别含糊,排查半天才发现是路径问题。解压后列出文件清单,重点看有没有 .prj 文件。.prj 记录的是坐标系信息,缺了它,数据就变成了「没有地理参照的图形」,后面叠加任何底图都会错位。
# 列出解压目录下的所有文件,关注扩展名和体积 ls -lh ./zhejiang_township/ # 典型 Shapefile 组合应该长这样: # zhejiang_township.shp 主几何文件 # zhejiang_township.shx 几何索引 # zhejiang_township.dbf 属性表 # zhejiang_township.prj 坐标系定义 # zhejiang_township.cpg 属性表编码说明这段命令的作用是快速确认文件完整性。如果只有 .shp 没有 .dbf,属性表就丢了;没有 .prj,坐标系就得靠猜。.cpg 文件容易被忽略,但它决定了 .dbf 里中文字段会不会变成乱码。如果缺 .cpg,可以手动创建一个同名 .cpg 文件,内容写 UTF-8 或 GBK,取决于原始数据的编码。
2.2 用 Python 读取并检查几何与属性
确认文件齐全后,用 Geopandas 读进来做一次体检。这一步要看的指标包括:要素数量、几何类型、坐标系、字段列表、字段类型、是否有空几何、是否有无效几何。乡镇街道边界数据常见的几何类型是 Polygon 或 MultiPolygon,如果出现 LineString 或 Point,说明数据可能被裁剪过或者本身不是面数据。
import geopandas as gpd # 读取 Shapefile,指定编码避免中文乱码 gdf = gpd.read_file("./zhejiang_township/zhejiang_township.shp", encoding="utf-8") # 基础体检 print("要素数量:", len(gdf)) print("几何类型:", gdf.geom_type.unique()) print("坐标系:", gdf.crs) print("字段列表:", gdf.columns.tolist()) print("字段类型:\n", gdf.dtypes) # 检查空几何和无效几何 print("空几何数量:", gdf.geometry.is_empty.sum()) print("无效几何数量:", (~gdf.geometry.is_valid).sum()) # 预览前几行属性 print(gdf.head())逻辑说明:encoding参数很关键,浙江的乡镇街道数据属性表里通常有中文名称,编码不对就会读出乱码。gdf.crs如果返回 None,说明 .prj 缺失或未被识别,需要手动指定。is_valid检查几何是否自相交、是否有重复节点,无效几何会在后续空间分析中导致拓扑错误。参数方面,如果数据量很大,head()可以换成sample(5)随机抽样,避免只看前几行漏掉问题。
2.3 坐标系判断:地理坐标还是投影坐标
这一步是选型的分水岭。如果gdf.crs显示的是 EPSG:4326 或类似的地理坐标系(单位是度),那么直接做面积计算、距离计算都会得到错误结果,因为度不是长度单位。乡镇街道级别的分析,通常需要投影到适合浙江的投影坐标系,比如 CGCS2000 3度带或 UTM 50N。判断方法很简单:看坐标数值的量级。经纬度数值在 118-123 和 27-31 之间,就是地理坐标;数值在几十万到几百万,就是投影坐标。
# 查看坐标数值范围,判断坐标系类型 bounds = gdf.total_bounds print("坐标范围:", bounds) # 如果是地理坐标,投影到适合浙江的坐标系 if gdf.crs and gdf.crs.is_geographic: # CGCS2000 3度带,中央经线120度,EPSG:4547 左右 gdf_proj = gdf.to_crs(epsg=4547) print("已投影,新坐标范围:", gdf_proj.total_bounds) else: gdf_proj = gdf.copy()参数说明:EPSG:4547 是 CGCS2000 / 3-degree Gauss-Kruger CM 120E,覆盖浙江大部分区域。如果数据跨带,需要按带号分别投影再合并。投影之后,面积和距离计算才有意义。这一步不做,后面算乡镇面积、做缓冲区全是错的,而且错得很隐蔽。
3. 把边界数据洗干净:属性表处理与拓扑修复的完整流程
数据能读进来只是第一步,真正决定能不能用的是属性表和拓扑质量。乡镇街道边界数据常见的问题有三类:属性字段命名混乱、名称字段有重复或缺失、边界之间有缝隙或重叠。这一章按顺序处理,每一步都有对应的检查方法和修复手段。
3.1 属性表字段重命名与冗余字段清理
原始数据的字段名经常是拼音缩写或者无意义的英文,比如 XZQDM、XZQMC、CODE、NAME_1 之类。做分析之前,把关键字段重命名成可读的名字,同时删掉用不上的字段,能显著减少后续操作的干扰。关键字段一般包括:行政区划代码、乡镇街道名称、所属区县、所属地市。
# 重命名关键字段,删除冗余字段 gdf_clean = gdf_proj.rename(columns={ "XZQDM": "adcode", # 行政区划代码 "XZQMC": "name", # 乡镇街道名称 "QYMC": "county", # 所属区县 "DSMC": "city" # 所属地市 }) # 只保留需要的字段 keep_cols = ["adcode", "name", "county", "city", "geometry"] gdf_clean = gdf_clean[keep_cols] # 检查名称字段是否有空值 print("名称为空的数量:", gdf_clean["name"].isna().sum()) print("代码为空的数量:", gdf_clean["adcode"].isna().sum())逻辑说明:重命名之前先用gdf.columns.tolist()确认原始字段名,不要凭猜测写映射。删除字段时保留 geometry 列,否则 GeoDataFrame 会退化成普通 DataFrame。名称和代码的空值检查是必须的,乡镇街道名称缺失会导致后续按名称关联失败,代码缺失则无法和统计年鉴等外部数据对接。
3.2 名称去重与行政区划代码校验
乡镇街道名称在不同区县之间可能重复,比如多个区县都有「城关镇」「开发区」。如果只按名称做关联,会串数据。正确做法是用行政区划代码做主键,名称只做展示。行政区划代码一般是 12 位(乡镇街道级),前 6 位是区县代码,中间 3 位是乡镇街道顺序码,后 3 位是村级代码(乡镇级通常补 000)。校验代码长度和前缀,能发现数据质量问题。
# 校验行政区划代码长度和唯一性 gdf_clean["adcode_len"] = gdf_clean["adcode"].astype(str).str.len() print("代码长度分布:\n", gdf_clean["adcode_len"].value_counts()) # 检查代码是否唯一 dup_codes = gdf_clean[gdf_clean.duplicated("adcode", keep=False)] print("重复代码数量:", len(dup_codes)) # 检查名称在同一区县内是否唯一 dup_names = gdf_clean[gdf_clean.duplicated(["county", "name"], keep=False)] print("同区县内重名数量:", len(dup_names))参数说明:乡镇街道级行政区划代码标准长度是 12 位,如果出现 9 位或 6 位,说明数据可能是村级或区县级被误标。重复代码通常意味着数据合并时出了问题,需要人工核对。同区县内重名如果存在,说明名称字段可能没有区分「街道」和「镇」,或者数据本身有重复记录。
3.3 拓扑修复:缝隙、重叠与无效几何的处理
乡镇街道边界理论上应该无缝无重叠,覆盖整个区域。但实际数据经常有缝隙(相邻边界没接上)和重叠(两块边界压在一起)。缝隙会导致空间统计时出现「无归属」区域,重叠会导致同一块面积被算两次。修复方法是用buffer(0)处理无效几何,用拓扑工具做缝隙检测和重叠检测。
from shapely.validation import make_valid # 修复无效几何 gdf_clean["geometry"] = gdf_clean["geometry"].apply( lambda geom: make_valid(geom) if not geom.is_valid else geom ) # 检测重叠:两两相交面积大于阈值 overlaps = [] for i, row_i in gdf_clean.iterrows(): for j, row_j in gdf_clean.iterrows(): if i < j and row_i.geometry.intersects(row_j.geometry): inter_area = row_i.geometry.intersection(row_j.geometry).area if inter_area > 1: # 阈值按投影单位,平方米 overlaps.append((row_i["adcode"], row_j["adcode"], inter_area)) print("重叠对数量:", len(overlaps)) # 检测缝隙:合并所有边界后与凸包对比 from shapely.ops import unary_union merged = unary_union(gdf_clean.geometry) print("合并后几何类型:", merged.geom_type)逻辑说明:make_valid是 Shapely 提供的几何修复函数,比buffer(0)更稳定,能处理自相交和重复节点。重叠检测用双重循环在数据量大时很慢,实际项目中可以用空间索引(gdf.sindex)加速。缝隙检测比较复杂,简单做法是把所有边界合并成一个大多边形,然后看合并结果是否有孔洞。如果有孔洞,说明中间有区域没被任何乡镇覆盖。
注意:拓扑修复会改变几何形状,修复前后要对比面积变化。如果某个乡镇面积变化超过 5%,需要单独检查,可能是原始几何有严重问题。
4. 避坑指南:乡镇街道边界数据处理的五个常见翻车现场
这一章集中讲我在实际项目里踩过的坑,每条按「现象 → 原因 → 解决」写。这些坑不限于浙江数据,其他省份的乡镇街道边界也适用。
4.1 中文属性乱码,名称全变成问号
现象:用 GIS 软件打开属性表,乡镇名称显示为「????」或「锟斤拷」。原因:Shapefile 的 .dbf 文件编码和读取时指定的编码不一致。老数据常用 GBK,新数据常用 UTF-8,但 .cpg 文件可能缺失或写错。解决:先用chardet检测编码,再用正确编码读取。如果 .cpg 缺失,手动创建并写入正确编码名。
import chardet # 检测 .dbf 文件编码 with open("./zhejiang_township/zhejiang_township.dbf", "rb") as f: raw = f.read(10000) result = chardet.detect(raw) print("检测编码:", result) # 用检测到的编码读取 gdf = gpd.read_file("./zhejiang_township/zhejiang_township.shp", encoding=result["encoding"])4.2 坐标系缺失,叠加底图整体偏移
现象:把边界数据叠加到在线底图上,整体偏移几百米甚至几公里。原因:数据没有 .prj 文件,或者 .prj 里定义的坐标系和实际不符。解决:先用坐标数值范围判断是地理坐标还是投影坐标,再根据数据来源推断正确的坐标系。浙江的乡镇街道数据常见坐标系有 EPSG:4326、EPSG:4490、EPSG:4547 等。如果无法确定,用已知地标点做配准。
4.3 边界之间有缝隙,空间统计出现无归属区域
现象:做人口或经济数据空间化时,发现有些区域没有被任何乡镇覆盖,统计总和对不上。原因:相邻乡镇边界在数字化时没有严格对齐,留下了细长缝隙。解决:用snap工具把相邻边界吸附到一起,或者用buffer先扩再缩。更彻底的做法是用拓扑规则检查,强制边界共享节点。
4.4 名称重复导致关联错误
现象:按乡镇名称关联统计数据,结果多个乡镇的数据串到一起。原因:不同区县有同名乡镇,名称不是唯一键。解决:一律用行政区划代码做关联主键,名称只用于展示。如果外部数据只有名称,先补全代码再关联。
4.5 数据量过大导致渲染卡顿
现象:把全省乡镇街道边界一次性加载到 GIS 里,缩放和平移卡顿严重。原因:几何节点太密,单个乡镇边界可能有上万个节点。解决:用simplify做几何简化,保留拓扑关系的前提下减少节点数。简化容差按投影单位设置,一般 10-50 米对乡镇级别展示足够。
# 几何简化,容差50米 gdf_simplified = gdf_clean.copy() gdf_simplified["geometry"] = gdf_simplified["geometry"].simplify(50, preserve_topology=True) # 对比简化前后节点数 print("简化前节点数:", sum(len(g.exterior.coords) for g in gdf_clean.geometry if g.geom_type == "Polygon")) print("简化后节点数:", sum(len(g.exterior.coords) for g in gdf_simplified.geometry if g.geom_type == "Polygon"))参数说明:preserve_topology=True保证简化后相邻边界仍然共享节点,不会产生新的缝隙。容差太大会导致边界形状失真,太小则简化效果不明显。建议按最终展示比例尺来定,1:10 万比例尺下 50 米容差基本看不出差别。
5. 从边界到分析:用乡镇街道数据做空间叠加与统计的进阶技巧
数据洗干净之后,真正的价值在于叠加分析。乡镇街道边界最常见的用法是作为统计单元,把人口、GDP、POI 等数据聚合到乡镇级别。这一章讲三个具体技巧:空间连接、分区统计、以及用边界数据做服务区分析。
5.1 空间连接:把点数据聚合到乡镇
假设你有一批门店坐标或者人口普查点,想统计每个乡镇有多少个点。用 GeoPandas 的sjoin可以一步完成。关键是确保两个数据在同一个坐标系下,否则连接结果为空。
# 读取点数据 points = gpd.read_file("./points/shops.shp") # 统一坐标系 points = points.to_crs(gdf_clean.crs) # 空间连接,点在乡镇内 joined = gpd.sjoin(points, gdf_clean, how="inner", predicate="within") # 按乡镇统计点数 count_by_town = joined.groupby("adcode").size().reset_index(name="point_count") print(count_by_town.head()) # 把统计结果合并回乡镇边界 gdf_result = gdf_clean.merge(count_by_town, on="adcode", how="left") gdf_result["point_count"] = gdf_result["point_count"].fillna(0)逻辑说明:predicate="within"表示点完全落在乡镇多边形内部。如果点恰好在边界上,可能被漏掉,可以用intersects替代,但会导致边界上的点被两个乡镇同时统计。how="left"保证没有点的乡镇也保留,计数填 0。这一步做完,每个乡镇的点的数量就挂到了边界属性表上,可以直接做专题图。
5.2 分区统计:用栅格数据算乡镇平均值
人口密度、夜间灯光、NDVI 这类栅格数据,想算每个乡镇的平均值,用rasterstats库最方便。核心是确保栅格和矢量坐标系一致,并且栅格值没有异常值干扰。
from rasterstats import zonal_stats # 分区统计,算平均值和总和 stats = zonal_stats( gdf_clean, "./raster/population.tif", stats=["mean", "sum", "max"], nodata=-9999, geojson_out=False ) # 把结果转成 DataFrame 并合并 import pandas as pd stats_df = pd.DataFrame(stats) gdf_result = pd.concat([gdf_clean.reset_index(drop=True), stats_df], axis=1) print(gdf_result[["name", "mean", "sum"]].head())参数说明:nodata=-9999指定栅格中的无效值,不指定的话无效值会被算进平均值,结果偏小。stats列表里 mean 是平均值,sum 是总和,max 是最大值。如果乡镇面积差异大,平均值和总和要结合看,避免小乡镇被大乡镇平均掉。
5.3 服务区分析:用边界数据做覆盖范围计算
门店选址或者应急设施布局,需要算每个乡镇到最近设施的距离。用乡镇质心代表乡镇,算质心到设施点的距离,再按距离分级。质心用representative_point比centroid更稳,因为凹多边形的质心可能落在边界外。
# 计算乡镇质心 gdf_clean["centroid"] = gdf_clean.geometry.representative_point() # 读取设施点 facilities = gpd.read_file("./facilities/hospitals.shp").to_crs(gdf_clean.crs) # 算每个乡镇质心到最近设施的距离 from shapely.ops import nearest_points def min_distance(point, facilities_geom): return min(point.distance(f) for f in facilities_geom) gdf_clean["nearest_facility_dist"] = gdf_clean["centroid"].apply( lambda p: min_distance(p, facilities.geometry) ) # 按距离分级 gdf_clean["dist_level"] = pd.cut( gdf_clean["nearest_facility_dist"], bins=[0, 5000, 10000, 20000, float("inf")], labels=["5km内", "5-10km", "10-20km", "20km以上"] ) print(gdf_clean["dist_level"].value_counts())逻辑说明:representative_point保证点在多边形内部,适合做距离计算的起点。nearest_points也可以用,但这里直接遍历设施点算最小距离更直观。距离单位是投影坐标系的单位,EPSG:4547 下是米。分级阈值按实际业务定,5 公里、10 公里是常见的服务半径参考。
5.4 导出与共享:把处理好的数据存成通用格式
处理完的数据要能给别人用,导出时注意格式选择和字段编码。GeoJSON 通用性好但体积大,GeoPackage 体积小且支持多图层,Shapefile 兼容性最好但有字段名长度限制。
# 导出 GeoJSON,指定编码 gdf_result.to_file("./output/township_clean.geojson", driver="GeoJSON", encoding="utf-8") # 导出 GeoPackage gdf_result.to_file("./output/township_clean.gpkg", driver="GPKG", layer="township") # 导出 Shapefile 时字段名会被截断,提前重命名 gdf_export = gdf_result.rename(columns={ "nearest_facility_dist": "nf_dist", "dist_level": "d_level" }) gdf_export.to_file("./output/township_clean.shp", encoding="utf-8")参数说明:GeoJSON 的encoding参数在部分版本中不生效,因为 GeoJSON 标准规定 UTF-8,所以导出前确保字段值已经是正确编码。Shapefile 字段名限制 10 个字符,超长会被截断,导出前重命名能避免字段丢失。GeoPackage 是目前最推荐的中间格式,单文件、支持中文、没有字段名限制。
我自己的习惯是,拿到任何一份边界数据,先花二十分钟做体检和清洗,再开始分析。这二十分钟能省掉后面几小时的排查。乡镇街道边界数据尤其如此,因为它处在省级和村级之间,数据来源杂、标准不统一,不先洗干净,后面每一步都是玄学。希望帮到你。
本文还有配套的精品资源,点击获取