简介:这是一份覆盖中国全部省级行政区划的GeoJSON数据集合,专为Web地图开发、GIS分析与前端可视化场景设计。资源内含34个JSON文件,对应全国34个省级单位,以Feature Collection形式组织,每个文件包含对应省份的边界几何信息及名称、行政代码等属性,可直接在JavaScript中调用,也兼容turf.js、geopandas等常见地理处理工具。GeoJSON本身轻量易解析,支持Polygon、MultiPolygon等几何类型,可直接用于浏览器渲染或服务端空间计算。压缩包整体约709KB,结构清晰,适合需要快速获取省界数据、进行空间统计或构建全国地图应用的开发者使用。目前已有927人学习下载,对于需要省市级地理边界数据、又不想自行从开放平台逐个爬取的用户,这份打包好的数据能显著节省预处理时间;也可作为基础底图,与人口、交通、环境等专题数据叠加,服务于智慧城市、区域规划等场景。
1. 各省市 GeoJSON 文件下载:数据齐了,地图才不翻车
做前端数据可视化或 GIS 分析时,第一步往往不是写代码画图,而是找一份能用的省市边界 GeoJSON。这个需求听着简单,真动手才发现:有的数据源边界是旧的,有的坐标系不是 WGS84,有的文件下载下来只有几 KB,一解析就报错。我最早做省级地图时也以为下载几个 json 就行,结果前半天全耗在修数据上。这篇笔记就围绕“geojson 各省市文件下载”这条主线,讲清楚数据从哪来、怎么批量落地、下载后怎么校验和排错。适合用 ECharts、Leaflet 做行政区划可视化,以及需要把 SHP 转 GeoJSON 的 GIS 从业者照着做。
2. 从数据源到本地:先选对 GeoJSON 下载渠道
找数据源时,我一般分两步:先看有没有现成 GeoJSON,没有再看 SHP 转换。现成 GeoJSON 里,最常用的是阿里 DataV 的开放接口;而国土、规划口径的数据往往只发 SHP,就需要 GDAL 转一手。无论哪条路,下载后都要做一次文件体检,避免把坏数据直接喂给前端。
2.1 阿里 DataV GeoJSON 数据源:最省事的省市边界
阿里 DataV 的 GeoJSON 接口没有鉴权、返回标准 FeatureCollection,很多开源 ECharts 地图示例都在用。它的 URL 规则很简单,按 6 位行政区划代码(adcode)组织:
- 全国:
https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json - 省级:
https://geo.datav.aliyun.com/areas_v3/bound/310000_full.json - 市级:
https://geo.datav.aliyun.com/areas_v3/bound/310100_full.json
这里的full表示带几何边界的完整文件;不带full的同路径 json 只有子区域列表,没有几何。下载单个文件用 curl 就够了:
# 下载全省边界,-L 跟随跳转,--fail 让 404 不落盘 curl -L --fail --retry 3 \ -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \ -o 100000_full.json \ https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json # 确认文件不是错误页 ls -lh 100000_full.json head -c 200 100000_full.json逻辑说明:-A带浏览器 UA,避免被源站反爬策略拦掉;head -c 200看前 200 字节,如果看到{"type"说明是 JSON,如果看到<html>说明被重定向到了错误页。--retry 3在网络抖动时自动重试,但注意它不会对 404 重试,所以还要配合--fail。
选择这个渠道的理由是省事、层级全、直接给 WGS84 坐标。但它也有明显边界:数据精度最高到县级,乡镇街道边界经常缺失;行政区划调整后更新有滞后。如果项目只需要省市级展示,它可以作为首选;要精确到乡镇街道,就得换天地图开放服务或地方测绘部门数据。
2.2 从 SHP 转 GeoJSON:当数据源只有 Shapefile 时的处理
很多规划、国土部门只发布 SHP,这也是“shp 文件下载”在检索里一直很热的原因。SHP 是二进制多文件格式,浏览器没法直接用,需要转成 GeoJSON。常见做法是装 GDAL,用ogr2ogr一行完成转换。注意 SHP 至少包含.shp、.dbf、.shx三个文件,命令里写主文件名就行:
# 基础转换,统一到 WGS84 坐标 ogr2ogr -f GeoJSON \ -t_srs EPSG:4326 \ -lco COORDINATE_PRECISION=6 \ output.geojson input.shp # 属性中文乱码时,强制按 GBK 读取 dbf ogr2ogr -f GeoJSON \ -t_srs EPSG:4326 \ --config SHAPE_ENCODING GBK \ -lco COORDINATE_PRECISION=6 \ output.geojson input.shp逻辑说明:-t_srs EPSG:4326把源坐标系转到经纬度,很多国内测绘数据是 CGCS2000 或高斯投影,不转直接出图会偏移几百米到几公里。COORDINATE_PRECISION=6保留 6 位小数,约 0.1 米精度,文件体积能缩小一半以上。第二个命令里的--config SHAPE_ENCODING GBK是关键,SHP 的 dbf 属性表常用 GBK 编码,不指定的话转换后的中文属性全是乱码。
参数说明:源坐标系未知时,先用ogrinfo -so -al input.shp查看字段块里的坐标信息。如果显示的是PROJCS或LOCAL_CS,就要向数据提供方确认坐标系代码,不能瞎转。转换后我会再用ogrinfo -al -so output.geojson统计要素数,和源文件对比,防止漏要素。
2.3 下载后的文件校验:看一眼 JSON 结构和坐标系
不管从哪个渠道拿到文件,先做 30 秒体检。我用 Python 检查type、features数量、几何类型和属性字段:
import json def check_geojson(path): with open(path, "r", encoding="utf-8") as f: data = json.load(f) assert data["type"] == "FeatureCollection", "type 不是 FeatureCollection" feats = data["features"] print("要素数:", len(feats)) geoms = {f["geometry"]["type"] for f in feats if f.get("geometry")} print("几何类型:", geoms) props = feats[0]["properties"] print("属性字段:", list(props.keys()) if isinstance(props, dict) else props) print("bbox:", data.get("bbox", "无"))逻辑说明:先断言最外层类型,再统计几何类型集合。如果出现GeometryCollection、LineString,说明这不是纯面边界数据,前端渲染要多写分支。打印属性字段是为了看清省份名称存在哪个键里,后面做下钻和匹配要用。
参数说明:这是一个快速脚本,严谨做法还要遍历所有 feature 检查geometry是否为空。县级数据里空几何比较多,比例超过 5% 就要考虑换数据源。判断坐标系有一个土办法:如果经纬度在 60~140、10~55 之间,基本是 WGS84 或 GCJ02 这类经纬度坐标;如果坐标变成几十万的大数,那是投影坐标系,必须用ogr2ogr -t_srs EPSG:4326转回。
3. 用脚本批量下载省市县三级边界:Python 实现与参数说明
单个文件能下载只是开始,真正要落地的是批量拉全国省市数据。这里我一般分三层做:先写一个不依赖外部库的最小下载函数,再套线程池和重试,最后用 manifest 文件记录每个文件的哈希和来源。
3.1 单文件下载的最小脚本:requests 与超时重试
import requests from pathlib import Path def download_geojson(adcode, out_dir="data", timeout=(5, 20)): out_dir = Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) url = f"https://geo.datav.aliyun.com/areas_v3/bound/{adcode}_full.json" target = out_dir / f"{adcode}_full.json" headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"} # stream=True 让响应体按块写入,避免大文件撑爆内存 with requests.get(url, headers=headers, timeout=timeout, stream=True) as r: r.raise_for_status() with open(target, "wb") as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk) print(f"{adcode} -> {target} ({target.stat().st_size} bytes)")逻辑说明:stream=True配合iter_content按 8KB 块写盘,下载中断时不会把半份文件读到内存里才报错。timeout=(5, 20)表示连接超时 5 秒、读取超时 20 秒;只填一个数字是整体超时,对慢网络不够精确。raise_for_status()在 HTTP 4xx/5xx 时直接抛异常,避免把 404 错误页当成 JSON 存下来。
参数说明:adcode是 6 位行政区划代码,out_dir会自动创建。如果源站对无 UA 的请求返回 403,统一加浏览器 UA 即可。不需要动verify=False,除非你明确知道是内网证书问题。
3.2 批量下载全国省市数据:并发、限速与断点续传
import json import time import concurrent.futures as cf import requests def get_children(adcode="100000"): # 不带 full 的 json 返回子区域列表,字段里有 adcode r = requests.get( f"https://geo.datav.aliyun.com/areas_v3/bound/{adcode}.json", timeout=10 ) r.raise_for_status() data = r.json() return [item["adcode"] for item in data if "adcode" in item] def safe_download(adcode, retries=3): for i in range(retries): try: download_geojson(adcode, out_dir="data") return adcode, True except Exception as e: print(adcode, "重试", i + 1, e) time.sleep(2 ** i) # 指数退避 2s/4s/8s return adcode, False adcodes = get_children("100000") # 全国 34 个省级 adcode with cf.ThreadPoolExecutor(max_workers=4) as pool: results = list(pool.map(safe_download, adcodes)) failed = [r for r in results if not r[1]] print("失败列表:", failed)逻辑说明:get_children先取省级列表,拿到 34 个 adcode。safe_download包了一层重试,异常时按 2 的指数退避等待,避免失败后立即重试再次被打回。ThreadPoolExecutor(max_workers=4)限住并发数,对源站友好,也避免大量小文件同时写盘时磁盘 IO 争抢。
参数说明:max_workers我一般用 4~6;如果目标是市县一级,总文件数上千,可以提到 8,但一定要配合限速。GeoJSON 单文件大部分在几百 KB 到几 MB,瓶颈通常不在带宽而在源站限流策略。真要较真,先单线程下载一个几十 MB 的文件测速,再决定并发数。断点续传这里没有做,因为阿里 DataV 不保证支持 Range 请求,失败后整体重下最省事;只有当你确认源站响应头里有Accept-Ranges: bytes时,才有必要实现分块续传。
3.3 文件落地后的目录组织与命名规范
import json import hashlib from pathlib import Path from datetime import datetime def build_manifest(source_dir="data"): records = [] for path in sorted(Path(source_dir).glob("*.json")): if path.name == "manifest.json": continue raw = path.read_bytes() records.append({ "file": path.name, "size": len(raw), "sha256": hashlib.sha256(raw).hexdigest(), "mtime": datetime.now().isoformat(timespec="seconds") }) out = Path(source_dir) / "manifest.json" out.write_text( json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8" ) return out逻辑说明:每次下载完一批,把所有文件的字节数、SHA256 和写入时间记到manifest.json。下次更新时先读旧 manifest,对比 sha256,变了才重新下载。path.read_bytes()对小文件没问题,但如果单个 GeoJSON 超过 50MB,建议改成流式分块计算哈希。
参数说明:文件名不要用中文。中文名在部分 HTTP 服务里会触发多次转义,跨平台解压也容易乱码。我习惯用{adcode}_full.json这种纯数字命名,省份名称放在properties里,前端要显示时再读属性。manifest.json要单独跳过,不然下次扫描会把自己也记进去。
4. GeoJSON 文件打不开、边界错乱、坐标不对:下载后的 5 个常见坑
数据下载完不代表结束,真正让项目翻车的全是下载后的小问题。这一章集中写我踩过的坑,每一条都按现象、原因、解决的顺序说。
4.1 geojson 可以用 ArcGIS 打开吗:先看坐标系与编码
现象:把一个 GeoJSON 拖进 ArcGIS Pro,地图上是空白,或者提示“无法读取数据源图层”;有时候能显示但位置偏到海里。
原因:很多人搜“geojson 可以用 arcgis 打开吗”,答案是“能,但有前提”。ArcGIS 对 GeoJSON 的支持要求外层结构是标准 FeatureCollection,坐标系要是 WGS84 地理坐标系,且属性字段编码是 UTF-8。如果你下载的数据是 GCJ02 加密坐标,或者 SHP 转出来的属性表是 GBK 且没带上编码声明,ArcGIS 就会出现上述怪现象。
解决:先用 QGIS 打开同一个文件做交叉验证。如果 QGIS 正常而 ArcGIS 不正常,多半是坐标系或编码问题。有一个通用修复命令:
# 用 GDAL 强制转成 ArcGIS 最认的标准结构 ogr2ogr -f GeoJSON \ -t_srs EPSG:4326 \ -lco COORDINATE_PRECISION=6 \ -lco WRITE_BOM=YES \ fixed.geojson broken.geojsonWRITE_BOM=YES会在文件头加 BOM,ArcGIS 对带 BOM 的 UTF-8 识别更稳定。坐标系的修复不能靠猜,先ogrinfo -so -al broken.geojson看定义,再决定要不要转。
4.2 县级边界合并后出现飞线/缺口
现象:下载了某省所有区县的 GeoJSON,用前端库做 dissolve 合并成省级边界,结果相邻两县之间有细缝,放大看还有一块块小缺口;更严重的是路径飞到很远的地方,出现一条横跨屏幕的“飞线”。
原因:共享边界在两份区县数据里可能没有精确对齐,坐标在小数点后两位就开始分叉。前端 dissolve 只按几何求并集,不处理拓扑关系,所以裂缝会直接暴露出来。飞线通常是某个多边形的坐标串里混入了错误坐标点,或者文件本身下载不完整。
解决:用后端计算引擎合并,比如 Shapely 的unary_union,先做拓扑修复再导出:
from shapely.geometry import shape from shapely.ops import unary_union import json with open("counties.geojson", encoding="utf-8") as f: data = json.load(f) geoms = [shape(feature["geometry"]) for feature in data["features"] if feature.get("geometry")] merged = unary_union(geoms) # 用 buffer(0) 修复微小的自相交和裂缝 merged_fixed = merged.buffer(0) print("合并后类型:", merged_fixed.geom_type)buffer(0)是一个经典技巧,它会把小于容差的缝隙吸掉,同时清理自相交,但不改变坐标精度。合并前最好先检查每个要素is_valid,把无效几何单独过滤出来,避免坏点污染整体。
4.3 文件下载不完整但没报错:Content-Length 与 gzip 的坑
现象:一个市级 GeoJSON 应该有 400 个乡镇,下载下来解析成功,但数出来只有 380 个;文件在浏览器里打开也正常,就是少了几个边界。
原因:下载过程中连接被源站断开,requests 默认在Content-Length对不上时会抛ChunkedEncodingError,但如果源站返回的是Transfer-Encoding: chunked,没有 Content-Length,断流时可能只警告而不报错。还有一种情况是源站在网关层把 gzip 压缩后的字节数作为 Content-Length,而客户端解压后文件大小对不上。
解决:下载后强制校验实际字节数与 Content-Length 不一致就重试:
import requests def download_with_length(url, target): with requests.get(url, stream=True, timeout=15) as r: r.raise_for_status() expected = r.headers.get("Content-Length") total = 0 with open(target, "wb") as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) total += len(chunk) if expected and total != int(expected): raise IOError(f"下载不完整: {total} != {expected}")注意如果服务器返回了Content-Encoding: gzip,你可以把expected和total都取解压后的字节数对比,或者直接把响应体的raw.read()拿来用。最简单的做法是:校验失败就删掉重下,宁可多费一次请求,也不要留下一个残文件。
4.4 数据源更新导致 URL 失效:版本化与镜像
现象:半年前写好的下载脚本,今天跑出来全是 404,或者返回的 JSON 里多了一批新区划,属性字段也变了。
原因:GeoJSON 数据源不是稳定契约,阿里 DataV 从areas_v2换到areas_v3时,旧 URL 批量失效;区划调整后 adcode 也会变,比如莱芜并入济南后相关代码就失效了。很多项目直接把在线 URL 写死在前端,这是最脆弱的做法。
解决:下载到本地后做一次版本化快照。我在数据目录里放一个source.json,记录下载时间、源 URL 和 adcode 列表;另外把manifest.json里的 sha256 作为文件逻辑版本。更新时先跑 HEAD 请求看Last-Modified或ETag,变了才重新下载。这样即使源站哪天删掉老版本,本地镜像仍然可用。不要长期依赖在线 URL,这是做数据项目的基本纪律。
4.5 下载速度慢/被限流:测速、重试与限流策略
现象:批量下载时,前 10 个文件很快,到第 50 个突然全部超时;稍等一会又能下载,但速度掉到几十 KB/s。
原因:大概率是源站对单一 IP 做了限流,或者上了防火墙策略。GeoJSON 文件虽小,但高并发短请求会触发网关的 rate limit。
解决:在脚本里加“下载文件测速”这一步,先探底再跑量:
# 下载前先测一条大文件的速度,-o /dev/null 丢弃内容 curl -o /dev/null -s -w "速度: %{speed_download} bytes/s\n" \ https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json拿到基准速度后,把并发线程数设置为一个保守值,线程内再限制请求频率。我自己的习惯是:单线程速度大于 500KB/s 时用 4 并发;小于 100KB/s 时只开 1 并发,同时把超时时间拉长。配合上一章的指数退避重试,基本能熬过限流窗口。
5. 把下载的 GeoJSON 用起来:本地数据服务与可视化验证
数据落地后,下一步就是让前端能访问到。我从来不直接双击 index.html 看效果,而是起一个本地 HTTP 服务,顺便解决跨域问题。这样能尽早发现坐标系、字段命名和文件损坏的问题。
5.1 用 Python 做本地 HTTP 服务供前端测试
直接用python -m http.server虽然快,但它的目录浏览在部分浏览器上有缓存,而且不能自定义响应头。我一般写一个小脚本,统一挂载数据目录并加 CORS 头:
from http.server import HTTPServer, SimpleHTTPRequestHandler import os os.chdir("data") # 数据目录,里面放着 *_full.json class Handler(SimpleHTTPRequestHandler): def end_headers(self): # 给所有响应加 CORS 头,前端跨域请求不会挂 self.send_header("Access-Control-Allow-Origin", "*") super().end_headers() def log_message(self, fmt, *args): # 减少控制台噪音,只记录访问路径 print(self.path) HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()逻辑说明:os.chdir("data")让服务根目录切到数据目录,URL 路径直接就是/100000_full.json。end_headers里先加Access-Control-Allow-Origin: *,这样用 ECharts 或 Leaflet 从另一个端口调试时不会被浏览器 CORS 拦截。
参数说明:127.0.0.1只监听本机,适合本地调试;如果要把数据暴露到局域网给同事看,改成0.0.0.0,但要注意服务没有认证,不要放到公网。8000端口如果被占用,换成 8080 或 9000 都行。
5.2 在 Leaflet 中快速验证数据是否可用
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>验证 GeoJSON</title> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> </head> <body> <div id="map" style="height: 500px"></div> <script> fetch("http://127.0.0.1:8000/100000_full.json") .then(r => r.json()) .then(data => { const map = L.map("map").fitBounds( L.geoJSON(data).getBounds() ); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 12 }).addTo(map); L.geoJSON(data, { style: { color: "#d00", weight: 1, fillOpacity: 0.2 }, onEachFeature: (feature, layer) => { layer.bindPopup(feature.properties?.name || "无名称"); } }).addTo(map); }); </script> </body> </html>逻辑说明:先用fetch拉取本地服务里的 GeoJSON,再用L.geoJSON(data).getBounds()算出边界范围,调用fitBounds让地图自动缩放。这个动作能立刻暴露坐标系问题:如果数据是从投影坐标系错误转出来的,地图会飞到一个奇怪的位置。onEachFeature里绑定弹出框,顺手验证properties里能不能取到中文名称。
参数说明:这个页面必须通过 HTTP 访问,不能直接file://打开,否则 fetch 会被浏览器安全策略挡掉。Leaflet 用的是 OpenStreetMap 瓦片,国内网络访问不稳定时可以换成其他瓦片源,但这一步只是为了验证数据,不要让它成为网络瓶颈。
5.3 简化与压缩:降低前端加载体积的实用技巧
下载的 GeoJSON 精度很高,直接塞给浏览器可能会让页面卡顿。以全国边界为例,原始文件可能几十 MB,简化到 5% 的顶点规模后视觉差异几乎看不出来。
# 安装 mapshaper 后做占位最优简化 npx mapshaper 100000_full.json \ -simplify 5% keep-shapes \ -o 100000_full_simplified.json # 再看看简化后的文件大小 ls -lh 100000_full_simplified.json逻辑说明:-simplify 5%意思是保留约 5% 的顶点,keep-shapes保证简化时不会把多边形面积缩到零或直接删除小岛。mapshaper 的简化算法对边界保持做得比简单抽稀好,适合前端可视化场景。
参数说明:5%不是固定值,省级数据我用 10%,市级用 5%,乡镇街道用 20%。简化后务必用第 5.2 节的 Leaflet 页面重新验证一遍,重点看沿海岛屿和跨行政区边界有没有粘连。这一步不要在第一版就开始做,先拿着完整精度数据跑通流程,最后再决定压缩比例。
6. 最后一步:给数据做版本管理与自动更新
数据文件下到本地、验证通过后,最容易被忽略的是更新机制。行政区划不是一成不变的,每过一两年就有合并、撤县设区、乡镇撤并。我不会手动去刷新下载,而是在数据目录里维护一个轻量级的source.json,记录每个文件的来源和哈希:
import json, hashlib from pathlib import Path def verify_local_data(source_path): with open(source_path, encoding="utf-8") as f: source = json.load(f) for item in source["files"]: path = Path(item["path"]) if not path.exists(): print(path, "缺失") continue digest = hashlib.sha256(path.read_bytes()).hexdigest() if digest != item["sha256"]: print(path, "内容已变化") else: print(path, "一致")逻辑说明:这个脚本不请求外部接口,只做本地一致性检查。每次重新下载或手工替换文件后,更新source.json里的 sha256 和下载时间。等到源站发布新版本时,你只需要比较 remote 的 ETag 与记录里的时间,就能知道哪些文件需要重下。
我的习惯是把source.json和manifest.json一起提交到 Git 仓库。数据文件可能太大不进版本库,但这两个 JSON 文件体积很小,能帮你回溯“这个边界是哪天谁下的、当时的源 URL 是什么”。这个习惯让我少踩了很多次“数据悄悄过期”的坑,也希望帮到你。
本文还有配套的精品资源,点击获取