简介包含全国乡镇地图 JSON 数据的压缩包以 1997 个 JSON 文件为主体另附 3 个 TXT 说明文件总文件数 2000 个、包体约 553.27MB。数据覆盖中国众多乡镇/街道的行政边界、经纬度坐标、行政区划等地理信息适用于 GIS 开发、地图应用、城市规划与人口分布分析等场景。开发者可利用 Python json、geopandas 或前端 JavaScript 等工具解析这些 JSON结合街道级边界数据完成可视化展示、交通路径规划或灾害风险评估。压缩包中长寿区、广州、环县等多个市县级行政边界文件独立成篇便于按区域提取使用TXT 文件通常包含数据说明或字段注释有助于快速理解结构。目前已吸引 510 人学习/下载适合需要获取全国乡镇级矢量边界数据的中高级 GIS 开发者、数据分析师及地图服务研究者可作为构建底图、区域统计或地图展示的基础数据源。1. 乡镇级边界 JSON能渲染、能关联业务才是它真正的价值做乡镇级地图可视化时最先卡住你的往往不是图表引擎而是缺一份能落地的乡镇边界 JSON。省市级边界数据不难找到乡镇这一级就成了稀缺品这份“全国乡镇地图 JSON.zip”把乡镇级行政区划边界收进一个包里解压后拿到的是 GeoJSON 结构的 JSON 数据。它能解决什么把人口、营收、客流这类业务指标落到乡镇面状区域做区域分析、选址评估、网格化管理。适合谁前端要把边界渲染上屏的、后端要批量解析做关联的、做数据处理要对齐行政区划编码的。要提醒的是这里的核心是 JSON拿到手不能直接看图得自己解码、渲染、做业务映射这是它和下好的图片资源最大的区别。2. 解压后先看 JSON 结构FeatureCollection、12 位乡镇编码与坐标系风险2.1 zip 包里的文件组织与字段清单先别急着把文件拖进地图库第一步是把包解开看看里面到底是单个全量文件还是按区域拆成了多个 JSON。我一般这么起手unzip 全国乡镇地图json.zip -d ./town_json find ./town_json -type f -name *.json | head -20unzip后面的-d指定解压目录避免把几十个文件直接撒在当前目录find配合head -20只是先看前 20 个文件确认文件命名规律和数量。如果全是类似all.json、towns.geojson这样的名字说明是单文件方案如果按区县代码命名那后面做按需加载会省力很多。打开任意一个 JSON结构基本逃不出 GeoJSON 的固定格式也就是一层FeatureCollection包着一个features数组里面每个元素对应一个乡镇。典型片段长这样{ type: FeatureCollection, features: [ { type: Feature, properties: { name: 某镇, code: 123456789012, level: town }, geometry: { type: MultiPolygon, coordinates: [] } } ] }properties里最值得关注的是code即乡镇级行政区划代码。它一般是 12 位数字前 6 位是区县级代码9 位是乡镇街道级代码。字段里常常还会带name、level之类信息但name只能用来展示不能用来当关联主键后面会单独讲重名问题。geometry的类型通常是Polygon或MultiPolygon。MultiPolygon的坐标嵌套层数比Polygon多一层处理时要先用type判断再说。很多新手直接写三层 for 循环去取值遇到MultiPolygon就少取了一层边界直接画成乱线。建议拿到文件后先用脚本统一点检一遍而不是依赖肉眼抽查。除了看结构还要清点数据覆盖度。乡镇一级的边界顶点非常密集一个区的 JSON 可能就有几十 MB全量全国数据体积会相当可观。如果你做的是 Web 页面直接把这个包塞给浏览器不是不行但首屏耗时和内存占用都会很难看我一般会在后面做切片和简化。2.2 坐标系风险先做坐标探针再决定是否转换国内的地理数据有一个绕不开的问题坐标系。常见的是 GCJ-02加密偏移后的火星坐标系和 WGS84GPS 原始经纬度混用。两份数据底子不同直接叠在一起画面就会整体朝东北方向漂移几百米并且越往高纬度越明显。乡镇级 JSON 到底用的是哪套坐标最不建议的做法是“猜”。最稳妥的是抽几个边界点和已知地图底图交叉比对。但这里有个更快的探针方法先把所有坐标的经纬度范围打印出来看数值是否落在正常区间内。import zipfile import json with zipfile.ZipFile(全国乡镇地图json.zip) as zf: json_files [n for n in zf.namelist() if n.endswith(.json)] first_file json_files[0] with zf.open(first_file) as fp: data json.load(fp) points [] for feat in data[features]: geom feat[geometry] if geom[type] MultiPolygon: polygons geom[coordinates] else: polygons [geom[coordinates]] for poly in polygons: for ring in poly: points.extend(ring[:5]) lons [p[0] for p in points] lats [p[1] for p in points] print(json文件, first_file) print(经度范围, min(lons), max(lons)) print(纬度范围, min(lats), max(lats))这段脚本用 zipfile 直接读取压缩包里的第一个 JSON绕开了“必须先解压全部文件”的笨办法。注意ring[:5]的处理每个封闭环只取前 5 个点做抽样即可全国乡镇数量很大全量遍历所有点会平白消耗内存。如果打印结果是经度 73 到 135、纬度 3 到 53 左右说明坐标基本是未经过投影的经纬度可以做下一步如果经度出现 3857 这种大数值或者纬度超过 90说明数据不是经纬度表达而是投影坐标得先做投影逆变换才能用于地图叠加。第二步是抽两三个乡镇边界上的点放进地图底图里目测是否贴合若整体偏离那就要批量做 GCJ-02 与 WGS84 的互转。这个转换脚本很多算法库都有现成实现核心思路是对每个坐标点做偏移量修正参数是固定的偏移模型。2.3 properties 缺 center 时怎么补别用环首点当中心很多乡镇 JSON 里只有name和code没有中心点坐标。做标注、做聚合、做选中态浮动时没有 center 会很麻烦。常见误用是直接拿coordinates数组里的第一个点当中心——那是边界顶点经常落在角落甚至飞地外面标注文字会跑到地图外。正确做法是用几何库计算几何中心也就是质心。我习惯在解析阶段直接补进properties里from shapely.geometry import shape import json with open(towns.json, encodingutf-8) as f: data json.load(f) for feat in data[features]: geom shape(feat[geometry]) feat[properties][center] [geom.centroid.x, geom.centroid.y] feat[properties][area] round(geom.area, 4) with open(towns_with_center.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse)shape()是把 GeoJSON 的 geometry 转成真正可计算的几何对象之后.centroid返回质心.area返回面积。这里有一个隐藏参数问题如果坐标是经纬度.area算出来的是“平方度”不是平方米或平方公里直接拿去做业务报表会出错。要得到真实面积得先把坐标投影成平面坐标系再算比如用 Web Mercator 或所在区域的 UTM 投影。我一般还会顺手把name和code生成一份映射字典方便后续关联。这一步做完这份乡镇 JSON 才真正从“能看”变成“能用”。3. 上手渲染乡镇边界地图库加载、样式与数据关联3.1 把 GeoJSON 喂给地图库前先做的三件事渲染之前有三个检查项缺一个都容易在运行时翻车。第一顶层必须是FeatureCollection。有些文件导出成了单个Feature或者GeometryCollection地图库只认前者就要先包一层。第二每个 feature 的geometry不能为空。乡镇数据里有时会混入一些“占位点”geometry 是 null渲染时地图库直接抛异常。第三坐标环要闭合。GeoJSON 规范要求外环首尾坐标一致但有的数据源导出时把最后一个点省掉了地图绘制时虽然大部分库能容忍但会留下一个肉眼不易察觉的缺口。用一个 Python 脚本做统一体检import json def check_geojson(path): with open(path, encodingutf-8) as f: data json.load(f) assert data.get(type) FeatureCollection, 顶层不是FeatureCollection problems [] for idx, feat in enumerate(data[features]): props feat.get(properties, {}) name props.get(name, ffeature_{idx}) code props.get(code, 无编码) geom feat.get(geometry) if geom is None: problems.append(f{name}/{code}: geometry为空) continue if geom[type] not in (Polygon, MultiPolygon): problems.append(f{name}/{code}: 类型不支持) ring geom[coordinates][0][0] if geom[type] Polygon else geom[coordinates][0][0][0] if ring[0] ! ring[-1]: problems.append(f{name}/{code}: 环未闭合) return problems problems check_geojson(towns.json) for p in problems[:20]: print(p)这段把常见问题一次性暴露出来。注意ring[0] ! ring[-1]的判断GeoJSON 的坐标是[经度, 纬度]数组直接比较两个数组即可。对已闭合的环这个检查不会误报。检查通过后再去接地图库。很多渲染问题其实都不是地图库的锅而是 JSON 本身不干净。3.2 渲染成乡镇色块图拿到干净的乡镇级 GeoJSON 后下一步是上屏。以主流地图渲染库为例大致的流程是先注册地图数据再把业务数据组装成name/value结构的数组最后配置色阶。伪代码如下async function loadAndRender(geoUrl, bizData) { const geoJson await (await fetch(geoUrl)).json(); // 把乡镇边界注册到地图库第二个参数就是GeoJSON对象 registerMap(towns, geoJson); chart.setOption({ series: [{ type: map, map: towns, label: { show: false }, data: bizData.map(row ({ name: row.name, value: row.metric })) }], visualMap: { min: 0, max: 100, inRange: { color: [#e8f5e9, #66bb6a, #2e7d32] } } }); }registerMap(towns, geoJson)把乡镇边界注册为一张名称为towns的自定义地图后续series里只要map: towns就能引用。bizData是业务侧数据渲染时地图库会拿row.name去 GeoJSON 的properties.name里找对应的乡镇找到后就把row.metric映射到视觉颜色上。这里有两个容易踩的点。第一名称匹配是渲染库的默认逻辑一旦业务数据里的乡镇名称和 JSON 里的name不一致这个乡镇就显示成空白。所以我在上面特意强调用code关联如果你用的地图库支持指定nameProperty就把字段改成code最稳。第二visualMap的max不要写死。如果指标最大值是 3000你的max写成 100那超过 100 的乡镇全部渲染成最深色地图看起来就是一块大黑斑。合理做法是先从业务数据里算出最大最小值再动态写入。3.3 关联业务数据用 12 位 code 而不是中文名业务表里最常见的字段是“所属乡镇”“所在区域”存中文名的有存编码的也有。中文名能看不能用因为乡镇重名率极高。就算在同一省份也可能出现多个“城关镇”光靠名称做关联结果必然脏。正确姿势是把业务数据的乡镇编码和 GeoJSON 的code对齐再回填中文名import pandas as pd import json with open(towns.json, encodingutf-8) as f: geojson json.load(f) code_to_name { feat[properties][code]: feat[properties][name] for feat in geojson[features] if feat[properties].get(code) } df pd.read_csv(biz_data.csv) df[town_code] df[town_code].astype(str).str.zfill(12) df[matched] df[town_code].isin(code_to_name) matched df[df[matched]] unmatched df[~df[matched]] print(匹配成功, len(matched), 未匹配, len(unmatched))str.zfill(12)是为了把“123456789012”这种以字符串形式存储的编码统一长度因为很多业务表导入时会把前导 0 丢掉。如果编码是 11 位zfill(12)补成 12 位才能和 GeoJSON 里的 code 对齐。unmatched数据要单独导出来检查可能是编码规则不一致也可能是业务数据覆盖到了 GeoJSON 里没有的地区这两种情况处理方式完全不同。匹配完成后再把code_to_name回填到df最后输出给渲染层用编码做主键、中文名做展示既稳定又直观。4. 乡镇 JSON 的避坑与排查漂移、孔洞、卡顿、重名匹配4.1 整张图向东北方向漂移几百米现象乡镇边界和河流、道路叠加时边界整体往右上角偏移面积越大偏差越明显。原因这份乡镇 JSON 用的是 GCJ-02 坐标系而底图数据用的是 WGS84或者反过来。GCJ-02 是国内地图产品常用的加密偏移坐标系在 WGS84 基础上做了非线性偏移偏移量在几百米级别肉眼极容易发现但程序难以通过简单加减修正。解决先判断 JSON 用的是哪套坐标系再批量转换。转换接口在常见坐标系转换库中都有实现本质是对经纬度做一次偏移修正。转换前把原始文件备份好转换后再次用上一章提到的探针脚本比对边界确认贴合后再进入下一步。注意转换是逐个点做的全国乡镇级数据点量很大转换过程会比较耗时建议只做一次并把结果单独存为一个新文件。4.2 简化后乡镇边界出现毛刺和穿洞现象对 GeoJSON 做压缩简化后某些乡镇边界出现锯齿甚至相邻两个乡镇之间出现白色缝隙像地图“穿了洞”。原因简化算法默认会用直线代替曲线当简化比例过大时两个相邻面的共享边界会各自独立优化导致两边不再完全重合。另一个常见原因是简化时没有保留拓扑关系删除顶点后本该相连的边界被截断了。解决简化时显式开启拓扑保持参数。用几何处理库做简化时preserve_topologyTrue要强制打开它能保证相邻面的公共边保持一致。简化比例上乡镇级数据我一般保留 10% 到 15% 的顶点不能再低了。比例过低会让地图在放大后完全失去参考价值。简化完成后再执行一次“相邻面边界重叠检测”如果发现缝隙可以用很小的缓冲区把相邻面做一次融合处理。4.3 全量 JSON 一次加载页面直接卡死现象浏览器地址栏输入页面地址后地图区域内一片空白控制台报内存溢出或渲染线程卡死有的机器直接标签页崩溃。原因全量全国乡镇 JSON 数据体积很大浏览器要一次性解析几万到几十万级的多边形顶点几何解析、坐标转换、Canvas 绘制同时进行内存和 CPU 双双爆掉。解决不要在前端加载全量数据。常见做法有两种一种是后端按区县切分 JSON 文件前端地图缩放或平移时按需请求另一种是把乡镇级 GeoJSON 转换为 TopoJSON 格式利用拓扑共享边把体积压缩百分之六十到八十。如果项目紧急最简单的处理是先把数据按省级或区县级拆包首屏只加载当前视图范围内的文件缩放级别小于一定阈值时甚至可以先显示市级边界做降级。4.4 乡镇重名多按中文名关联全是脏数据现象业务数据“所属乡镇”填的是“城关镇”和 GeoJSON 里的多个“城关镇”全部匹配上一张地图最多有十来个同名乡镇被同时高亮。原因乡镇名称在全国范围内重复率极高“城关镇”“中心镇”“街道”这类名字一个省能出现好多次。按中文名做关联本质是拿非唯一键做主键结果必然混乱。解决一律用 12 位行政区划编码关联。业务表如果没有现成的乡镇编码字段就先从管理后台或档案系统里补列补不了就用“乡镇名称 上级区县名称”拼接成复合键再把 GeoJSON 里的code[:6]前缀和区县名称绑定做二次匹配。切忌直接写一条where name 城关镇然后拍脑袋上线。5. 面向业务的加工流按县切分、编码回填、精简体积5.1 按 6 位区县前缀切分子文件真实业务场景里前端和数据分析平台都很少需要一次性拉全量乡镇。更多的需求是“给我某个区县的乡镇边界”。与其让后端每次去大文件里过滤不如在生产环节就把数据切好。import json import os from collections import defaultdict def split_towns_by_county(src_path, out_dir): os.makedirs(out_dir, exist_okTrue) groups defaultdict(list) with open(src_path, encodingutf-8) as f: data json.load(f) for feat in data[features]: code feat[properties][code] groups[code[:6]].append(feat) for county_code, features in groups.items(): fc { type: FeatureCollection, features: features } out_path os.path.join(out_dir, f{county_code}.json) with open(out_path, w, encodingutf-8) as f: json.dump(fc, f, ensure_asciiFalse) return list(groups.keys()) county_codes split_towns_by_county(towns.json, ./towns_by_county) print(切分区县数, len(county_codes))这个脚本的核心是code[:6]也就是把乡镇级 12 位编码截取前 6 位得到区县级编码。区县下面的所有乡镇 feature 会被聚合到同一个分组里然后分别写文件。ensure_asciiFalse特别重要不然中文乡镇名会被转成\uXXXX形式文件体积变大不说排查问题也很痛苦。切分之后前端要做的事就简单了地图移动到哪个区县就请求对应的123456.json文件加载量从全量几百 MB 降到单文件几 MB首屏压力小一个量级。5.2 补 center、area 与行政名称字典切分完还要补业务字段。上一章在单个文件里补 center现在要做的是批量补全并生成可复用的索引字典。import json from shapely.geometry import shape def enrich_features(features): for feat in features: props feat[properties] geom shape(feat[geometry]) props[center] [round(geom.centroid.x, 6), round(geom.centroid.y, 6)] props[area] round(geom.area, 6) return features index {} for root, _, files in os.walk(./towns_by_county): for fp in files: if not fp.endswith(.json): continue with open(os.path.join(root, fp), encodingutf-8) as f: fc json.load(f) fc[features] enrich_features(fc[features]) for feat in fc[features]: code feat[properties][code] index[code] { name: feat[properties][name], county: fp[:6], center: feat[properties][center] } with open(os.path.join(root, fp), w, encodingutf-8) as f: json.dump(fc, f, ensure_asciiFalse) with open(town_index.json, w, encodingutf-8) as f: json.dump(index, f, ensure_asciiFalse)center保留 6 位小数大约对应 0.1 米精度对展示类业务完全够用还能控制文件体积。area在经纬度坐标系下是平方度如果业务要真实面积这里要额外做投影转换或者直接用行政区划统计年鉴的面积数据替代。town_index.json生成后后端接口可以直接查这个字典不用再每次加载全量 GeoJSON。前端点击某个乡镇时也能从 index 里快速取到中心点和名称连地图引擎的渲染查询都省了。5.3 浮点精度压缩与体积控制乡镇边界数据大除了顶点数量多还有一个隐藏因素坐标小数位数太多。很多数据导出时坐标保留了 12 位小数实际对地图显示毫无意义反而让 JSON 体积翻倍。def trim_geometry(geometry, keep6): if geometry[type] Polygon: return { type: Polygon, coordinates: [ [[round(x, keep), round(y, keep)] for x, y in ring] for ring in geometry[coordinates] ] } if geometry[type] MultiPolygon: return { type: MultiPolygon, coordinates: [ [ [[round(x, keep), round(y, keep)] for x, y in point] for point in ring ] for ring in geometry[coordinates] ] } return geometry with open(towns.json, encodingutf-8) as f: data json.load(f) for feat in data[features]: feat[geometry] trim_geometry(feat[geometry], keep6) with open(towns_compressed.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse)keep6表示经纬度保留 6 位小数约等于 0.1 米精度。如果只用做全国范围概览可以保留 4 位大约 10 米精度体积还能再缩一截。MultiPolygon的嵌套层级比Polygon多一层压缩函数里对这两种类型分别处理嵌套层级不能搞混。经过这一步再加上切分乡镇 JSON 在生产环境里的加载和传输压力基本就解决了。6. 上线前的自检技巧几何自检、边界锚点和缓存策略6.1 几何自检闭合环、合法性与边界锚点发布上线前我习惯再跑一遍几何自检。上一章检查过闭合环这里重点是拓扑合法性和边界锚点。from shapely.geometry import shape from shapely.validation import explain_validity with open(towns.json, encodingutf-8) as f: data json.load(f) invalid [] for feat in data[features]: geom shape(feat[geometry]) code feat[properties][code] if not geom.is_valid: invalid.append((code, explain_validity(geom))) print(无效乡镇数量, len(invalid)) for item in invalid[:10]: print(item)is_valid是几何对象的合法性检查explain_validity会返回具体原因比如“Self-intersection”“Ring Self-intersection”这类描述。乡镇边界经常因为拓扑接边问题出现自相交这种数据在底图上看不出问题但做空间计算时结果完全不可信。边界锚点我一般会抽两到三个县人工核对它们的相邻边界是否严格重合。操作方式是读取两个县边界计算公共边长度如果公共边为 0说明两个县之间存在缝隙或重叠需要回到源数据处理。锚点核对是自动化验证之外的最后一道人力防线。6.2 缓存、GZip 与增量更新自检通过后还有一件事不能省缓存策略。GeoJSON 是纯文本格式GZip 压缩率非常高生产环境部署时一定要开启 GZip 压缩通常压缩后体积能降到原来的百分之二十以下。另外不要直接拿原始 zip 包发布接口。我见过有同学图省事把 zip 解压后整个挂到静态资源服务器上结果每次业务更新前端都要拉全量数据。更合理的是用切分后的towns_by_county目录做静态资源文件名里带上区县编码和版本号例如123456_v3.json后端只把版本号写进接口返回前端根据版本号决定是否刷新缓存。行政边界会随着区划调整不定期变化每次拿到新数据先跑一遍“编码变化对比”把新旧两份 JSON 的code列表做差集找出新增、失效和代码变更的乡镇。这个差集结果同步给业务系统前端才不会在区划调整后出现“地图上有边界、数据表里没编码”的错位。那次做某跨平台系统的乡镇图层时我跳过了边界锚点核验结果两个相邻乡镇的边界在业务侧出现一条细缝排查了一整天才发现是源数据拓扑问题。从那以后我每次处理乡镇级 JSON 都强制走一遍坐标探针、几何自检、按县切分发布三步流程。希望帮到你。本文还有配套的精品资源点击获取