CIA World Factbook 于 2026 年 2 月 4 日正式停更,这个持续半个多世纪的地缘情报数据库留下了 1990 至 2025 年共 36 个版本、281 个实体、超过 100 万条数据字段的完整历史。构建这样一个版本化归档系统的核心挑战在于:如何设计存储模型以支持时间旅行查询(Time-Travel Query),如何在数据格式十年十变的情况下保持字段级别的纵向可比性,以及如何对外提供高效的查询接口。本文从存储架构、查询设计、导出演进三个维度,拆解这一工程的实现要点。
版本化存储模型设计
该归档项目采用关系型数据库作为核心存储层,支持 SQL Server 与 SQLite 两种部署形态。SQLite 版本约 324 MB,内置 FTS5 全文搜索索引,是线上 Web 应用的生产底座;SQL Server 版本则面向 Power BI 等企业分析场景。两套系统的表结构完全一致,均由五张核心表构成,形成了典型的星型模式 + 时间维度扩展。
第一张表是 MasterCountries(主实体表),记录 281 个 canonical 实体的元数据。这张表是静态的、仅包含一次写入的基准数据,涵盖 ISO 代码、FIPS 代码、实体类型( sovereign、territory、dissolved 等九种分类)、COCOM(美国作战司令部)区域归属等不变信息。实体类型分类通过 classify_entities.py 脚本自动完成,依据是 Dependency Status 与 Government Type 两个字段,辅以 283 行硬编码的边缘情况处理规则。
第二张表是 Countries(国家年份表),记录 9,500 条国家 - 年份组合记录。每条记录对应某个实体在某一年份的存在状态,包含该年份的元数据字段。这张表解决了「某实体在某年份是否存在」的问题 —— 例如 Netherlands Antilles(荷属安的列斯)于 2010 年解散,Serbia and Montenegro(塞尔维亚和黑山)于 2006 年解体,这些历史实体在其存续年份有记录,解散后不再出现。
第三张表是 Categories(类别表),记录 83,599 条类别记录。每个国家 - 年份组合下划分若干类别(如 Geography、People and Society、Government 等),类别表中存储类别名称与排序权重。
第四张表是 FieldNameMappings(字段名映射表),这是整个系统最关键的质量保障层。CIA 在 36 年间对字段名进行了大量重命名与格式调整,原始字段名变体达到 1,090 个,经过七层映射规则(identity、rename、dash_format、consolidation、country_specific、noise、manual)后收敛为 416 个 canonical 字段名。例如「GDP - real growth rate」与「Real GDP growth rate」被映射为同一条目,使得跨年份的时间序列分析成为可能。
第五张表是 CountryFields(字段值表),存储全部 1,061,522 条字段级数据记录。每条记录包含实体 ID、年份、类别 ID、字段名(原始与映射后两种)、字段值、字段顺序等。这是一个典型的时间序列宽表设计,每一行代表某个实体在某年份的某个字段取值。
从时间旅行的角度审视,这五张表构成了一套隐式版本向量。实体层面由 MasterCountries + Countries 联合表达(实体存在性 + 年份覆盖),字段层面由 CountryFields 的 (EntityID, Year) 双键表达(字段值的时间切片)。查询某国在某年的完整状态,本质上是一次双键 JOIN:先从 Countries 确认该实体在该年份存在,再从 CountryFields 拉取该 (EntityID, Year) 组合下的全部字段值。
时间旅行查询接口实现
时间旅行查询(Time-Travel Query)是指以某个历史时间点为锚点,返回该时间点对应的数据状态。该归档系统在前端提供了三类时间旅行能力。
第一类是 Field Time Series(字段时间序列),即选定一个国家 + 一个字段,绘制该字段在 36 年间的数值变化曲线。这背后的 SQL 查询逻辑是:以 CanonicalFieldName 为过滤条件,以 EntityID 为分组键,以 Year 为升序排列,提取每个年份的 FieldValue。由于字段名在 1990-2025 年间经历了多次重命名,查询层必须先通过 FieldNameMappings 将用户输入的字段名转换为对应的历史变体,再执行时序查询。官方提供的 sample_queries.sql 中包含了此类查询的完整示例。
第二类是 Change Detection(变化检测),即对比相邻年份之间某个国家所有字段的变化情况。实现逻辑是:对目标国家执行两次查询,分别获取 Year=N 和 Year=N-1 的字段集,然后逐字段比对值的变化。变化类型分为四类:新增字段(去年无今年有)、删除字段(去年有今年无)、值变更、值未变。UI 层以可视化方式高亮显示变化的字段,用户可以直观看到 CIA 在一年内对某国的数据做了哪些调整。
第三类是 Text Diff(文本对比),即对同一个字段在两个不同年份的文本值进行逐字对比。这特别适用于描述性字段(如 Government country summary、Economy overview 等长文本字段),用户可以选择任意两个年份,系统以 side-by-side 视图展示差异,变更部分用颜色高亮标注。
在技术实现层面,SQLite 的 FTS5 全文搜索索引为时间旅行查询提供了重要的加速能力。用户可以输入类似「population AND 1990..2000」的查询语法,搜索特定时间段内的关键词匹配结果。FTS5 的倒排索引结构使得这类时间范围 + 关键词的组合查询可以在毫秒级完成,这是支撑线上交互式探索的技术基础。
Web 应用层采用 FastAPI + Jinja2 模板构建,数据库连接池配置为默认参数(FastAPI 依赖的 SQLAlchemy asyncpg 默认池大小为 5、最大为 10),查询超时设置为 30 秒。对于时间序列等重查询,前端实现了分页加载与虚拟滚动,避免一次性返回 36 年数据导致的渲染卡顿。
导出格式的十年演进
CIA Factbook 本身在 36 年间经历了至少十次重大格式变更,这直接塑造了归档系统的 ETL 策略。1990-1999 年是纯文本时代,依赖 Project Gutenberg 的公开文本版本,但十年间出现了四种不同的格式约定:1990-1993 年使用 Country: Name 标题加缩进字段的布局,1994 年引入标记符(_@_、_*_、_#_),1996 年改用无标记的裸章节头,1999 年再次调整分隔符方案。每个变体都需要独立的正则解析器,这在 load_gutenberg_years.py(1,043 行)中体现为四个分支处理逻辑。
2000-2020 年是 HTML 时代,CIA 将 Factbook 迁移至网站发布。这 21 年间又经历了至少五次页面布局重构:2000 年的 <b>FieldName:</b> 内联格式,2004 年的 <td class="FieldLabel"> 表格布局,2008 年的 CollapsiblePanel JavaScript 折叠面板,2014 年的 <h2> 展开式章节结构,2017 年的 field-anchor <div> 嵌套结构。每一次改版都意味着解析器需要重写,build_archive.py(986 行)中的核心逻辑就是检测当年使用的是哪种 HTML 布局,然后调用对应的解析函数。2001 年尤为特殊 ——Wayback Machine 保存的 HTML zip 文件损坏,解析不得不回退到 Gutenberg 纯文本版本,成为唯一一年同时动用两套解析 pipeline 的情况。
2021-2025 年进入 JSON 时代,数据来源切换为 GitHub 上的 factbook/cache.factbook.json 仓库。该仓库以 git commit 为版本载体,每个年份对应一个年末快照。reload_json_years.py(413 行)直接检出对应 commit,解析 JSON 结构后入库。这一时期的格式最稳定,但需要处理 JSON 字段中嵌入的 HTML 标签(通过 strip_tags 处理)。
从导出格式的角度看,用户可通过前端导出为 CSV、Excel、PDF 三种格式。CSV 导出采用 UTF-8 编码,字段值中的逗号与换行符做双引号转义处理;Excel 格式使用 openpyxl 库生成,支持多工作表(每个类别一个 sheet);PDF 格式则通过 weasyprint 或类似库将 HTML 渲染为 PDF,保留基本的样式与分页。这三种导出格式的共同特点是:以 (EntityID, Year) 为单位打包,即一次导出一个国家 - 年份组合的完整数据,而非全库导出。
落地参数与工程要点
若要复现或扩展此类版本化归档系统,以下参数与设计决策值得关注。数据库选型方面,SQLite 适合中小规模(<50 万条时间序列记录)且需要独立分发的场景,SQL Server 适合大规模(> 100 万条)且需要 Power BI 集成的企业环境。两种数据库的表结构设计应保持兼容,以便后期迁移。
时间维度建模方面,推荐采用 (EntityID, Year) 双键作为事实表的主键组成部分,而非使用单独的 VersionID 或 Timestamp 字段。这种设计的优势在于:查询某时间点的状态只需一次等值 JOIN,无需范围扫描;历史状态查询的 SQL 语义清晰(WHERE EntityID = X AND Year = Y)。若需要更细粒度的时间戳(如月份级别),可在事实表中增加 Date 字段,并在其上建立索引。
字段名标准化是纵向可比性的前提。建议参考该项目的七层映射规则构建映射表,并在 ETL 流程中同步更新映射表,而非在查询层临时处理。映射表的维护是持续性工作 —— 每当 CIA 发布新版本,ETL 脚本应自动检测新增字段名,由人工审核后加入映射表。
前端时间旅行功能的性能瓶颈通常在数据库查询而非渲染层。建议对 (EntityID, Year, CanonicalFieldName) 三元组建立联合索引,确保时间序列查询走索引扫描而非全表扫描。SQLite 中可使用 EXPLAIN QUERY PLAN 验证索引命中情况。
版本化归档系统的核心价值在于支持「回到过去」的查询能力 —— 无论是分析某个国家在特定年份的经济数据,还是追踪某个指标在数十年间的演变趋势,其前提都是存储模型能够忠实地记录每一年的状态,并在查询时准确定位到目标时间点。该项目以五张表 + 字段映射层的组合,交出了一份值得参考的答卷。
参考资料
- CIA World Factbook Archive 官方站点与 GitHub 仓库:https://github.com/MilkMp/CIA-World-Factbooks-Archive-1990-2025