简介:全国五级行政区划数据覆盖省、市、区县、乡镇、村委会五个层级,收录近77万条真实行政区域记录,适合地理信息开发、物流配送、统计分析、电子商务本地化服务等需要地域维度的应用场景。压缩包采用RAR格式,单包仅7.97MB,内含一个可直接导入MySQL的SQL脚本;脚本包含建表语句和完整的INSERT数据插入语句,每行记录按省、市、区县、街道/乡镇、村等层级字段组织,导入后即可创建数据表并直接查询使用。目前已有1074人学习使用,特别适合需要快速获取标准化行政区域名录的后端开发、数据分析、GIS相关从业者。相比自行爬取和清洗行政区划数据,这份数据能帮助使用者省去大量预处理时间,可直接支撑地图搜索、区域聚合统计、电商附近推荐、物流路线规划等功能,为项目提供可靠且易于持续维护的基础数据保障。
1. 全国5级行政区划数据到底解决什么问题:77万条记录不是一张表那么简单
做电商订单区域统计时,我拿到过一批用户地址,有人写“某省某市某区某街道”,有人直接写“某村委会”,还有人写“某开发区”。要把这些参差不齐的地址归到同一个统计口径,靠正则不行,靠人工更不可能。唯一靠谱的办法,是先用一份全国5级行政区划数据做底表:从省、市、区县、乡镇到村委会,每一级都有稳定的名字和代码,用户填写的任意一个地址片段都能在这张表上找到归属。这份数据将近77万条记录,听上去很庞大,其实前两级只占几百条,真正的大头是乡镇和村委会这两级。它的核心竞争力不是“多”,而是层级关系:有了这张表,地址标准化、区域统计、物流路由、政务下钻都能变成一次简单的关联查询。
适合谁?做数据仓库、数据治理、后端接口开发、GIS 可视化、电商物流系统的人。如果你正在处理用户收货地址、门店定位、网格划分、报表下钻这类需求,这份数据就是你最值得先投入的基础设施。接下来这套做法,是我在模拟项目 X 里实际用过的完整落地路径:先建表、再清洗、后更新、最后踩坑。
2. 拿到77万条原始数据后的第一件事:先建一张能落地的五级区划表
原始数据拿到手通常是 CSV,但直接用不是好主意。第一步永远是建表,把层级、代码、父级关系落成结构化字段,否则后面每次查询都是灾难。很多人觉得数据量不大,直接导入临时表,需要的时候再查就行了,但实际业务一跑起来,地址标准化、区域下钻、报表聚合全都要依赖这张表,它的设计质量直接决定后续开发顺不顺。
2.1 五级层级怎么定义:从省到村委会的四种常见结构
“5级”并不是所有地方都严格串联,常见的结构有四类,表结构设计必须全部兼容。
第一类是最标准的省-市-区县-乡镇-村委会,例如大多数普通地级市;第二类是直辖市结构,直辖市本身是一级,下面直接是区,区下面是街道,街道下面是社区,严格说是四级,但为了统一,仍然把直辖市作为省级、区作为市级或区县级处理;第三类是省直辖县级市,县级市直接挂在省下面,没有地级市这一层;第四类是开发区和高新区这样的经济功能区,它们有自己的统计用代码,也出现在这个数据里,但不属于行政区划。
表里建议加一个 level 字段,1 到 5 分别代表省级、市级、区县级、乡镇级、村级。注意在实际数据里,level 和代码长度有时并不能完全对应。比如省直辖县级市的 level=3,但其父级是 level=1,而不是 level=2。这些情况后面清洗时单独处理。
我用一个表格总结常见层级关系:
| level | 常见叫法 | 代码处理建议 | 常见父级 |
|---|---|---|---|
| 1 | 省 / 自治区 / 直辖市 | 取前 2 位 | 无 |
| 2 | 地级市 / 地区 / 自治州 | 取前 4 位 | 省级 |
| 3 | 区 / 县 / 县级市 | 取前 6 位 | 地级市或省级 |
| 4 | 乡镇 / 街道 | 取前 9 位 | 区县级 |
| 5 | 村委会 / 居委会 | 取完整 12 位 | 乡镇 / 街道 |
这个表格不是让你把 code 存成数字,而是告诉你“截断到第几位去匹配父级”。代码统一存 char(12),不要用 int,这一点我下面单独说。
2.2 区划代码的父子关系与12位编码逻辑
区划代码的 12 位字符串本身就有很明显的树形关系:省级看前 2 位,市级看前 4 位,区县看前 6 位,乡镇看前 9 位,村看完整 12 位。父级 code 就是本级 code 按上述长度截断后的结果。所以我建表时不额外设计 tree 表,直接用 parent_code 字段存父级 code,省去递归表,查询也更快。
这里有一个关键点:parent_code 在原始数据里可能没有直接给,需要自己算。计算规则很简单,但有个边界:省直辖县级市的父级是省级,所以它的 code 在市级段不一定能找到对应节点。比如某个县级市的 code 前 4 位是一段自定义序列,直接按“前 4 位去匹配市级节点”会断链。我的做法是先看第 2 级节点是否存在,不存在就把父级指向省级节点。
在做编码处理时不要用 int 类型存 code,必须用 char(12)。很多人在导入时把 code 当成数字,前导 0 丢失,父级关联就全断了。这个错我亲眼见人犯过,还查了半天才发现是表结构的问题。另外名称字段也要注意,不要用 varchar(20) 这种过短的类型,因为村委会的完整名称可能很长,比如“某省某市某区某乡某村村民委员会”,截断之后就没法看了。
2.3 建表SQL与导入的三种姿势
这里给出一套可直接落地的建表 SQL,我建议把版本号也加进去,因为区划数据每年都会更新,保留版本才能做历史关联:
CREATE TABLE `region` ( `code` char(12) NOT NULL COMMENT '12位区划代码', `name` varchar(64) NOT NULL COMMENT '区划名称', `level` tinyint NOT NULL COMMENT '层级:1省 2市 3区县 4乡镇 5村', `parent_code` char(12) NOT NULL COMMENT '父级区划代码', `old_name` varchar(64) DEFAULT NULL COMMENT '曾用名,用于历史匹配', `version` varchar(10) NOT NULL COMMENT '数据版本,如2024', PRIMARY KEY (`code`, `version`), KEY `idx_parent` (`parent_code`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;主键用 (code, version) 而不是单字段 code,是为了在同一张表里保留多个历史版本。如果业务上只关心最新版本,也可以把版本拆成单独的历史表,但我个人的经验是复合主键省事,diff 和回溯都方便。
导入 77 万条记录,常见有三种方式:MySQL 的 LOAD DATA、Python 批量 insert、Spark 之类的分布式导入。77 万条根本用不到 Spark,前两种足够了。先看 LOAD DATA:
LOAD DATA LOCAL INFILE '/tmp/region.csv' INTO TABLE region CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (code, name, level, parent_code, version);IGNORE 1 LINES 是跳过 CSV 表头;如果文件带 BOM,建议先用编辑器转成无 BOM 或使用下面 Python 方式;LOAD DATA 在 MySQL 8 里默认可能关闭 local_infile,需要先确认SHOW VARIABLES LIKE 'local_infile',否则会报错。
我平时更常用 Python 导入,因为可以在写入前做一次类型清洗和数量校验:
import pymysql import csv conn = pymysql.connect( host='localhost', user='root', password='your_password', database='test', charset='utf8mb4' ) cursor = conn.cursor() sql = "INSERT INTO region (code, name, level, parent_code, version) VALUES (%s, %s, %s, %s, %s)" rows = [] with open('region.csv', newline='', encoding='utf-8-sig') as f: reader = csv.reader(f) next(reader, None) for row in reader: # 统一去空格,code 必须保持字符串,不能转 int code = row[0].strip() name = row[1].strip() level = int(row[2]) parent_code = row[3].strip() version = row[4].strip() rows.append((code, name, level, parent_code, version)) if len(rows) >= 1000: cursor.executemany(sql, rows) rows.clear() if rows: cursor.executemany(sql, rows) conn.commit() cursor.close() conn.close()这段代码里最需要注意的是 code 字段:strip 是去掉首尾空格,但绝不能用 int 转换。另外用 utf-8-sig 打开文件可以自动处理 BOM。批量提交设成 1000 条,避免单条 insert 太慢,也不会因为事务过大占太多内存。导入完成后,顺手跑一下第 3 章的校验脚本,确认行数、断链、重复都正常,再继续往下做。
3. 把五级长表摊成省市区村一体的宽表:清洗与合并实战
长表是每一条记录一个节点,查询某个村需要向上 join 四次或五次才能拿到完整的省市区。业务系统里更常用的是宽表:一行记录就是一个村,字段直接列出省名、市名、区县名、乡镇名、村名。统计时不用再做一串 join,关联订单表也只要一次。这部分是整个环节里最费精力的地方,因为真实数据里总会混着前面说的省直辖县级市、开发区、飞地、改名等问题。
3.1 为什么需要宽表:一次join出省市区街道村
你可以把长表想象成一张树表,每个节点只知道自己是谁、父亲是谁;宽表则是把从根到叶子路径上的所有名字都冗余到一行里。这样做有两个直接好处:一是查询快,二是口径统一。如果每次统计都要现算路径,不同程序员写出来的代码可能会因为括号、截断长度不一致而得出不同结果,宽表把口径固化在生成逻辑里。
我一般用 Python 写宽表生成,逻辑是:对每一行节点,从它自己开始沿着 parent_code 向上回溯,直到没有父级为止。用一个字典缓存所有节点信息,77 万行的量级,运行时间也就是几秒到十几秒:
import pandas as pd df = pd.read_csv('region.csv', dtype={'code': str, 'parent_code': str}) df['level'] = df['level'].astype(int) region_map = {r['code']: r for _, r in df.iterrows()} def resolve_path(code): """从某个节点回溯到根,返回每一级的名称。""" levels = {1: '', 2: '', 3: '', 4: '', 5: ''} node = code visited = set() while node and node in region_map and node not in visited: visited.add(node) row = region_map[node] levels[row['level']] = row['name'] node = row['parent_code'] return levels wide_rows = [] for _, row in df.iterrows(): if row['level'] != 5: continue levels = resolve_path(row['code']) wide_rows.append([ row['code'], row['name'], levels[1], levels[2], levels[3], levels[4], levels[5], row['parent_code'] ]) wide_df = pd.DataFrame(wide_rows, columns=[ 'code', 'name', 'province_name', 'city_name', 'county_name', 'town_name', 'village_name', 'parent_code' ]) wide_df.to_csv('region_wide.csv', index=False, encoding='utf-8-sig')这段代码里我加了一个 visited 集合,防止脏数据里出现 parent_code 指向自己的情况,不加的话会死循环。levels 字典的 key 是 level 值,如果原始数据里的 level 标错了,回溯结果就会错位,所以跑完一定要做好校验。宽表只保留村级叶子节点,如果业务需要统计乡镇级,可以把 if row['level'] == 5 改成 >= 4,自行选择。
3.2 合并规则:同名节点不能按名字匹配,必须按code前缀
在清洗中最容易翻车的操作是按 name 做关联。全表有大量重名:不同乡镇下都有“xx村”,不同县下都有“xx镇”。有人想用“省+市+区+乡镇+村”拼接成唯一键,但只要有一个名字带空格或简称,马上就崩。正确做法是全程按 code 截断或 parent_code 回溯,name 只用来展示和检索。
对于省直辖县级市,宽表里市级字段是空的。处理策略有两种:如果业务强制要求五级都有值,可以把省级名称回填到市级字段,并在表里标一个 is_virtual_level 字段;如果统计时需要区分“真实市”和“回填市”,就保留空值。我个人推荐回填,因为报表下钻时少一层会导致很多前端组件渲染异常,回填之后至少层级是完整的。判断逻辑可以这样写:
if not levels[2]: levels[2] = levels[1]这种回填要记录到文档里,不然其他同事看到某县的市字段和省一样,会以为数据错了。同理,开发区这类功能区的 code 在标准行政区代码里可能找不到对应父级,正确做法是先把它归并到所属行政区,归并规则维护一张映射表,不在宽表脚本里硬编码。
3.3 校验脚本:查重、查断链、查层级连续性
清洗完必须校验,不校验就上线等于埋雷。我最常跑三条 SQL,分别查重复 code、断链、层级连续性。第一类是查重复:
SELECT code, COUNT(*) AS cnt FROM region GROUP BY code, version HAVING cnt > 1;有重复 code 说明源文件里有脏行,通常同一个 code 出现了两条不同 name,需要人工确认保留哪条。第二类查断链:
SELECT r.code, r.name, r.parent_code FROM region r LEFT JOIN region p ON r.parent_code = p.code AND r.version = p.version WHERE p.code IS NULL AND r.level > 1;level=1 的省级节点没有父级,所以排除掉;其他节点如果 parent_code 找不到对应记录,说明断链,需要回溯检查。第三类查层级连续性:
SELECT r.code, r.name, p.level AS parent_level FROM region r LEFT JOIN region p ON r.parent_code = p.code AND r.version = p.version WHERE r.level = 3 AND ( (p.level = 2 AND LEFT(r.code, 4) <> LEFT(p.code, 4)) OR (p.level = 1 AND LEFT(r.code, 2) <> LEFT(p.code, 2)) );这里特意兼容了省直辖县级市:如果父级是 level=2,本级 code 前 4 位要和父级 code 前 4 位一致;如果父级是 level=1,只用管前 2 位一致。这样就不会把正常的省直辖县级市误判成断链。校验脚本应该作为每次更新后的固定动作,不要只在导入时跑一次。
4. 从静态数据到可用服务:增量更新与对外接口设计
区划数据不是死数据,每年都有村合并、镇改街道、县改区。很多项目上线后就不再更新,过了两年地址匹配准确率肉眼可见地下降。这章讲两件事:怎么更新,以及怎么把这份静态数据变成一个可以被业务系统调用的服务。
4.1 更新节奏与增量比对:全量替换还是打补丁
区划主管部门一般会发布新版本,常见做法是每年拿新版全量覆盖,但这会让用过旧版 code 的历史订单失去关联。我一般这样设计:region 表保留 version 字段,同一张表里同时存在最近两个版本,并定期清理更久的版本;再建一张变更表,记录 code 的新增、停用、改名、父级变化。这样历史统计和新报表都能对上。
增量比对的脚本可以复用同一份 CSV 读取逻辑,核心是 diff 两个版本的字典:
import csv def load_version(path): m = {} with open(path, newline='', encoding='utf-8-sig') as f: for row in csv.DictReader(f): code = row['code'].strip() m[code] = (row['name'].strip(), int(row['level']), row['parent_code'].strip()) return m old = load_version('region_2023.csv') new = load_version('region_2024.csv') added = {c: v for c, v in new.items() if c not in old} removed = {c: old[c] for c in old if c not in new} renamed = {c: (old[c], new[c]) for c in old if c in new and old[c][0] != new[c][0]} parent_changed = {c: (old[c], new[c]) for c in old if c in new and old[c][2] != new[c][2]}拿到这四类变更之后,写入变更表即可。注意千万不要把 removed 的记录物理删除,正确做法是在 region 表加一个 valid_to 字段,把停用日期写进去,查询默认过滤。这种保留历史的做法能让你在回看某个季度数据时,依然按当时的有效区划来统计,不会被后来的行政调整带偏。
4.2 对外接口设计:按父级查子级与关键字搜索
无论内部用还是给前端组件用,接口就两个:按父级取子级、按关键字搜索。这里我用 FastAPI 示例,逻辑可以直接搬到任何框架里:
from fastapi import FastAPI, Query app = FastAPI() @app.get("/region/children") def get_children(parent_id: str = "0"): if parent_id == "0": sql = "SELECT code, name, level FROM region WHERE level = 1 AND valid_to IS NULL ORDER BY code" params = () else: sql = "SELECT code, name, level FROM region WHERE parent_code = %s AND valid_to IS NULL ORDER BY code" params = (parent_id,) cursor.execute(sql, params) rows = cursor.fetchall() return [{"code": r[0], "name": r[1], "level": r[2]} for r in rows] @app.get("/region/search") def search_region(keyword: str = Query(..., min_length=2), level: int = None): sql = "SELECT code, name, level FROM region WHERE name LIKE %s AND valid_to IS NULL" params = [f"%{keyword}%"] if level: sql += " AND level = %s" params.append(level) cursor.execute(sql, params) rows = cursor.fetchall() return [{"code": r[0], "name": r[1], "level": r[2]} for r in rows]parent_id 传 0 时返回省级列表,传某个 code 时返回下一级。search 接口的 LIKE 前导通配符在 77 万行上性能一般,如果量大,建议把区划表加载到内存或用 ES 做关键词检索。level 参数是可选的,用于只搜某级节点。
4.3 与业务地址库融合:把区划名称做成地址分词词典
业务系统里用户填写的地址是自然语言,不是标准区划名。最常见的中文地址写法是“某省某市某区某街道某村”,少数会省略中间某级。常见做法是把所有区划名称加载到一个词典里,对地址做最大长度匹配,优先匹配长词,避免“某县”把“某县某某镇”截断。我写了一个简化版:
def match_region(address, region_map): matched = [] i = 0 while i < len(address): for length in range(min(12, len(address) - i), 1, -1): seg = address[i:i + length] if seg in region_map: matched.append(region_map[seg]) i += length break else: i += 1 return matchedregion_map 的 value 建议存一个列表,因为同名村可能对应多个 code,例如不同乡镇下都有“和平村”。单纯贪心匹配会把“和平村”解析成第一个出现的和平村,造成误归。更稳的办法是匹配到多个候选项时,根据地址里相邻的上级词做二次过滤,比如先匹配出“某镇”,再用“某镇”去限制“和平村”的候选集。对新手来说,先跑通这个贪心版本,再看匹配失败的具体样例逐步加规则,比一开始就设计复杂的 NER 模型更实际。
5. 避坑:五级区划数据使用中的常见问题与排查
使用这份数据踩过的坑,绝大多数不是数据量问题,而是行政区划本身的复杂性和版本混乱造成的。这一章列几个高频问题,全部按“现象 → 原因 → 解决”来写,可以直接当排查手册用。
5.1 开发区和高新区代码导致“一区两名”
现象:业务地址里写“某市高新区”,区划表里同时存在“某区”和“某高新技术产业开发区”两条同级记录,按名称统计时被拆成两个区域,报表对不上。
原因:经济功能区有独立统计代码,但行政上仍属于所在行政区,名字和行政区不同。
解决:建一张功能区映射表,把开发区 code 归并到所属行政区 code。统计时用行政区 code 聚合,展示时如果用户想看到开发区名称,可以单独存一个 display_name 字段,不要直接把功能区当成独立行政区。
5.2 省直辖县级市让五级变四级
现象:宽表里某县级市的市级字段为空,报表里出现大量“未知市”,下钻少了一层。
原因:行政层级本身没有地级市,县级市直接挂在省级下面。
解决:在生成宽表时对缺失市级做回填,把省级名称复制到市级字段,同时加一个 is_virtual_level 字段标明是回填的。后端接口可以在返回时带这个标识,前端展示和统计口径统一按五级走。
5.3 村委会改名后历史数据无法关联
现象:旧版本里叫“某村”,新版本里改名“某社区”,用新表去匹配历史订单,匹配失败。
原因:行政区划调整导致名称变更,但 code 可能没变,也可能连 code 一起变了。
解决:保留历史版本,不要把老记录的 name 直接覆盖,而是把旧名称写入 old_name 字段。业务匹配时先按 code 精确匹配,匹配不到再尝试通过曾用名映射。这样既不影响新数据,也不会把历史统计做没了。
5.4 飞地与地图边界不匹配:统计口径和展示口径的冲突
现象:某村在行政上属于 A 市,但地理上位于 B 市地界,在地图上画出来会“飞”出去,用户认为数据错了。
原因:行政区划管辖区与地理边界不重合,也就是俗称的飞地。区划代码表体现的是行政隶属关系,不保证几何多边形连续。
解决:做数据分析按区划 code 归属统计;做 GIS 可视化按实际空间边界展示。两个口径不能混用。如果业务只有经纬度没有区划码,用空间 join 可能把飞地归到错误行政区,这时候必须抽出来人工复核,不能直接信空间计算结果。
5.5 村委会和居委会归类口径不一致
现象:统计“城市人口”和“农村人口”时,同一个村的数据在两个统计口径里结果不同,领导问起来解释不清。
原因:有些版本的 5 级数据把村委会和居委会都叫“村委会”,level 字段都标 5,但城乡分类代码不同。有人按名称后缀判断,有人按代码判断,口径自然不一致。
解决:表字段里单独保存城乡分类信息,判断城市/乡村时直接用这个字段,不要用 name 后缀的“村委会”“居委会”来判断。判断逻辑写成文档并让前端、后端、数据组统一使用,避免各写各的规则。
6. 把77万条数据用出花:两个值得马上做的进阶动作
6.1 用code前缀做任意层级聚合,比group by name更稳
如果你的宽表里每一行是一个村级节点,那下面这条 SQL 就是各省的村数量:
SELECT substr(code, 1, 2) AS province_code, COUNT(*) AS cnt FROM region_wide GROUP BY substr(code, 1, 2);换成 substr(code, 1, 4) 就是各市,换成前 6 位就是各区县。跨年对比时,用 name 聚合会因为更名而断裂,用 code 前缀只要前缀归属不变,结果就可比。我在做区域看板时,所有下钻都基于 code 前缀计算,前端点击某个区域,后端拿到的参数也是这个前缀,而不是一串中文名,这样可以从根上避免重名和改名的干扰。
6.2 给区划表挂中心点,把文字区划变成可下钻地图
区划表本质上是树形文本,想落到地图上还差一个坐标。可以给每个节点增加 lng、lat 两个字段:
ALTER TABLE region ADD COLUMN lng DECIMAL(10, 6), ADD COLUMN lat DECIMAL(10, 6);省级和市级可以用当地中心点,村级中心点建议通过地址库解析或 POI 聚合得到,不要自己去地图上一个个点。有了坐标之后,就能实现“全国地图 → 省 → 市 → 区县 → 乡镇 → 村”的逐级下钻,点击某个区域,直接用区划 code 去查业务数据。这一步做之前先确认底图的坐标系,业务坐标和地图坐标系不一致的话,地图上会偏移几百米,看起来像数据错位。我的习惯是每个版本更新后,先跑一遍校验脚本,再同步坐标字段,最后重新生成宽表,这个顺序不要乱。这套流程我重复了很多次,最难的不是建表,而是版本切换时怎么保住历史数据关联,所以我坚持每一版都留快照、都跑 diff,宁可多存一份数据,也不要在排查时发现旧账对不上。希望帮到你。
本文还有配套的精品资源,点击获取