1. 从零搭建历史数据获取通道的完整思路拿到“获得武汉疫情历史数据紧急上线”这个需求时我第一反应不是打开编辑器写代码而是先问了自己三个问题数据从哪来、要什么粒度、上线后谁来维护。这三个问题决定了后面所有技术选型的走向。很多同行一接到“紧急上线”四个字就条件反射地开始堆代码结果往往是接口跑通了、数据也拉下来了但字段对不上、时间戳错乱、历史数据缺段最后返工的时间比开发还长。我踩过这个坑所以这次先把思路理清楚再动手。这个需求的核心价值在于把分散在不同渠道、不同格式的历史公共卫生事件数据整合成一份结构统一、时间连续、可直接用于分析和展示的数据集。它解决的是“数据散、格式乱、无法直接使用”的问题。适合谁来参考我认为三类人最需要一是需要快速搭建数据管道的数据工程师二是要做数据可视化或趋势分析的前端与分析师三是负责应急响应系统搭建的技术负责人。哪怕你之前没接触过公共卫生领域的数据只要理解“采集—清洗—存储—输出”这条链路就能把方法迁移过去。为什么强调“历史数据”而不是实时数据因为历史数据的难点不在采集频率而在时间跨度的完整性和字段口径的一致性。实时数据你只要保证接口通、频率稳就行历史数据你要面对的是不同时期发布口径变了、字段名改了、甚至某些日期根本没有记录。这些坑不提前想清楚上线当天就会被打回。所以我的整体思路是先确定数据源的可获取性再设计一个能容纳“缺失”和“变更”的存储结构最后才是写采集和清洗逻辑。顺序反了后面全是补丁。在工具选型上我倾向于用 Python 做采集和清洗原因很直接处理时间序列、缺失值填充、字段映射这些事pandas 生态最成熟社区里能搜到的解决方案最多紧急上线时“能搜到答案”比“性能极致”重要得多。存储层用关系型数据库比如 PostgreSQL而不是直接扔 CSV是因为历史数据需要频繁按时间范围查询和聚合关系型数据库的索引和 SQL 能力能省掉大量手写过滤逻辑。输出层则根据消费方决定给前端用就出 JSON给分析师用就出 CSV 或直接连库。这套组合不是唯一解但它是在“紧急”约束下容错率最高的方案。提示紧急上线不等于跳过设计。恰恰相反越是时间紧越要把数据模型定死否则后期改字段的成本会指数级上升。2. 数据源摸底与采集策略的取舍2.1 为什么不能只依赖单一数据源历史公共卫生事件数据有一个很现实的问题没有任何一个单一来源能覆盖全部时间跨度且字段完整。官方发布渠道可能只有汇总数据媒体报道可能只有某几天的明细社区整理的数据又可能存在口径不一致。我实际处理时的做法是至少准备两个独立来源做交叉验证。一个作为主源提供结构化的时间序列另一个作为辅源用来补全主源缺失的日期或校验异常值。这里有个经验不要试图找到一个“完美数据源”再开始。紧急上线场景下正确的做法是先用主源把主干数据拉下来跑通全链路然后再用辅源做增量补全。我见过有同行为了等一个“更全”的数据源硬生生拖了三天结果主源接口变了之前写的解析逻辑全废。先跑通、再优化这是紧急项目的铁律。2.2 采集方式的选择接口、页面还是文件数据获取方式无非三种调接口、抓页面、下文件。优先级应该是有稳定接口就用接口没有接口但有结构化页面就用页面解析两者都没有才考虑人工整理文件。接口的好处是字段明确、格式稳定、容易做增量页面解析的坏处是页面结构一变就得改代码维护成本高文件方式最不可靠因为版本管理和更新同步全靠人工。我在这个项目里遇到的情况是主源提供的是分页接口每页返回一个时间段的汇总数据。这种接口的坑在于分页参数和总页数不一定可靠有时候总页数字段是错的你按它循环会漏数据。我的处理方式是不信任总页数而是用“返回结果为空”作为终止条件同时记录每次请求的时间戳和页码方便出问题时回溯。这个细节看起来小但上线后如果发现数据缺了几天没有请求日志你根本查不出是漏请求还是源本身就没有。2.3 请求频率与重试机制的设计紧急上线不代表可以粗暴地高频请求。一方面高频请求容易被限流甚至封禁另一方面历史数据本身不会瞬间变化没必要抢那几秒。我的策略是单次请求间隔控制在合理范围遇到失败采用指数退避重试最多重试三次三次都失败就记录到失败队列等主流程跑完再统一处理。重试机制里有一个容易被忽略的点区分可重试错误和不可重试错误。网络超时、服务端 5xx 可以重试参数错误、资源不存在4xx重试多少次都没用只会浪费时间。我在代码里用状态码做了分流4xx 直接跳过并记录5xx 才进入重试逻辑。这个判断省下来的时间在紧急上线时非常可观。import time import requests def fetch_with_retry(url, params, max_retries3): for attempt in range(max_retries): try: resp requests.get(url, paramsparams, timeout10) if resp.status_code 200: return resp.json() elif 400 resp.status_code 500: # 客户端错误重试无意义 return None else: time.sleep(2 ** attempt) except requests.exceptions.RequestException: time.sleep(2 ** attempt) return None上面这段代码就是我当时用的简化版逻辑。注意timeout一定要设不设的话遇到慢响应会一直挂着整个采集流程卡死。2 ** attempt就是指数退避第一次等 1 秒第二次 2 秒第三次 4 秒给服务端喘息时间也给自己留出容错空间。3. 数据清洗中最容易翻车的几个环节3.1 时间字段的标准化没有想象中简单历史数据里最让人头疼的就是时间字段。我遇到过的情况包括有的源用“2020-01-01”有的用“2020/1/1”有的用时间戳还有的只给“1月1日”不带年份。如果不统一后面做时间序列分析时排序会乱、聚合会错。我的做法是所有时间字段进入清洗流程后第一步就强制转成 ISO 8601 格式YYYY-MM-DD转不了的记录单独打标不混入主数据集。这里有个细节时区问题。如果数据源没有明确标注时区默认按数据发布地的本地时间处理并在元数据里记录这个假设。不要小看这一点跨天聚合时差几个小时就可能导致某一天的数据被算到前一天或后一天。我在项目里专门加了一个字段记录“原始时间字符串”就是为了出问题时能追溯到底是解析错了还是源本身就有问题。3.2 缺失日期的处理补零、插值还是留空历史数据几乎不可能每一天都完整。缺失日期怎么处理直接决定了后续分析的准确性。我的原则是区分“真实为零”和“未记录”。如果源明确表示某天没有新增那就是零如果源只是没有那天的记录那就是缺失不能补零。把缺失当零处理会严重拉低趋势线的准确性。具体操作上我会先生成一个完整的日期序列作为基准然后把采集到的数据左连接到这个序列上。连接后为空的日期根据源的性质决定填零还是留空。如果留空在输出时用null而不是0让消费方自己决定怎么处理。这个区分在紧急上线时特别重要因为一旦上线后数据被引用再改口径就要通知所有下游成本极高。3.3 字段映射与口径变更的记录不同来源的字段名和含义可能不一样。比如同样是“新增人数”有的源叫new_cases有的叫newly_confirmed有的叫新增。清洗层必须做一层映射把所有来源统一到一套标准字段名上。但更重要的是记录口径变更。如果某个源在某个时间点之后改了统计口径你必须在数据里标记出来否则前后数据放在一起对比就是错的。我的做法是在标准字段之外额外维护一张“口径变更记录表”记录变更时间、变更内容、影响范围。这张表不直接参与分析但它是数据可信度的依据。上线后如果有人质疑某段时间的数据异常你可以快速定位是源的问题还是清洗的问题。常见问题错误做法正确做法时间格式不统一直接字符串排序统一转 ISO 8601 后再排序缺失日期全部补零区分真实零和未记录字段名不一致在每个消费方单独映射清洗层统一映射并记录口径变更忽略直接拼接标记变更点分段处理4. 存储结构设计与上线前的压力测试4.1 表结构怎么定才能扛住后续查询存储结构决定了上线后查询的快慢和扩展的难易。我用的是一张主表加一张元数据表的结构。主表存核心时间序列数据字段包括日期、地区标识、指标名、指标值、数据来源、采集时间。元数据表存数据源信息、口径变更记录、采集批次日志。为什么拆两张表因为主表会被高频查询字段越少索引越快元数据表查询频率低但需要保留完整历史拆开后互不影响。主表的索引我建了两个一个是日期加地区标识的联合索引用于按时间范围和地区查询另一个是采集时间的索引用于排查某次采集是否成功。索引不是越多越好每多一个索引就多一份写入开销。紧急上线时数据量可能不大但你要为后续数据增长留出余地所以索引策略要提前想清楚。4.2 上线前的数据完整性校验清单上线前必须做一轮完整性校验不能只看“接口通了”就发布。我的校验清单包括日期范围是否覆盖预期区间、每天是否有记录、关键字段是否有空值、数值是否在合理范围内、不同来源的交叉验证是否一致。这些检查写成脚本每次采集完自动跑一遍输出校验报告。其中“数值是否在合理范围内”这一条最容易被忽略。历史数据里偶尔会出现异常大的值可能是录入错误也可能是真实峰值。我的处理是不自动修正但标记出来让人工确认。自动修正的风险在于你可能把真实信号当噪声抹掉了这在公共卫生数据分析里是大忌。4.3 紧急上线后的监控与回滚方案上线不是终点。紧急上线的项目往往没有完善的监控但至少要有两个东西采集成功率监控和数据量突变告警。采集成功率低于阈值时通知负责人数据量突然翻倍或归零时也要告警因为这通常意味着源变了或者采集逻辑出问题了。回滚方案同样重要。我的做法是每次采集写入新批次数据时保留上一批次如果新批次校验不通过可以快速切回上一批次。这听起来有点笨但在紧急场景下能快速回滚比追求零错误更实际。你不可能保证第一次就完美但你可以保证出问题时影响可控。-- 按日期和地区查询的典型 SQL SELECT date, region, metric_name, metric_value FROM historical_data WHERE date BETWEEN 2020-01-01 AND 2020-03-31 AND region target_region ORDER BY date ASC;上面这条查询就是主表联合索引的典型使用场景。注意ORDER BY date能利用索引顺序避免额外排序。如果你的查询里经常出现按指标名过滤那可能还需要考虑把指标名加入索引但不要盲目加先看实际查询模式。5. 实操中积累的几条硬核经验5.1 不要在上线当天改数据模型这是我用血换来的教训。曾经有一次紧急上线采集和清洗都跑通了结果上线前两小时有人提出“能不能加一个字段”我想着加个字段而已就改了。结果那个字段在部分历史数据里不存在导致清洗逻辑报错整个上线推迟了半天。从那以后我定了一条规矩上线当天只修 bug不加功能、不改模型。任何新需求记下来上线稳定后再排期。5.2 日志要记到“能复现问题”的程度紧急项目出问题时你没有时间慢慢调试。日志必须记录请求的完整 URL 和参数、响应状态码、响应体的关键字段、处理到哪一步失败。我甚至会把失败记录的原始响应存一份到单独的文件里方便事后分析。日志级别用 INFO 记录正常流程用 ERROR 记录异常不要什么都打 DEBUG否则日志量大到没法看。5.3 给下游消费方留一份“数据说明书”数据上线后消费方会来问各种问题这个字段什么意思、为什么这天是空的、数值单位是什么。与其一个个回答不如提前写一份数据说明书包含字段定义、时间范围、缺失说明、口径变更记录。这份文档不需要多正式一个 Markdown 文件就够但能省掉大量沟通成本。我在项目里把说明书和数据一起发布消费方自助查阅效率提升非常明显。5.4 关于“紧急”的心态调整最后说一点心态上的经验。“紧急上线”四个字容易让人焦虑但焦虑不会让代码跑得更快。我的做法是把任务拆成最小可交付单元先保证主干数据能采、能存、能查再考虑补全和优化。主干跑通的那一刻压力就小了一半。剩下的时间用来做校验和文档而不是继续加功能。这个节奏控制比任何技术技巧都重要。数据获取这件事技术难度往往不是最高的难的是对数据本身的理解和对异常情况的预判。你把数据源摸透了、把缺失和变更想清楚了、把回滚方案准备好了上线就是水到渠成的事。反过来如果只盯着代码忽略了数据层面的复杂性那上线才是麻烦的开始。