双 12 没买显卡,我买了一大箱电解质水和对乙酰氨基酚。因为我知道,接下来一周,我的身体又要为我的代码逻辑漏洞买单了。
上周接了个体育健身 APP 的数据清洗外包,对方要竞品在各大应用商店的用户评论,做情感分析和关键词提取。活儿不复杂,爬虫嘛,老本行。我用了异步 aiohttp,配了代理池,目标站的反爬也不算严,就是有个恶心人的地方:它的分页不是靠 URL 参数,是靠一个动态生成的 `data-next-page-token`,藏在上一页响应的 JSON 里。我写了个递归函数去抓,测试了前 50 页,一切正常,数据哗哗地进 MongoDB,我就挂着脚本睡觉去了。
醒来一看,数据库里多了 80 万条“评论”。心里刚飘过一丝窃喜,随手点开几条就凉了半截。从第 51 页开始,后面的数据全乱了,用户 ID 和评论内容对不上,时间戳全是 1970 年。问题就出在那个 `token` 上。我的递归函数里,处理响应的逻辑是:先解析 JSON,提取数据列表入库,然后提取 `next_token`,如果 `token` 不为空,就带着它发起下一次请求。但我犯了个低级错误——我没验证数据列表是否为空。目标站那个 API 有个特性,当页码超出实际范围,它不会返回错误,而是返回一个 HTTP 200,里面的 `data_list` 是个空数组,但 `next_token` 居然他妈的不是 `null`,而是一个固定的无效字符串!我的脚本就拿着这个无效的 `token`,乐此不疲地一遍遍请求,对方也一遍遍返回空数组,而我的入库函数照单全收,把空数组里不存在的字段(或者默认值)全插进去了。
80 万条垃圾数据。甲方明天就要初步结果。删除重跑的时间不够了,代理 IP 的余额也见底了。那一刻不是愤怒,是生理性的反胃,太阳穴突突地跳。我灌了半瓶电解质水,吞了颗止痛药,知道今晚又得通宵。但这次我没立刻开始删库重写。我逼自己坐下来,先把所有日志翻了一遍。
教训太贵了,必须刻进骨头里。我重新设计了这个采集任务的纠错逻辑,核心就三条,但每一条都是用真金白银和时间买来的。第一,验证前置。在任何解析和数据入库操作之前,必须对响应体进行有效性校验。不仅是 HTTP 状态码 200,还要检查关键字段是否存在、数据类型是否正确、数组是否非空。我写了个校验函数,专门对付这种“优雅的失败”。第二,设置双重终止条件。不能只依赖 `next_token`。必须同时记录当前页码(哪怕它是推算的),并与一个设定的最大安全页数比较。另外,如果连续 N 次(比如 3 次)获取到的数据列表为空,立即抛出异常并终止任务,而不是傻乎乎地继续。第三,引入中间状态和断点续传。把每次成功抓取后的 `token` 和当前页码持久化到文件或一个小型数据库里。万一脚本崩溃或需要手动干预,可以从上次有效的位置继续,而不是从头再来,或者更糟,从错误的位置继续污染数据。
搞健身教练这半年,我老跟学员说“动作质量大于数量”,“宁轻勿假”。代码也一样。以前总想着快,堆线程、上异步,以为并发数上去就能碾压一切。现在才明白,对于数据管道,“鲁棒性”才是唯一的 KPI。你爬得再快,数据是脏的、错的,那就是负资产,清理成本比获取成本高十倍。这次我花了整整十个小时来清洗和修复那 80 万条记录,重写脚本只用了两小时。慢就是快,这话真他妈是血泪换来的。
窗外的天又快亮了。手边是对乙酰氨基酚的盒子,和空了的电解质水瓶。身体在报警,但脑子异常清醒。在这个超级个体阶段,每一次技术债,最终偿还的都是自己的健康和时间。显卡可以等等,但代码里的逻辑漏洞,一刻也不能等。














