文保数据库地名标准化修复全记录

一、问题起点

BYP's DB 的国保子库 national_sites.db 中,第七批以前的文物记录 Location 字段普遍缺失地级市这一层级,例如:

Id 名称 Province Location 问题
180 满堂围 广东省 广东省始兴县 缺"韶关市"
2908 北山摩崖造像 四川省 四川省大足县 缺市 + 已跨省划归重庆
2947 独乐寺 河北省 河北省蓟县 缺市 + 已跨省划归天津

直接后果:用户在列表页按 "韶关市" 或 "天津市" 筛选时,查不到辖下县市的文物;全文搜索 "韶关" 也只命中 Location 中字面包含 "韶关市" 的 3 条,漏掉了实际在韶关辖下的"满堂围"。


二、数据源

采用民政部官方公布的权威数据:

为什么选这份数据


三、整体方案

分四阶段推进,每阶段独立成脚本,便于回滚与复核:

阶段 A 基础设施:HTML → admin_division.db(3209 条扁平表)
阶段 B 诊断分析:扫描 5052 条 Location,三分类定位问题
阶段 C 增强回填:8 层规则 + 150 条历史字典 → 100% 覆盖
阶段 D 正式落库:ALTER TABLE + 批量 UPDATE + 建索引 + 前端联动

四、阶段 A:构建行政区划数据库

脚本

表结构(极简 4 字段 + 3 索引)

CREATE TABLE administrative_division (
    code        TEXT PRIMARY KEY,  -- 6 位国标代码
    name        TEXT NOT NULL,     -- 名称
    parent_code TEXT,              -- 父级代码(自引用)
    level       INTEGER NOT NULL   -- 1=省 / 2=市 / 3=县
);
CREATE INDEX idx_parent ON administrative_division(parent_code);
CREATE INDEX idx_level  ON administrative_division(level);
CREATE INDEX idx_name   ON administrative_division(name);

关键技巧

产物


五、阶段 B:诊断 Location 匹配

脚本

三分类策略

类别 判定 含义
A 已含市 Location 剥省名后以当代市名开头 直接可用
B 命中县 以当代县名开头 通过 parent_code 回溯父级市
C 未命中 两者都不匹配 需特殊处理

结果

分类 数量 占比
A 2748 54.4%
B 1916 37.9%
C 388 7.7%

388 条未命中清单完整存档于 诊断结果.md


六、C 类六大模式归纳

通过人工复核 388 条未命中,归纳出六种数据模式:

模式 数量 典型例子
1. 历史县名已撤县设市 / 改区 ~250 长安县→西安市长安区、黔西县→毕节市黔西市、隆昌县→内江市隆昌市
2. 地级市改名 ~15 襄樊市→襄阳市
3. 第七批"地区"未改"市" ~37 昌都地区→昌都市、海东地区→海东市
4. 直辖市 + 街道/景点 ~15 北京市延庆区
5. 多地并列 ~30 江苏省苏州市、无锡市、常州市
6. 真正的数据错误 ~10 浙川县(应为淅川县)、汩罗市(应为汨罗市)、黑龙扛省、新疆维吾尔自区

七、阶段 C:增强回填

脚本

8 层规则(按优先级从高到低)

规则 0  按 Id 特殊绑定      → 极少数 Location 不完整的个案(如水洞沟遗址)
规则 c  跨省大型遗产        → 长城、京杭大运河、茶马古道等 → City='跨省'
规则 s  新疆生产建设兵团    → 特殊处理
规则 a  直辖市              → City = Province
规则 A  当代市名匹配        → 直接采用
规则 B  当代县名 → 父市     → 通过 parent_code 回溯
规则 d  历史地名字典        → 约 150 条 (省,老名) → (市,新区县)
规则 e  多地并列            → 拆分后分别匹配,分号";"拼接

关键机制

(1) 省名别名反向匹配

处理错别字省名(不止匹配当代标准名,也匹配已知错别字变体):

PROVINCE_ALIAS = {
    '新疆维吾尔自区': '新疆维吾尔自治区',
    '黑龙扛省': '黑龙江省',
}

strip_province() 在剥省名前缀时会反向遍历所有映射到目标省的错别字,确保 "黑龙扛省绥滨县" 也能正确剥离。

(2) 容错 Location 异常残留

# '安徽省省阜南县' → 剥'安徽省'后剩'省阜南县' → 自动再剪一层 → '阜南县'
if rest.startswith('省') or rest.startswith('市'):
    rest = rest[1:]

(3) 历史地名字典 150 条

老地名(典型) 当代归属 类型
陕西省长安县 西安市长安区 撤县设区
四川省大足县 重庆市大足区 跨省调整
河北省蓟县 天津市蓟州区 跨省调整
四川省涪陵市 重庆市涪陵区 跨省调整
贵州省黔西县 毕节市黔西市 撤县设市
山东省长岛县 烟台市蓬莱区 撤县并区
广西三江县 柳州市三江侗族自治县 自治县全称
河南省浙川县 南阳市淅川县 错别字修正
湖南省汩罗市 岳阳市汨罗市 错别字修正
海南省西、南、中沙群岛办事处 三沙市 2012 新设地级市

完整字典见 增强回填City.pyHISTORICAL_CITY 常量。


八、执行结果

来源分布(5052 / 5052 = 100%)

来源 数量 占比 说明
A 当代市名匹配 2746 54.4% Location 已含当代市名
B 当代县名→父市 1698 33.6% 通过 parent_code 回溯
d_历史字典 331 6.6% 老地名映射
a_直辖市 261 5.2% 北京/上海/天津/重庆
c_跨省遗产 13 0.3% 长城等大型遗产
special_兵团 2 0.04% 新疆生产建设兵团
special_byid 1 0.02% 水洞沟遗址(Id=3290)

零未命中,388 条 C 类全部收敛。


九、阶段 D:正式落库

脚本

安全机制(两步确认)

  1. 默认 python 落库City字段.py预览,只读不改库
  2. --apply 并手动输入 yes 才真正执行
  3. 执行前自动备份 national_sites.db.bak.YYYYMMDD_HHMMSS
  4. 执行后自检:City 非空率 + Top 20 市聚合

执行步骤

ALTER TABLE NationalHistoricalandCulturalSites ADD COLUMN City TEXT;
-- 批量 UPDATE 5052 条
CREATE INDEX idx_city ON NationalHistoricalandCulturalSites(City);

落库后 Top 20 城市

排名 城市 数量
1 北京市 132
2 运城市 102
3 郑州市 81
4 长治市 73
5 晋城市 72
6 晋中市 69
7 保定市 66
8 重庆市 64
9 苏州市 61
10 渭南市 60
11-20 西安 / 南京 / 临汾 / 洛阳 / 赤峰 / 黄山 / 张家口 / 杭州 / 泉州 / 上海 40~57

山西省三市(运城 / 长治 / 晋城)同时进入 Top 6,印证"中国古建看山西"的常识,数据质量可信。


十、前端同步(方案 B 完整联动)

数据库层到位后,Flask 侧同步升级:

文件 改动
routes.py overview()city_stats Top 20;list_sites()city 查询参数 + 联动下拉 city_optionsshow_site() 同步;搜索 LIKE 条件追加 City 字段
list.html 新增"市"列;"按市"下拉(选定省后联动出现,带计数);per_page / 分页 / 详情链接全部携带 city 参数
index.html 新增"按城市分布 Top 20"柱状图(绿色渐变,点击跳转按市筛选);搜索框 placeholder 补"城市"

搜索能力质变

修复前搜"韶关"仅 3 条(Location 字面含"韶关市"),修复后 4 条——新增"满堂围"(Location=广东省始兴县,City=韶关市)。这是 City 字段既服务展示也打通搜索的闭环价值。


十一、复盘与方法论

成功要点

  1. 先诊断再修复:未着急改数据,先扫描出 388 条 C 类,归纳 6 种模式,对症下药
  2. 基础设施先行:独立的 admin_division.db 可复用于其他子系统(MeteoGeo、HisWLD 等)
  3. 规则优先级分层:8 层规则按优先级排列,高置信度的特殊处理优先
  4. 两步确认落库:预览 + 备份 + 手动 yes,零风险

数据治理启示


十二、相关资源

代码脚本(位于附件目录)

数据产物

数据库文件(位于 d:\BYP\Project\BYP\BYPsNotes\db\

外部参考