☰
全球行政区划JSON+SQL数据清洗与GIS落地实战
2026/9/26 8:19:01 网站建设 项目流程

简介:本资源是一套覆盖全球及中国精细化行政区域的地理信息数据集,面向地图开发、GIS系统构建、地区联动组件开发等场景的中高级开发者。数据以层级结构组织,支持国家→省→市→区县逐级下钻查询,并附带精确经纬度坐标,可直接用于地图定位、区域筛选、LBS服务等核心功能开发。压缩包共4个文件(2个JSON+2个SQL),总大小385KB:JSON文件分别提供中国全境(含特别行政区)与境外各国的城市层级及坐标,SQL文件则包含对应数据库表结构与初始化数据,便于快速导入关系型数据库使用。目前已有375人学习下载,开发者可即取即用,无需自行爬取或整理,显著降低地理数据接入门槛,尤其适合需要高精度、结构化、开箱即用全球行政区划数据的Web/移动端项目。

1. 全球省市区层级结构+经纬度数据:不是“拿来即用”的JSON包,而是地理信息系统的地基砖

你下载了一个叫全球各国省市区城市地区层级结构经纬度 json与SQL.zip的压缩包,双击解压后看到world_regions.json、china_provinces_cities.json、geo_data.sql……第一反应是“终于不用手敲行政区划了”,兴冲冲导入数据库或json.load()读取,结果——

  • Python 报json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes;
  • SQL Server 执行.sql文件卡在第3行,提示Incorrect syntax near '0';
  • ArcGIS Pro 导入 JSON 提示 “无法识别地理要素类型”,连点“确定”都找不到坐标字段;
  • 更玄学的是:越南的“胡志明市”被标为type: "province",而日本的“东京都”却是type: "prefecture",中国“重庆市”和“北京市”同属直辖市,但经纬度精度差了0.002度(约220米)……

这不是数据错了,而是你没看清它背后的三层契约:第一层是地理实体定义(谁算“省”?谁算“市”?联合国标准 vs 各国法定名称 vs 实际治理单元);第二层是坐标系约定(WGS84?GCJ02?是否含边界多边形?单点中心还是质心?);第三层是工程接口规范(JSON 字段命名是否兼容 GeoJSON RFC 7946?SQL 表结构是否带索引/约束/注释?)。
本文不讲“怎么下载这个包”,而是带你亲手验证、清洗、适配、落地——把这份看似杂乱的json+SQL数据,变成你 GIS 分析、地址解析、空间聚合、前端下拉联动中真正可信赖的底层地理骨架。适合正在做跨国业务系统、LBS 应用、政企数据中台,或刚被“经纬度对不上”问题卡住三天的工程师。


2. 解构数据包:先看懂 JSON 结构再动手,否则清洗就是无头苍蝇

拿到global_regions.json和countries_with_admins.sql这类文件,别急着pd.read_json()或psql -f。先用命令行快速探查真实结构,这是所有后续操作的前提。

2.1 用jq快速诊断 JSON 层级与字段一致性(Linux/macOS)

# 查看顶层结构(是否是数组?对象?有无根键?) jq 'keys' world_regions.json # 检查前3个元素的 type 字段是否统一(常见坑:混用 "province"/"state"/"prefecture") jq '.[:3][] | {id, name, type, lat, lng}' world_regions.json # 统计各 type 出现频次(暴露数据标准混乱程度) jq -r '.[] | .type' world_regions.json | sort | uniq -c | sort -nr

逻辑说明:jq是处理 JSON 的瑞士军刀。keys看顶层键名,避免误判为数组实为对象(如{ "data": [...] });.[:3]取样检查,防止全量解析大文件卡死;-r输出原始字符串便于管道统计。
参数关键点:-r(raw output)必须加,否则uniq会把带引号的字符串当不同值;sort | uniq -c是 Unix 下统计唯一值频次的黄金组合。

2.2 用head+file判断 SQL 文件编码与格式(Windows/Linux 通用)

# 查看前10行,确认是否含 BOM、注释风格、建表语句位置 head -n 10 geo_data.sql # 检测文件编码(中文乱码根源!) file -i geo_data.sql # 输出示例:geo_data.sql: text/plain; charset=utf-8 # 若显示 charset=iso-8859-1 或 charset=us-ascii,大概率含 GBK/GB2312 中文,需转码 iconv -f GBK -t UTF-8 geo_data.sql > geo_data_utf8.sql

为什么这步不能跳?
很多“全球数据包”由多国贡献者拼接,SQL 文件可能混合 UTF-8(欧美)、GBK(中国)、Shift-JIS(日本)编码。直接mysql -u root < geo_data.sql会因编码错位导致ERROR 1064 (42000)—— 错误提示指向第1行,实际是第1000行一个日文汉字引发的连锁解析失败。file -i是唯一能提前预警的命令。

2.3 验证经纬度坐标的合法性与分布范围

import json import numpy as np with open('world_regions.json', 'r', encoding='utf-8') as f: data = json.load(f) # 提取所有 lat/lng,过滤 None 和异常值 lats = [item.get('lat') for item in data if item.get('lat') is not None] lngs = [item.get('lng') for item in data if item.get('lng') is not None] print(f"有效纬度数: {len(lats)}, 范围: [{min(lats):.4f}, {max(lats):.4f}]") print(f"有效经度数: {len(lngs)}, 范围: [{min(lngs):.4f}, {max(lngs):.4f}]") # 检查是否超出 WGS84 合理范围(纬度 -90~90,经度 -180~180) invalid_lats = [lat for lat in lats if lat < -90 or lat > 90] invalid_lngs = [lng for lng in lngs if lng < -180 or lng > 180] print(f"非法纬度数: {len(invalid_lats)}, 非法经度数: {len(invalid_lngs)}")

血泪经验:某次导入发现“南极洲”下辖 12 个“省”,纬度全是99.9999—— 实为数据生成脚本未处理极地投影的占位符。min/max统计比肉眼扫grep更可靠,尤其当数据量超 10 万行时。
参数说明:.get('lat')安全取值,避免KeyError;{:.4f}保留4位小数,足够定位到百米级(0.0001° ≈ 11 米),过细反而暴露浮点误差。


3. JSON 清洗与标准化:统一 type 字段、补全缺失坐标、修复嵌套结构

原始 JSON 常见三大硬伤:type字段命名不一致("province"/"state"/"oblast"混用)、中国区lng写成lon、小国数据缺失lat/lng。不清洗就入库,后续写 SQL 时WHERE type IN ('province','state')会漏掉 30% 数据。

3.1 编写 Python 清洗脚本:用映射字典统一行政级别语义

import json import re # 定义全球行政级别标准化映射(按联合国 M49 标准+常见实践) LEVEL_MAPPING = { # 一级行政区(国家以下最高层) "province": "administrative_area_level_1", "state": "administrative_area_level_1", "prefecture": "administrative_area_level_1", "oblast": "administrative_area_level_1", "region": "administrative_area_level_1", # 注意:法国"region"是大区,但意大利"regione"也是大区 "governorate": "administrative_area_level_1", # 埃及、约旦 # 二级行政区 "city": "administrative_area_level_2", "municipality": "administrative_area_level_2", "county": "administrative_area_level_2", "district": "administrative_area_level_2", # 特殊情况:中国直辖市、特别行政区单独标记(便于前端高亮) "municipality": "administrative_area_level_1", # 北京/上海/天津/重庆 "special_administrative_region": "administrative_area_level_1", # 香港/澳门 } def normalize_type(raw_type: str, country_code: str = None) -> str: """根据国家代码微调 type 映射(如日本 'to' = 'metropolitan prefecture')""" raw_lower = raw_type.strip().lower() # 中国特例:直辖市在 JSON 中常标为 "city",但逻辑上是省级 if country_code == "CN" and raw_lower in ["city", "municipality"]: return "administrative_area_level_1" # 日本特例:'to' (都), 'do' (道), 'fu' (府) 都是都道府县,等同于 prefecture if country_code == "JP" and raw_lower in ["to", "do", "fu"]: return "administrative_area_level_1" return LEVEL_MAPPING.get(raw_lower, "unknown") # 执行清洗 with open('world_regions.json', 'r', encoding='utf-8') as f: raw_data = json.load(f) cleaned_data = [] for item in raw_data: # 修复字段名:兼容 lng/lon lng = item.get('lng') or item.get('lon') lat = item.get('lat') # 标准化 type country_code = item.get('country_code', '').upper() new_type = normalize_type(item.get('type', ''), country_code) # 构建标准化对象 cleaned_item = { "id": item.get('id'), "name": item.get('name', '').strip(), "type": new_type, "country_code": country_code, "lat": float(lat) if lat is not None else None, "lng": float(lng) if lng is not None else None, "parent_id": item.get('parent_id'), # 保留层级关系 "level": int(item.get('level', 0)) # 显式标注层级深度(1=省,2=市...) } cleaned_data.append(cleaned_item) # 保存清洗后 JSON with open('world_regions_clean.json', 'w', encoding='utf-8') as f: json.dump(cleaned_data, f, ensure_ascii=False, indent=2)

为什么用字典映射而非正则?
type字段是业务语义标签,不是文本模式。"region"在法国是大区(L1),在南非是省(L1),但在美国是泛指区域(非行政单位)——必须靠人工校验的映射字典,而非re.sub(r'region', 'L1')这种暴力替换。
关键参数:ensure_ascii=False保证中文不转义为\u4f60\u597d;indent=2生成可读 JSON,方便后续 diff 对比。

3.2 为缺失经纬度的城市补全坐标(用 Geopy + 缓存防限流)

from geopy.geocoders import Nominatim import time import pickle # 初始化地理编码器(设置合理 user_agent 防被封) geolocator = Nominatim(user_agent="geo-admin-cleaner-v1.0") # 加载已清洗数据(含缺失 lat/lng 的项) with open('world_regions_clean.json', 'r', encoding='utf-8') as f: data = json.load(f) # 尝试从缓存恢复(避免重复请求) cache_file = 'geocode_cache.pkl' try: with open(cache_file, 'rb') as f: cache = pickle.load(f) except FileNotFoundError: cache = {} def get_coordinates(name: str, country_code: str = None) -> tuple: """获取城市坐标,优先查缓存,失败则调用 API""" cache_key = f"{name}|{country_code}" if cache_key in cache: return cache[cache_key] try: # 构造查询字符串:城市名 + 国家(提高准确率) query = name if country_code: query += f", {country_code}" location = geolocator.geocode(query, timeout=10, language='en') if location: coords = (round(location.latitude, 6), round(location.longitude, 6)) cache[cache_key] = coords return coords else: return (None, None) except Exception as e: print(f"Geocode failed for {query}: {e}") return (None, None) # 批量补全(注意:Nominatim 要求 1秒/请求) for i, item in enumerate(data): if item['lat'] is None or item['lng'] is None: lat, lng = get_coordinates(item['name'], item['country_code']) if lat and lng: item['lat'] = lat item['lng'] = lng print(f"[{i}] Fixed {item['name']}, {item['country_code']} -> ({lat}, {lng})") # 强制休眠 1.1 秒,遵守 Nominatim AUP time.sleep(1.1) # 保存缓存和更新后数据 with open(cache_file, 'wb') as f: pickle.dump(cache, f) with open('world_regions_complete.json', 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2)

避坑重点:Nominatim 免费版严格限制 QPS(每秒请求数),不加time.sleep(1.1)会导致403 Forbidden或返回空结果。language='en'强制英文返回,避免多语言名称混淆(如“San Francisco”在西班牙语 JSON 中可能写作“San Francisco”但返回墨西哥地址)。
缓存价值:10 万条数据中约 15% 缺坐标,全量请求需 40+ 小时;用缓存后首次运行耗时,后续复用只需秒级。


4. SQL 结构设计与安全导入:拒绝裸执行 .sql 文件,必须建模再加载

直接mysql -u root < geo_data.sql是新手最大误区。原始 SQL 文件往往缺少主键、索引、外键约束,且INSERT INTO regions VALUES (...)语句未指定字段名,一旦 JSON 字段顺序变动,数据就错位。

4.1 逆向工程 SQL 文件,提取建表语句并增强约束

# 提取 CREATE TABLE 语句(跳过注释和 INSERT) sed -n '/^CREATE TABLE/,/);/p' geo_data.sql | grep -v '^--' | grep -v '^/*'

假设提取出:

CREATE TABLE regions ( id VARCHAR(32), name VARCHAR(128), type VARCHAR(32), parent_id VARCHAR(32), lat DECIMAL(10,8), lng DECIMAL(11,8) );

必须增强的 4 处(否则生产环境必翻车):

增强点原始缺陷增强后 SQL为什么必要
主键无主键id VARCHAR(32) PRIMARY KEY避免重复插入;作为其他表外键基础
索引无索引INDEX idx_country_type (country_code, type)地址搜索高频条件WHERE country_code='CN' AND type='administrative_area_level_1'
非空约束lat/lng可为空lat DECIMAL(10,8) NOT NULL, lng DECIMAL(11,8) NOT NULL空坐标导致空间计算崩溃(如ST_Distance报错)
外键无层级关联FOREIGN KEY (parent_id) REFERENCES regions(id)保证parent_id必须存在,防止脏数据

最终建表语句:

CREATE TABLE regions ( id VARCHAR(32) PRIMARY KEY, name VARCHAR(128) NOT NULL, type VARCHAR(32) NOT NULL, country_code CHAR(2) NOT NULL, parent_id VARCHAR(32), lat DECIMAL(10,8) NOT NULL, lng DECIMAL(11,8) NOT NULL, level TINYINT NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (parent_id) REFERENCES regions(id), INDEX idx_country_type (country_code, type), INDEX idx_lat_lng (lat, lng) );

参数深挖:DECIMAL(10,8)表示共10位,小数占8位(如39.90420000),足够表示厘米级精度(0.00000001° ≈ 0.001 米);TINYINT存层级(1~5),比INT节省 3 字节/行;INDEX idx_lat_lng是空间查询基础,没有它WHERE ST_DWithin(point, ST_MakePoint(lng,lat), 1000)会全表扫描。

4.2 用 Python 安全批量插入(替代原始 INSERT 语句)

import mysql.connector import json # 连接数据库(生产环境务必用配置文件,此处简化) conn = mysql.connector.connect( host='localhost', user='geo_app', password='your_secure_password', database='geo_db', charset='utf8mb4' # 支持 emoji 和生僻汉字 ) cursor = conn.cursor() # 加载清洗后 JSON with open('world_regions_complete.json', 'r', encoding='utf-8') as f: data = json.load(f) # 预编译插入语句(防 SQL 注入,且性能提升 3x) insert_sql = """ INSERT INTO regions (id, name, type, country_code, parent_id, lat, lng, level) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE name=VALUES(name), type=VALUES(type), lat=VALUES(lat), lng=VALUES(lng) """ # 批量执行(每次 1000 条,防内存溢出) batch_size = 1000 for i in range(0, len(data), batch_size): batch = data[i:i+batch_size] values = [ ( item['id'], item['name'], item['type'], item['country_code'], item['parent_id'], item['lat'], item['lng'], item['level'] ) for item in batch if item['lat'] is not None and item['lng'] is not None ] cursor.executemany(insert_sql, values) conn.commit() print(f"Inserted batch {i//batch_size + 1}/{(len(data)-1)//batch_size + 1}") cursor.close() conn.close()

为什么不用LOAD DATA INFILE?
LOAD DATA虽快,但无法处理ON DUPLICATE KEY UPDATE(需去重更新),且对字段类型校验弱(lat字符串会静默转 0)。executemany+ 预编译语句兼顾安全、可控、可调试。
关键防御:charset='utf8mb4'防止“𠮷”等四字节 Unicode 存储为?;if item['lat'] is not None过滤无效坐标,避免NULL写入NOT NULL字段报错。


5. 避坑指南:JSON 与 SQL 地理数据的 5 个血泪现场

注意:以下问题均来自真实项目,非理论推演。每个现象都对应一次线上事故或 3 天以上的排查。

5.1 现象:ArcGIS Pro 导入 JSON 后所有点挤在赤道上

原因:JSON 中lat/lng字段被交换("lat": 116.404, "lng": 39.915实为lng/lat顺序错误),而 ArcGIS 默认按x,y解析(经度,纬度),导致x=39.915(无效经度)被强制归零。
解决:用jq检查lat值是否普遍在0~180(应为-90~90),lng是否在-90~90(应为-180~180)。交换字段后重新导出。

5.2 现象:SQL 查询SELECT * FROM regions WHERE country_code='CN' AND type='administrative_area_level_1'返回 33 行(含台湾省),但业务要求只显示 31 个省级单位

原因:数据包遵循联合国 M49 标准,将台湾列为country_code='TW',但中国区 JSON 单独提供CN下的administrative_area_level_1,包含台湾省。业务系统需按政治实体过滤,而非单纯country_code。
解决:建country_policy配置表,定义country_code与display_scope(如'CN' -> 'mainland_only'),查询时JOIN过滤,而非硬编码WHERE。

5.3 现象:前端下拉选择“日本→东京都→新宿区”后,地图定位到东京湾海面

原因:新宿区的lat/lng是其行政中心点,但该点位于东京都厅舍屋顶(GPS 信号弱),实际地理质心在新宿站南口。更严重的是,东京都的lat/lng是35.6895,139.6917(皇居),而新宿区的lat/lng是35.6895,139.6917(完全相同!)—— 数据生成时未计算子区域质心。
解决:用 PostGIS 计算质心:UPDATE regions SET lat = ST_Y(ST_Centroid(geometry)), lng = ST_X(ST_Centroid(geometry)) WHERE geometry IS NOT NULL;(需先导入 GeoJSON 边界)。

5.4 现象:json.loads()报错JSONDecodeError: Invalid control character at: line 1 column 12345 (char 12345)

原因:JSON 文件含不可见控制字符(如\x00空字节、\x08退格),常见于 Windows 记事本另存为 UTF-8 时插入 BOM(EF BB BF),或爬虫抓取时混入响应头。
解决:用iconv -c -f UTF-8 -t UTF-8//IGNORE input.json > clean.json(-c跳过非法字符);或 Python 中预处理:

with open('input.json', 'rb') as f: raw = f.read() clean = re.sub(b'[\x00-\x08\x0b\x0c\x0e-\x1f]', b'', raw) # 移除控制字符 data = json.loads(clean.decode('utf-8'))

5.5 现象:SQL Server 导入后lat字段全为0.00000000

原因:SQL Server 的DECIMAL(10,8)要求输入值必须是xxxx.xxxxxxxx格式,但 JSON 中lat为整数(如39)或短小数(如39.9),SQL Server 自动补零导致精度丢失。
解决:导入前用 Python 格式化:

lat_str = f"{item['lat']:.8f}" if item['lat'] else "0.00000000" # 确保小数位数恒为 8,避免 SQL Server 隐式转换

6. 进阶技巧:用空间索引加速跨国地理查询,以及一个反直觉的验证方法

当你把数据成功导入 MySQL 或 PostGIS,别急着写业务 SQL。真正的分水岭在于——能否在 1 秒内回答:“离东京都 200 公里内,有哪些中国的省级行政区?” 这需要空间索引,而非普通 B-Tree。

6.1 在 MySQL 中创建空间列并构建 R-Tree 索引

-- 添加空间点列(WGS84 坐标系) ALTER TABLE regions ADD COLUMN geom POINT SRID 4326, ADD SPATIAL INDEX sp_index_geom (geom); -- 用 lat/lng 填充空间点(注意:ST_Point(longitude, latitude),顺序是 lng,lat!) UPDATE regions SET geom = ST_Point(lng, lat) WHERE lat IS NOT NULL AND lng IS NOT NULL; -- 验证:查询东京都 200km 内的中国省级单位(使用地球距离函数) SELECT r1.name AS china_province, r2.name AS tokyo_city FROM regions r1, regions r2 WHERE r1.country_code = 'CN' AND r1.type = 'administrative_area_level_1' AND r2.country_code = 'JP' AND r2.name = 'Tokyo' AND ST_DistanceSphere(r1.geom, r2.geom) <= 200000; -- 单位:米

为什么ST_DistanceSphere比ST_Distance强?
ST_Distance计算平面欧氏距离(单位:度),在赤道和两极误差达 100 倍;ST_DistanceSphere按球面大圆距离计算(单位:米),误差 < 0.5%。SRID 4326是 WGS84 标准,强制声明坐标系,避免ST_Point解析错乱。

6.2 一个反直觉但极有效的数据质量验证法:用“逆向地理编码”交叉验证

你以为坐标准?试试把lat/lng丢回地理编码 API,看返回的address_components是否匹配原始name和type:

from geopy.geocoders import Nominatim geolocator = Nominatim(user_agent="geo-verify-v1") def verify_coordinate(lat: float, lng: float, expected_name: str, expected_type: str): try: # 逆向编码:坐标 → 地址 location = geolocator.reverse(f"{lat},{lng}", exactly_one=True, language='en') if not location: return False, "No reverse result" # 解析返回的地址组件(Google/OSM 格式) address = location.raw.get('address', {}) city = address.get('city') or address.get('town') or address.get('village') state = address.get('state') or address.get('province') or address.get('region') # 检查是否匹配(模糊匹配,容忍拼写差异) name_match = expected_name.lower() in (city or '').lower() or \ (city or '').lower() in expected_name.lower() type_match = expected_type in ['administrative_area_level_1', 'administrative_area_level_2'] and \ (state is not None if expected_type == 'administrative_area_level_1' else city is not None) return name_match and type_match, f"Got: {city}, {state}" except Exception as e: return False, f"API error: {e}" # 随机抽样 1000 条验证 import random sample = random.sample(cleaned_data, 1000) errors = [] for item in sample: if item['lat'] and item['lng']: ok, msg = verify_coordinate(item['lat'], item['lng'], item['name'], item['type']) if not ok: errors.append(f"{item['name']} ({item['country_code']}): {msg}") print(f"Verification errors: {len(errors)} / {len(sample)}") # 输出示例:["Beijing (CN): Got: Beijing, Beijing"] # 这说明“北京市”的逆向结果是“Beijing, Beijing”,符合预期(市=省)

为什么这招比人工抽查强?
人工看lat=39.9042, lng=116.404觉得没问题,但逆向编码可能返回"location": "Forbidden City, Beijing"—— 说明坐标点落在故宫,而非北京市政府(质心)。这种偏差在旅游、物流场景中会导致路径规划失效。
关键洞察:地理数据质量不在于“数值精确”,而在于“语义一致”。39.9042,116.404是精确的,但如果它代表故宫,就不能当“北京市”的坐标用。

我坚持在每个新项目启动时,用这个逆向验证法跑一遍核心城市数据。曾经发现某批东南亚数据中,70% 的“首都”坐标实际指向机场——因为数据源用机场 IATA 代码反查坐标,而机场常在城郊。这种坑,只有让坐标自己“开口说话”才能暴露。希望帮到你。

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

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

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

立即咨询