简介面向Web前端开发、GIS分析与数据可视化场景的全国省级行政区划GeoJSON数据合集覆盖34个省级行政区含省、自治区、直辖市及港澳地区以省份边界几何信息为核心可直接用于地图绘制、区域统计与地理数据展示。压缩包共34个json文件整体约709KB文件按省级行政代码命名如450000.json、810000.json每个文件对应一个省级单位采用Feature Collection结构组织省界Geometry与名称、行政代码等属性字段便于程序化读取与解析。已有927人学习下载。文件可配合Leaflet、ECharts、Mapbox等前端地图库渲染也可结合turf.js、GeoPandas等工具做空间计算与投影转换为智慧城市、交通规划等场景提供基础地理参考数据。1. 先把 geojson 各省市文件下载当工程做别当资料收藏做地图大屏或区域数据分析时第一步永远是拿到一份能用的省级、市级边界 GeoJSON。很多同学在这个环节耗掉一整天从社区仓库翻到一份看似完整的 geojson 各省市文件加载进地图才发现边界锯齿明显、整体偏移几百米、属性字段全是四位数字码还不知道对应哪个市。问题不在找数据而在没搞清楚 GeoJSON 的组织方式、数据源坐标系和精度等级就急着拿来用。这篇文章把下载这事从选源、拉取、清洗到验收讲透照着跑一遍半小时内拿到一份坐标统一、层级清晰的省市边界数据集。2. 下载前先看懂 GeoJSON 结构省界为什么大多是 MultiPolygon写脚本之前先弄清楚手中的文件内部长什么样。GeoJSON 本质是一个 JSON 文本Leaflet、OpenLayers、QGIS 这类工具能直接消费它但同样叫省界数据的文件内部结构可能差很多直接决定后面批量脚本该按什么路径取值。遇到过不少同事把数据下载完就丢给前端前端盯着空白页面排查了半天最后发现是 properties 字段名跟组件期望的对不上。下载前先想清楚数据拿来干什么这决定了你选多精细的源、做不做简化。只做全国大屏展示省界级别就够做省级专题图至少要到市级边界做分析型项目比如统计各区指标区县级边界才够用。需求层级没定就下载最容易出现文件太大跑不动、精度不够看不清两头堵的情况。2.1 FeatureCollection 与 Feature一个省不是一块简单多边形一个省的边界在现实中不只是一块连续多边形。沿海省份有成片岛屿内陆省份历史上形成过飞地这些几何体拼在一起才构成完整的省级边界。所以标准组织方式是顶层声明为 FeatureCollection里面每一项是一个 Feature。每个 Feature 由 properties 和 geometry 两部分组成前者存属性信息后者存几何信息。{ type: FeatureCollection, features: [ { type: Feature, properties: { name: 北京市, adcode: 110000, level: province }, geometry: { type: MultiPolygon, coordinates: [ [ [ [116.46, 39.92], [116.48, 39.95] ] ] ] } } ] }上面这段的坐标只是示意真实文件里一个省的 coordinates 数组会塞成百上千个经纬度点。注意 geometry 的 type 是 MultiPolygon 而不是 Polygon省一级几乎不会有单 Polygon 的情况因为岛屿、飞地会让几何体变成多个多边形。写处理脚本时第一条防御逻辑就是判断 geometry[type]。def normalize_multipolygon(geometry): 把 Polygon 统一成 MultiPolygon后续只有一套处理逻辑 if geometry[type] Polygon: geometry { type: MultiPolygon, coordinates: [geometry[coordinates]], } return geometry这段转换看起来多余但能省掉大量麻烦。很多人在循环里直接写 geometry[coordinates][0] 取第一个环遇到单 Polygon 时层数是对的遇到 MultiPolygon 就报错等数据源换成另一种类型脚本又挂一次。统一成 MultiPolygon 之后简化、转坐标、拆分都能共用同一套遍历逻辑。2.2 省市区三级字段adcode 才是稳定主键处理省市区三级数据最稳定的关联键是 adcode也就是六位行政区划代码。110000 是省级码110100 是市级码110101 是区级码层级天然包含在数字里比 name 文本匹配靠谱得多。常见数据源的 properties 里通常带这几个字段| 字段 | 含义 | 样本值 | | name | 行政区名称 | 北京市 | | adcode | 六位行政区划代码 | 110000 | | level | 层级标识 | province / city / district | | children | 下级 adcode 列表 | [110101, 110102] | | center | 中心点坐标 | [116.4, 39.9] |需要注意字段命名在各数据源里不统一有的叫 citycode有的叫 geoCode。建议在读取函数里做一层兼容优先取 adcode取不到时尝试 citycode再不行按 name 回退匹配。这个兼容逻辑别散落在各个脚本里集中放在一个 read_geojson 函数里后面所有批处理都走它。def get_adcode(properties): for key in (adcode, citycode, geoCode, code): value properties.get(key) if value is not None: return int(str(value)[:6]) return None截取前六位是因为有些源会把完整的 12 位统计用行政区划码塞进来后六位是村级码。批量下载市级数据时如果源文件带 children 数组直接展开省下面的 adcode 列表就是市级的请求参数有些源不带就按省码前缀去拼比如 110000 对应的市级码以 1101 开头但这种方式有例外不建议依赖数字规律优先信任 children 字段。如果你拿到的数据源同时提供省、市、区三级独立文件目录组织上按 /province、/city、/district 分三个目录即可文件名统一用 adcode。联动逻辑很简单省文件的 children 数组给出市级 adcode市文件的 children 给出区级 adcode前端一级级请求对应文件这就是地图大屏最常见的省市区下钻实现路径。按 adcode 命名文件比按中文名命名省心得多脚本里排序、匹配、做联动都不涉及中文编码问题展示层需要中文名时从 properties.name 读取即可这个习惯在数据源频繁更新时尤其有用文件名稳定引用方代码就不用跟着改。3. 数据源选择接口、社区仓库、SHP 老数据各有各的坑GeoJSON 数据源没有唯一正确的答案只有适合当前场景的选择。接实时接口适合小批量、要最新行政区划的场景批量离线文件适合做原型和本地分析如果手头只有老 SHP 文件转换才是最省事的路径。下面把三条路线的选型和执行方式都过一遍。3.1 地图开放平台的行政区域接口可控但要注意坐标加密最可控的获取方式是走地图开放平台的行政区划接口。常见流程是在平台控制台创建应用拿 key然后按 adcode 查询下级区域返回 JSON 里带边界坐标。优点是数据会随行政区划调整更新不用自己维护版本缺点是多数平台默认返回 GCJ-02 加密坐标必须转换后才能投到 WGS84 底图而且接口一般有 QPS 限制批量拉全部省市区时会被限流。curl -s https://example.com/api/district?adcode110000key你的key上面是示意地址实际请替换成你申请的平台域名。返回内容通常是一个 JSON包含该省级单位下所有市级的 name、adcode 和边界坐标。首次使用时先在文档里确认坐标系声明有的平台提供 WGS84 参数有的只给 GCJ-02。如果只给 GCJ-02别心存侥幸直接按第 4 章的互转算法做统一处理。我的常规操作分四步走。第一步在平台控制台创建应用拿到请求 key看文档里行政区划接口的调用限额。第二步用省级 adcode 列表逐个请求把返回的市级列表和 adcode 先落盘成索引文件。第三步再按市级 adcode 逐个请求市级边界。第四步核对返回内容和坐标声明决定是否直接进清洗流程。把 34 个省级 adcode 放在配置文件里循环请求返回的市级列表合并成一张总表这是最不容易漏数据的方式。3.2 开源社区仓库与可视化组件内置数据快但要核对元信息做原型验证时优先用整理好的数据集。在开源代码托管平台搜行政区划 geojson能找到多个维护中的数据集文件普遍按 adcode 命名有的还分省、市、县三级目录下载即用。使用前必须做三件事看最近更新时间确认坐标声明读许可条款。最容易翻车的是坐标声明数据集从地图平台抓来后有些作者做了 WGS84 转换有些没做README 里却没写明加载后整体偏移排查半天才发现是坐标系问题。另一个常见来源是可视化组件库内置的静态边界数据。大屏组件一般自带全国省市边界 JSON配置里直接引用文件名就能用。好处是格式和组件完全匹配坏处是数据滞后涉及近年撤县设区等调整的边界可能还是旧版本。做内部原型可以做对外交付或严肃分析时一定要拿权威确认过的边界数据比对。提醒一句很多数据集的更新时间只看文件提交时间不代表边界数据本身更新过。有些仓库几年前的省界文件改了 README 就显示近期更新时间实际坐标完全没变。比更新时间更可靠的信号是看数据集是否随行政区划调整发布新版比如某市撤县设区后数据集是否对应更新了区级文件。接口、社区数据、组件内置数据三者怎么选要最新和可控走接口要省事走社区仓库要跟组件完全兼容走内置数据。但任何来源都要过一遍坐标系和精度检查这一步省不了。3.3 SHP 转 GeoJSON老数据救活靠一条 ogr2ogr 命令不少场景下你手里不是没有数据而是有老同事留下的 SHP。这时候没必要重新全网找 GeoJSON用 GDAL 转一下就能进现代工具链。常见做法是装一个 QGIS它自带完整 GDAL 命令行工具或者直接用 pip 安装 gdal 包在 Python 里也一样能用。ogr2ogr -f GeoJSON -t_srs EPSG:4326 -nlt PROMOTE_TO_MULTI \ -lco ENCODINGUTF-8 output.geojson input.shp-t_srs EPSG:4326 把源数据坐标投影到 WGS84。注意源 SHP 不一定是经纬度坐标很多国土、测绘来源的数据是 CGCS2000 或高斯投影平面坐标不转的话经纬度数值会错出几十万米。ENCODINGUTF-8 是给属性表 dbf 用的防止转换后中文变乱码。如果转换出来中文仍是问号多半源 dbf 实际是 GBK去掉 -lco 参数、让系统按本地编码读源文件再转一次就能对上。-nlt PROMOTE_TO_MULTI 这个参数知道的人不多但很实用它能把单 Polygon 要素自动提升为 MultiPolygon配合第 2 章的 normalize 逻辑后续统一处理不再分两种情况。这条命令处理跨文件数据时尤其省心省掉一个遍历改类型的环节。4. 用 Python 把下载、坐标统一、简化一次跑完不同的数据源只影响拉取那一步拉下来之后的处理流程是共通的坐标统一到 WGS84几何按展示层级简化最后按规范命名落盘。这一章给出一套可以直接落地的 Python 流水线下载脚本和清洗脚本分开写方便各自单独重跑。4.1 批量下载脚本重试退避、断点续传、内容校验缺一不可批量拉省市数据最大的风险不是网络慢而是平台限流和文件损坏。下载下来的文件如果只有几百字节打开一看是错误提示页浪费的不只是时间还有后续整个处理流程的信任度。我习惯把下载脚本写成幂等且带重试的已经存在的文件自动跳过这样中途断了重跑不会重复拉。import time import requests from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed ADCODES [110000, 120000, 130000, 310000] # 实际使用时补全省级 adcode 列表 BASE_URL https://example.com/api/district OUT_DIR Path(./geojson_raw) def fetch_one(adcode: str, key: str): 下载单个 adcode 的 GeoJSON已存在且非空则跳过 out_file OUT_DIR / f{adcode}.json if out_file.exists() and out_file.stat().st_size 100: return adcode, True for attempt in range(1, 4): try: resp requests.get( f{BASE_URL}?adcode{adcode}key{key}, timeout20 ) resp.raise_for_status() text resp.text.strip() if not text.startswith({) and not text.startswith([): print(f[异常] {adcode} 返回非 JSON第 {attempt} 次) continue out_file.write_text(text, encodingutf-8) return adcode, True except requests.RequestException as e: print(f[失败] {adcode} 第 {attempt} 次: {e}) time.sleep(2 ** attempt * 2) return adcode, False def main(): OUT_DIR.mkdir(parentsTrue, exist_okTrue) key 你的平台key with ThreadPoolExecutor(max_workers4) as pool: futures [pool.submit(fetch_one, code, key) for code in ADCODES] for fut in as_completed(futures): code, ok fut.result() print(f{code}: {ok if ok else failed}) if __name__ __main__: main()几个关键点说下。max_workers4 是常用的小并发量接口限流一般按 QPS 算四线程对多数平台是安全值真要拉大量区县数据再根据接口文档调高或改串行。重试等待 2 ** attempt * 2 秒是退避策略第一次失败等 4 秒第二次等 8 秒避免限流解除前反复打接口。跳过条件里 st_size 100 是防损坏文件平台返回的 403 JSON 或空 HTML 通常小于 100 字节这种文件直接删掉重拉别留给后续清洗。提示下载完成的目录建议直接对接对象存储。团队多台机器或前后端都要用这批数据时把 geojson_raw 挂到自建的 MinIO 桶里后续清洗脚本读桶内文件、写桶内文件比每台机器各存一份方便得多文件命名和目录结构与本地路径完全一致迁移成本几乎为零。4.2 坐标统一到 WGS84pyproj 转标准坐标系GCJ-02 要自己算源数据坐标系基本分三类WGS84、CGCS2000、GCJ-02。前两者都有 EPSG 编码用 pyproj 一行转换即可GCJ-02 是加密坐标没有公开的 EPSG 定义只能使用公开的偏移算法做互转。我一般先把数据源全部统一成 WGS84再往下游分发这样前端 Leaflet、后端 Shapely、桌面端 QGIS 用的都是同一套坐标。import json import pyproj from pathlib import Path from shapely.geometry import shape, mapping from shapely.ops import transform def unify_to_wgs84(in_path: Path, out_path: Path, source_crs: str EPSG:4326): 把指定坐标系的 GeoJSON 转成 WGS84EPSG:4326 data json.loads(in_path.read_text(encodingutf-8)) transformer pyproj.Transformer.from_crs(source_crs, EPSG:4326, always_xyTrue) for feature in data.get(features, []): geom shape(feature[geometry]) new_geom transform(transformer.transform, geom) feature[geometry] mapping(new_geom) out_path.write_text(json.dumps(data, ensure_asciiFalse), encodingutf-8)CGCS2000 的 EPSG 码是 4490如果你拿到的文件声明是国家大地坐标系 2000source_crs 填 EPSG:4490。转换为 EPSG:4326 时always_xyTrue 确保经纬度按 x经度、y纬度 的顺序传给变换器Python 里经常有人因为顺序写反倒置了经纬度转完才发现地图上南北颠倒。碰到 GCJ-02 数据pyproj 不认需要下面的公开偏移算法来处理import math x_pi 3.14159265358979324 * 3000.0 / 180.0 pi 3.1415926535897932384626 a 6378245.0 ee 0.00669342162296594323 def _transform_lat(x, y): ret -100.0 2.0 * x 3.0 * y 0.2 * y * y \ 0.1 * x * y 0.2 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * pi) 20.0 * math.sin(2.0 * x * pi)) * 2.0 / 3.0 ret (20.0 * math.sin(y * pi) 40.0 * math.sin(y / 3.0 * pi)) * 2.0 / 3.0 ret (160.0 * math.sin(y / 12.0 * pi) 320 * math.sin(y * pi / 30.0)) * 2.0 / 3.0 return ret def _transform_lon(x, y): ret 300.0 x 2.0 * y 0.1 * x * x \ 0.1 * x * y 0.1 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * pi) 20.0 * math.sin(2.0 * x * pi)) * 2.0 / 3.0 ret (20.0 * math.sin(x * pi) 40.0 * math.sin(x / 3.0 * pi)) * 2.0 / 3.0 ret (150.0 * math.sin(x / 12.0 * pi) 300.0 * math.sin(x / 30.0 * pi)) * 2.0 / 3.0 return ret def wgs84_to_gcj02(lng, lat): WGS84 转 GCJ-02 dlat _transform_lat(lng - 105.0, lat - 35.0) dlng _transform_lon(lng - 105.0, lat - 35.0) radlat lat / 180.0 * pi magic math.sin(radlat) magic 1 - ee * magic * magic sqrtmagic math.sqrt(magic) dlat (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi) dlng (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi) return lng dlng, lat dlat def gcj02_to_wgs84(lng, lat): GCJ-02 转 WGS84一次正向偏移反减误差约 1 米边界数据够用 lng_t, lat_t wgs84_to_gcj02(lng, lat) return lng - (lng_t - lng), lat - (lat_t - lat)gcj02_to_wgs84 不是精确逆变换而是用正向偏移量反减。城市范围内偏移量大约 300 到 600 米一次反算后剩下的误差通常是 1 到 2 米画边界图完全够用。有厘米级测量需求就改用迭代逼近或者在源头上直接找 WGS84 数据别在转换精度上反复纠结。4.3 按展示层级做几何简化tolerance 选错是锯齿和文件膨胀的根源边界坐标点是全精度存储的全国大屏根本不需要成千上万个点来画一个省级轮廓。用 Shapely 的 simplify 做 Douglas-Peucker 简化能明显压缩文件体积让前端的加载和交互都跟手。关键是 tolerance 的单位是度不是米选错了不是锯齿就是细节全丢。from shapely.geometry import shape, mapping def simplify_geojson(geojson_data: dict, tolerance: float 0.01) - dict: 按 tolerance度简化所有几何保持拓扑有效 for feature in geojson_data.get(features, []): geom shape(feature[geometry]) simplified geom.simplify(tolerance, preserve_topologyTrue) feature[geometry] mapping(simplified) return geojson_datapreserve_topologyTrue 保证简化后多边形不会自相交、不会出现碎片多边形。省略这个参数时Shapely 默认走快速算法速度更快但可能产生拓扑错误大批量数据时容易翻车输出到 QGIS 一跑有效性检查全是 self-intersection。下面的 tolerance 参考值按展示层级给| 展示范围 | tolerance度 | 效果与体积 | | 全国大屏 | 0.01 | 保留主要轮廓文件可压到百 KB 级 | | 省级详情 | 0.002 | 市级边界相对可用 | | 区县级 | 0.0005 | 保留河网、街道等相对精细的边界 |0.01 度大约对应 1.1 公里全国看省界时边界平滑够用市级要看清楚村界就太粗了得降到 0.0005 左右。实际项目里我会按层级分别处理同一个源文件跑三遍生成三套目录给前端不同缩放级别用这也是大屏应用比较稳妥的资源组织方式。5. 避坑手册下载和清洗阶段的高频问题现象原因一次讲透这一章的每一条都来自实际项目里反复出现的问题。提前说明这些问题没有一个能靠换电脑或重新下载解决根因基本固定在坐标系、编码、数据精度这三类里。排查时先对号入座能省掉几个小时的无效劳动。5.1 地图上整体偏移几百米坐标加密没处理现象用 Leaflet 加载省界边界线和底图上的行政区轮廓整体偏移偏移方向基本一致放大后有的地方偏 200 米有的地方偏 500 米。原因数据源返回的是 GCJ-02 加密坐标而你叠加的是 WGS84 底图。两个坐标系之间不是简单平移存在非线性偏移所以肉眼看到的是整体往东北偏的规律性错位。解决用 4.2 的 gcj02_to_wgs84 对每个顶点做反算再落盘。转完以后抽三个城市点跟底图真实位置对比确认误差在可接受范围。数据量大时先转完省级文件验证一次再批处理全部市区县别一口气全转完再回来查。5.2 下载的文件只有几百字节错误提示被当成了正常数据现象批量脚本跑完日志显示全部成功但 QGIS 加载后地图区空白。打开文件一看内容是{code:403,success:false}之类的错误 JSON 或一整个 HTML 错误页。原因平台限流、key 过期或参数格式不对返回的是错误响应但脚本只检查了 HTTP 状态码 200没检查内容本身是不是合法 GeoJSON于是把错误页当正常文件写盘。解决在写盘前加一层内容校验检查文本去空格后的首字符是不是{或[同时校验文件大小下限。4.1 的脚本已经带上这条逻辑如果你有自己的下载脚本务必补上这一段别让脏数据流进下一环节。5.3 放大到市界后锯齿严重精度等级和简化参数不匹配现象省级视角看边界没问题放大到某个市之后海岸线和界河边界呈明显锯齿状甚至出现直角折线。原因数据源本身是低精度简模或预处理时 tolerance 设在 0.01 级别把中尺度的弯折都削平了。解决先区分是源数据粗还是简化过度。把原始文件直接拖进 QGIS 看一遍原始文件就锯齿就换更精细的数据源原始文件平滑、处理后锯齿把 tolerance 从 0.01 降到 0.002 重新生成。不同数据源精度差异很大拿到新文件先做一次原始精度抽查再定简化参数。5.4 属性表中文乱码SHP 转换和 JSON 读取的编码双坑现象QGIS 打开转换后的 GeoJSONname 列全是鍖椾含之类的乱码或 Python 读入时直接抛 UnicodeDecodeError。原因SHP 的属性文件 dbf 用 GBK 编码存储转换时没指定 UTF-8GDAL 按系统默认编码读成了错误映射。反过来Python 读取时如果源文件是 GBK你用 UTF-8 硬读一样乱码。解决ogr2ogr 转换时带 -lco ENCODINGUTF-8转换结果仍乱码就去掉该参数重转让 GDAL 按本地系统编码读取源文件再输出。Python 读取时先向数据源确认编码再在 open 里显式指定不要依赖系统默认行为。5.5 省级文件膨胀到几十 MB子级边界全部内嵌了现象单个省级 GeoJSON 文件 30MB网页加载明显卡顿前端组件直接假死。打开内容看省界 Feature 里塞了完整的地市级甚至区县级边界。原因部分数据源在一个省级文件里内嵌了全级别边界省界只是最外层轮廓文件体积被子级坐标撑大。解决按使用场景拆分。只需要省界时用 Shapely 把 coordinates 里子级部分剥离仅保留省级外环需要省市区联动时另写拆分脚本按 adcode 把每个下级区域写成独立文件。拆完再走一遍简化单文件体积能压到原来的十分之一。6. 数据到手先别急着接前端在 QGIS 里做十分钟几何体检清洗完的 GeoJSON不要直接就丢给前端组件。先在 QGIS 里做一轮几何体检发现的问题数量级通常超过预期尤其是来自社区仓库的数据。体检三个动作检查有效性、叠加底图、修复问题。打开 QGIS用 Layer 菜单添加矢量图层选中你的 GeoJSON然后在 Vector 菜单的 Geometry Tools 里选 Check Validity运行后会生成错误结果图层列出所有 self-intersection、empty geometry、duplicate nodes。双击错误行地图会定位到出错位置能直观看到问题要素长什么样。对省级边界来说最常见的错误是自相交多半来自原始数据转换时顶点顺序错乱。修复时不要用简化掩盖问题。QGIS 的 Fix geometries 工具会把自相交多边形修复成有效几何但会改动节点数量修复完务必再跑一次 Check Validity 确认没有引入新错误。对大量区县边界我更习惯回到脚本层用 Shapely 的 buffer(0) 统一清理它对无效几何做零距离缓冲重建能一次处理完整个文件。体检第二项是叠加底图目检。QGIS 默认的 OpenStreetMap 底图走 WGS84把省界文件叠上去肉眼扫一遍重点看沿海省份的海岸线、有飞地的省份、三省交界处。整体往一个方向偏移是坐标系问题局部贴不上多半是数据精度或行政区划调整导致边界漂移需要人工判断是否更换数据源版本。这个流程十分钟能走完却拦得住大部分返工。最后一个习惯体检通过的成品内部归档我存成 GeoPackageGPKG而非 GeoJSON。GeoJSON 是文本格式经纬度以浮点字符串保存反复读写存在精度损耗GPKG 是二进制坐标和字段类型保存更完整。对外分发再导出 GeoJSON内部存量和空间分析用 GPKG下载的边界坐标和上次比对差了 5 米的坑就不会出现。我踩过最重的一次是把加密坐标的市界直接投到 WGS84 底图整个项目的方案汇报全用了偏移 400 米的地图从那以后拿到任何 GeoJSON 先查坐标声明没声明就先抽两个点验证再批量处理这套习惯把后面所有玄学偏移的排查时间都省掉了。希望帮到你。本文还有配套的精品资源点击获取