既然决定回归,那就重写那套烂了半年的自动化框架。今天下午,团队里那个刚毕业的小孩跑过来,怯生生地说“哥,那个爬虫又挂了,客户在群里骂”。我看着他屏幕上一片红的日志,心里那股邪火“噌”就上来了。不是冲他,是冲我自己。这套东西是我去年为了抢一个电商数据项目,通宵三天用Python requests和正则表达式硬怼出来的,当时觉得能跑就行,结果成了团队这半年的技术债噩梦。每次挂,都是临时打补丁,代码已经臃肿得像一坨发霉的面团。
不能再这样了。交付压力再大,也得把地基重新打一遍。我让小孩先去安抚客户,自己关上门,把PyCharm开了。第一件事,不是写代码,是画图。把整个数据采集流程,从登录、获取列表页、解析详情、到入库和异常重试,全在白板上拆成模块。每个模块之间必须用队列解耦,绝不能再像以前那样,一个函数从头写到尾,一处报错全盘崩溃。
核心痛点就三个:网络波动、网站反爬升级、数据格式突变。针对网络,我把所有HTTP请求全部包进了自定义的RetrySession类里,不是简单sleep重试,而是根据HTTP状态码和异常类型分层处理。比如遇到429频率限制,就启用指数退避算法;遇到连接超时,先切备用代理IP,再重试。这里我用了tenacity库,把重试策略配置化,最大重试次数、等待时间、重试条件(比如只对ConnectionError和Timeout重试)写得清清楚楚。
解析环节是以前的重灾区,全靠正则和字符串切片,DOM结构一变就崩。这次我强制全部改用lxml和XPath,虽然写起来麻烦点,但容错性好太多了。每个解析函数里,我都加了三层防御:先用XPath尝试定位元素,如果返回空列表,立刻记录警告日志,并尝试备用XPath方案(我提前为每个关键字段都准备了至少两条定位路径);还加了try-except块,专门捕获XPath语法错误和索引越界,一旦捕获,不是直接抛异常,而是把当前HTML片段快照保存到本地指定目录,文件名带上时间戳和URL,方便后续离线分析反爬策略。这相当于给脚本装了个黑匣子。
光有异常处理不够,还得有眼睛。我单独写了个monitor模块,用logging模块把不同级别的日志(DEBUG、INFO、WARNING、ERROR)分文件输出。ERROR级别的日志一旦产生,除了落盘,还会通过配置好的钉钉Webhook机器人,把关键信息(报错位置、时间、触发URL)实时推到项目告警群里。我还加了个心跳机制,脚本每处理完一个批次的任务,就向一个健康检查端点发送一次“存活信号”;如果超过预设时间没收到信号,监控脚本就会判定主程序可能已僵死,自动重启它,并推送一条“进程僵死重启”的告警。
搞这些基础设施,花了我整整两天,一行业务数据都没爬。团队里有人嘀咕,说客户催得紧,是不是先应付过去。我没理会。我知道,那种“应付”就是往未来的自己身上捅刀。这种“工匠精神”不是矫情,是血泪教训换来的:当你半夜三点被电话吵醒,迷迷糊糊连服务器,面对一堆毫无头绪的报错时,你就会明白,前期多花几个小时构建的健壮性,不是成本,是救命的氧气。代码不仅是给机器跑的,更是给未来那个焦头烂额的自己看的。
框架搭好,我把核心采集循环重新写了一遍。用了concurrent.futures的ThreadPoolExecutor做并发,但严格控制了最大线程数,避免把目标网站搞挂。每个worker线程里,都套用了那套完整的异常处理和日志体系。敲下最后一行,跑起测试用例。看着日志文件里规整的输出,告警群里一片寂静(没有消息就是最好的消息),那种久违的、对代码的掌控感,稍微回来了一点。我知道明天还会有新的坑,但至少,船底的漏洞被我焊上了几块扎实的钢板。














