BYP国保数据库已完整收录第四批至第八批全国重点文物保护单位共 5052 条数据,用户可按批次筛选查看。
但在实际使用中发现问题:
| 批次 | 公布年份 | 调整数量 | 问题 |
|---|---|---|---|
| 第四批 | 1996年 | 12处 | 查阅第六批列表时,看不到该批合并的106处项目 |
| 第五批 | 2001年 | 23处 | 用户需要了解某批次是否有合并/归入项目 |
| 第六批 | 2006年 | 106处 | 调整数据与列表数据分离,查阅不便 |
| 第七批 | 2013年 | 47处 | |
| 第八批 | 2019年 | 50处 |
直接后果:用户在列表页查看"第六批"国保时,无法同时看到该批次的106处合并项目,需要额外翻阅文档,信息割裂。
核心需求:在国保列表页按批次展示对应的合并/归入项目,数据与源文档自动同步。
采用已整理的Markdown文档:
extern/notes/文物考古/全国重点文物保护单位/国保调整.md分三阶段推进,每阶段独立验证:
阶段 A 数据提取:国保调整.md → merge_adjustments.json(Python脚本自动解析)
阶段 B 后端集成:routes.py 读取JSON → 传递到模板
阶段 C 前端展示:list.html 可折叠卡片 + 响应式表格
最初手动创建 merge_adjustments.json,录入部分示例数据测试:
{
"第六批": {
"year": "2006",
"type": "合并",
"count": 106,
"data": [
{"name": "金界壕遗址", "time": "金", "location": "河北省丰宁满族自治县", "note": "归入..."},
// ... 仅录入16条示例
]
}
}
第一个500错误:访问页面时报 TypeError: 'builtin_function_or_method' object is not iterable
根因分析:
- JSON中使用 items 作为数据列表的key
- 在Jinja2模板中 merge_data.items 被解析为Python字典的内置方法 .items()
- 导致 for item in merge_data.items 尝试迭代方法对象而非列表数据
解决方案:
- 将 items 改为 data,避免与Python内置方法名冲突
- 同步更新模板中所有 merge_data.items → merge_data.data
第二个500错误:JSON文件存在但解析失败
根因分析:
JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig)
utf-8 编码无法处理BOM解决方案:
# 修改前
with open(json_path, 'r', encoding='utf-8') as f:
# 修改后
with open(json_path, 'r', encoding='utf-8-sig') as f:
为避免手动录入238条数据,编写Python脚本自动从Markdown提取:
app/national_sites/提取国保调整数据.pydef parse_markdown_to_json(md_path):
"""从国保调整.md提取数据并生成JSON"""
# 第四批、第五批、第六批:5列表格
# | 序号 | 名称 | 时代 | 地址 | 备注 |
rows = re.findall(r'\|\s*\d+\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|', table)
# 第七批:7列表格(需跳过类别和编号列)
# | 序号 | 类别 | 编号 | 名称 | 时代 | 地址 | 备注 |
rows = re.findall(r'\|\s*\d+\s*\|\s*.*?\s*\|\s*.*?\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|', table)
# 第八批:6列表格(需跳过编号列,分5个子类别)
# | 序号 | 编号 | 名称 | 时代 | 地址 | 备注 |
rows = re.findall(r'\|\s*\d+\s*\|\s*.*?\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|', table)
国保调整数据提取工具
创建日期:2026年5月7日
============================================================
源文件:...国保调整.md
输出文件:...merge_adjustments.json
开始从Markdown提取数据...
✓ JSON文件已生成
数据统计:
✓ 第四批: 12 处,实际提取 12 条
✓ 第五批: 23 处,实际提取 23 条
✓ 第六批: 106 处,实际提取 106 条
✓ 第七批: 47 处,实际提取 47 条
✓ 第八批: 50 处,实际提取 50 条
总计:238 条数据
============================================================
✓ 所有批次数据提取完整!
{
"第四批": {
"year": "1996",
"type": "归入",
"count": 12,
"data": [
{
"name": "镇朔楼",
"time": "明",
"location": "河北省张家口市",
"note": "归入清远楼"
}
// ... 共12条
]
},
// 第五批至第八批...
}
在 list_sites() 函数中增加JSON读取逻辑:
import json
from pathlib import Path
def list_sites():
# ... 原有查询逻辑 ...
# 读取国保调整数据(如果有批次筛选)
merge_data = None
if batch:
try:
json_path = Path(__file__).parent / 'merge_adjustments.json'
if json_path.exists():
with open(json_path, 'r', encoding='utf-8-sig') as f:
all_adjustments = json.load(f)
merge_data = all_adjustments.get(batch)
except Exception:
merge_data = None # 读取失败不影响正常显示
return render_template('national_sites/list.html',
# ... 原有参数 ...
merge_data=merge_data)
batch 参数时读取JSONutf-8-sig 处理BOMPath(__file__).parent 确保容器内外路径一致将调整信息从页面底部移至列表前面(筛选条件之后、数据表格之前),更符合用户查阅习惯。
采用默认关闭、点击展开的交互方式:
{% if merge_data %}
<div class="merge-adjustments">
<!-- 标题栏(可点击) -->
<div class="merge-header" onclick="toggleMergeData()">
<h3>📋 {{ batch }}合并/归入项目</h3>
<div class="toggle-icon">▼</div>
</div>
<!-- 内容区(默认隐藏) -->
<div id="mergeContent" style="display: none;">
<!-- 调整说明 + 数据表格 -->
</div>
</div>
<script>
function toggleMergeData() {
var content = document.getElementById('mergeContent');
var icon = document.querySelector('.toggle-icon');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
} else {
content.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
}
}
</script>
{% endif %}
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
用户反馈:"背景大红大紫太难看了!"
background: #f5f7fa;
color: #2c3e50;
border: 1px solid #e0e0e0;
符合整体页面风格,视觉舒适。
sites-table 样式类(与下方国保列表完全一致)table-wrapper 包裹(支持移动端横向滚动)<div class="table-wrapper">
<table class="sites-table">
<thead>
<tr>
<th class="num-col">序号</th>
<th>名称</th>
<th class="time-col">时代</th>
<th class="location-col">地址</th>
<th>归入/合并说明</th>
</tr>
</thead>
<tbody>
{% for item in merge_data.data %}
<tr>
<td class="num-col">{{ loop.index }}</td>
<td class="site-name">{{ item.name }}</td>
<td class="time-col">{{ item.time }}</td>
<td class="location-col">{{ item.location }}</td>
<td class="note-col">{{ item.note }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
移动端效果:表格可横向滚动,不影响主列表浏览。
| 批次 | 访问URL | 显示效果 | 数据完整性 |
|---|---|---|---|
| 第四批 | ?batch=第四批 |
可折叠卡片 + 12条表格 | ✓ 100% |
| 第五批 | ?batch=第五批 |
可折叠卡片 + 23条表格 | ✓ 100% |
| 第六批 | ?batch=第六批 |
可折叠卡片 + 106条表格 | ✓ 100% |
| 第七批 | ?batch=第七批 |
可折叠卡片 + 47条表格 | ✓ 100% |
| 第八批 | ?batch=第八批 |
可折叠卡片 + 50条表格 | ✓ 100% |
现象:TypeError: 'builtin_function_or_method' object is not iterable
根因:JSON字段名 items 与Python字典方法 .items() 冲突
解决:字段名改为 data,避免使用Python内置方法名
启示:Jinja2模板中应避免使用Python字典方法名(items、keys、values等)作为JSON字段名
现象:JSONDecodeError: Unexpected UTF-8 BOM
根因:Windows保存的JSON文件带BOM标记,Python默认utf-8无法处理
解决:使用 utf-8-sig 编码读取(自动跳过BOM)
启示:跨平台项目需特别注意文件编码差异,容器环境更严格
现象:第四批5列、第七批7列、第八批6列,列数不统一
解决:编写多套正则表达式,按批次特征提取对应列
启示:Markdown解析需考虑格式多样性,不能假设所有表格结构一致
现象:500错误(模板解析失败)
根因:在HTML style 属性中直接使用 {% if %} 语法
解决:改用CSS类方式(class="odd-row"),避免内联模板语法
启示:Jinja2模板语法不应嵌入HTML属性值中
app/national_sites/提取国保调整数据.py — Markdown转JSON自动化脚本app/national_sites/merge_adjustments.json — 国保调整数据(238条)app/national_sites/routes.py — 后端路由(含JSON读取逻辑)app/national_sites/templates/national_sites/list.html — 前端模板(可折叠卡片)national_sites.db — 国保主数据库(5052条)admin_division.db — 行政区划基础库(3209条)| 日期 | 版本 | 说明 |
|---|---|---|
| 2026-05-07 | v1.0 | 初始版本,完成核心功能开发 |
| 2026-05-07 | v1.1 | 修复JSON字段名冲突和BOM编码问题 |
| 2026-05-07 | v1.2 | 优化前端样式,实现可折叠卡片和响应式表格 |
| 2026-05-07 | v1.3 | 编写自动化提取脚本,完成238条数据同步 |
总结:通过三阶段开发,成功实现了国保列表页的调整数据展示功能。从问题诊断、方案设计、技术攻关到用户反馈迭代,完整实践了数据治理与前端开发的最佳实践。自动化工具的引入大幅降低了后续维护成本,为知识库系统的持续优化奠定了坚实基础。