Published on

一个验证码问题,怎么变成了一个开源项目

Authors
  • avatar
    Name
    Pony Ma
    Twitter

最开始,我只是想修一个爬虫。

它要做的事情不复杂。打开详情页,处理 VAPTCHA 轨迹验证码,等 PDF 出来,上传,然后处理下一条。

麻烦在于,本地大部分时候能跑,线上却经常白屏。代码一样,代理供应商一样,启动参数也一样,本地和线上都用 headless。连续试了很多次,结果仍然很稳定,本地能拿到数据,线上基本拿不到。

这类问题最难受的地方,是每个猜测听起来都合理。代理质量、headless、浏览器指纹、验证码识别服务,挨个都像原因。可日志一条条对下来,又没有哪一个能解释全部现象。

从 Patchright 换到 Camoufox

后来我单独写了一个 Camoufox 版本。没有继承旧爬虫,只参考原来的取任务、代理、PDF 上传和数据推送逻辑。目的很简单,先把浏览器环境换掉,看看问题到底还在不在。

Camoufox 确实解决了一部分浏览器环境问题,但页面仍然会出错。有时验证码在 iframe 里,有时会弹出一个小窗口。有时图片框已经出现,里面的题目还没加载完,程序却已经截图识别了。

还有一种情况更直接。页面顶部提示“配置加载失败”或者“验证失败”,下面一片空白。这个 Context 继续刷新也很难恢复,只能记下错误提示,关掉 Context,重新创建一个。

做到这里,我才把一次任务的流程真正理顺。

创建浏览器上下文以后,先挂上 PDF 监听,再打开详情页。验证码出现在哪个页面,就去哪个页面处理,但 PDF 监听始终留在详情页。验证码图片稳定以后再截图、识别和绘制。任何一步超时,都要能看出具体卡在哪里。

如果 Context 已经异常,就直接换一个。代理还没过期,就放回代理池,不让一次页面失败浪费整条代理。

PDF 到手以后,别再等了

排查时有一条日志让我印象很深。PDF 明明已经捕获到了,程序却还卡在验证码流程里。

原来的代码总想等页面完整走完。验证码消失,页面加载成功,然后再处理 PDF。实际运行时,这几个事件没有固定顺序。PDF 可能先回来,验证码弹层还留在页面上。

后来我把 PDF 捕获设成当前任务的结束信号。只要拿到有效 PDF,正在进行的验证码等待、截图或者鼠标操作全部停止。程序马上上传文件、推送数据,然后进入下一条任务。

这个改动很小,却让整个流程顺了很多。爬虫想要的是 PDF,页面有没有走到一个漂亮的结束状态,并不重要。

轨迹又成了新的问题

流程能跑以后,剩下的问题集中到了鼠标轨迹上。

识别服务会返回一组坐标。最早的做法是让鼠标逐个经过这些点。画出来的形状基本正确,鼠标却会不停抖动,有时像在图上乱涂。验证码要求的是一条连续轨迹,三十个识别点并不代表鼠标必须做三十次僵硬的移动。

我录了一些人工绘制的数据,再回头看代码里的轨迹。人工移动会有快慢、停顿和小幅修正,转弯处也会自然减速。代码生成的轨迹虽然加了随机数,放到一起看仍然很像,同样的采样方式,同样的节奏,连停顿的位置都接近。

于是轨迹处理慢慢变成了一个独立问题。

输入是一条参考路径。输出是一组带时间的鼠标事件。生成的轨迹可以小幅平移,也允许每个点有一点偏差,但整体形状要保留。起点、终点和急转弯的位置收紧一些,其他地方可以自然变化。

同时还要守住边界。轨迹不能跑出验证码图片,偏移不能大到影响识别,所有事件的时间必须递增。随机也不能各玩各的,速度、节奏和修正习惯需要保持一定关联。

Guided Motion

写到后面,这部分代码已经和 VAPTCHA 没有太大关系了。它不需要知道任务从哪里来,也不关心代理、浏览器上下文和 PDF。它只负责把参考路径变成一条受约束、带时间的运动轨迹。

我把这部分重新整理成了 Guided Motion,并发布到了 PyPI

pip install guided-motion

使用时只需要提供参考点、运动边界和允许的偏差。

from guided_motion import Bounds, Constraints, MotionPlanner

plan = MotionPlanner(seed=42).plan(
    reference_points,
    bounds=Bounds(x=0, y=0, width=420, height=280),
    constraints=Constraints(
        max_deviation=6.0,
        endpoint_deviation=1.5,
    ),
)

它不负责识别验证码,也不保证自动化不会被检测。它能做的事情很具体,保留参考路径的形状,在明确的偏差和边界内生成一条完整的时空轨迹。

这个项目没有什么宏大的起点。它原本只是一个爬虫里越写越长的鼠标函数。等我把页面加载、验证码处理和 PDF 流程拆开以后,才发现这段代码已经可以单独拿出来了。