跳到主要内容

为什么我们放弃了用无头浏览器自爬 Turnitin

一篇复盘 —— 从自爬到账号池+失败分类器的三个阶段,以及每阶段踩过的坑。

技术反爬Playwright

半年前,我们内部有一个很朴素的想法:既然 Turnitin 官方 Similarity Report API 只对签约机构开放,那就用 Playwright 写个无头浏览器脚本,登录、提交、下载报告,做成一个内部工具。

那时候团队觉得这个东西大概一周能搞定。事实证明,它花了我们整整三个月,最后还不得不推倒重写。本文复盘我们在这期间踩过的三类大坑。

阶段一:朴素爬虫(一周写完,一周挂掉)

最早的实现非常”直男”:

await page.goto('https://www.turnitin.com/login');
await page.fill('input[name="email"]', email);
await page.fill('input[name="password"]', password);
await page.click('button[type="submit"]');

第一次运行时它工作得很好。上线后的第三天,所有请求开始返回一个奇怪的 Cloudflare Challenge 页。我们加了常见的 stealth 插件,熬了两天把登录路径走通了。然后第五天,登录之后的每个页面都开始需要重新过 JS challenge。

结论:光是伪装成人类还不够——我们需要真实的浏览器指纹

阶段二:真实指纹 + 代理池(两个月的复杂度爆炸)

我们切到了 Playwright 的 persistent context,给每个账号一个独立的 user_data_dir。这意味着:

  1. Chrome 会记住 localStorage、IndexedDB、Cookie、插件状态
  2. Canvas/WebGL 指纹在同一 profile 里是一致的
  3. 登录一次之后的 session 能保持好几天

这部分效果很好。但同时带来了新问题:

问题 1:Profile 锁

Chrome 同一个 user_data_dir 只能一个进程打开。我们原本设计的并发 pool 一下子退化成了串行,吞吐量掉到了 1/N。

修复:把 profile 目录做成 租约制 —— 每个 job 启动前从 pool 里”借”一个 profile,执行完归还。如果 crash 了 TTL 过期自动释放。

问题 2:代理粘性

每个 profile 必须绑定一个稳定出口 IP。如果这次从美国东岸登录、下次从新加坡出口请求,Turnitin 的风控会立刻要求重新验证。

修复:代理池做成了 profile × proxy 的绑定关系,而不是独立池。admin-web 上每个账号可以手动指定代理,也可以进默认 resolver。

问题 3:上游限流

忙的时候 Turnitin 会返回 429 或一个带验证码的页面。我们最初简单地重试,结果两个小时之内整个账号池被标记冷却。

修复:做了一个 FailureClassifier,把错误归到 turnitin_blocked / login_failed / upload_failed / report_timeout 等结构化码。

  • turnitin_blocked → 账号立即进冷却池,N 小时后再出来
  • login_failed → 尝试一次密码刷新,还不行就 disable 账号
  • report_timeout → 从 checkpoint 恢复,不重新上传

失败分类器是整个项目里最值得的一个设计投资。它把”我们系统哪里病了”从模糊的体感变成了可查询的数据。

阶段三:重新架构成”准 SaaS”

当我们发现 3-4 家同行客户都在问”能不能给我也来一份”之后,我们意识到这东西可能值得做成产品。为此把系统重构了一遍:

  • 多租户:从单机单账号改成多客户共享账号池,加上 API Key 限流
  • 两阶段提交:登录+提交在 browser phase,报告轮询在 session phase。这两阶段的 checkpoint 允许容器重启不丢状态
  • 结算系统:引入 quota_grants 作为配额的唯一来源,兑换码/订阅/支付都往同一张表写
  • 失败退款:所有平台侧失败都自动调用 QuotaService.refund,客户无感知

重构完成上线后,我们的失败率稳定在 2% 左右,大部分是上游真 down。

给后来者的几个建议

  1. 不要假设你能骗过 Cloudflare。你会败得很惨。带上真实浏览器 profile,把钱花在好代理上。
  2. 尽早做失败分类器。在你还只有 3 种失败的时候做好它,到 30 种的时候你会感谢自己。
  3. 两阶段提交是救命稻草。容器 OOM、网络抖动、工作节点升级,只要 checkpoint 还在,就不用重跑几十秒钟的登录。
  4. 账号池是 profile 池,不是账号池。profile 状态变质是最常见的隐性故障,监控它。

如果你也在做类似的事情,欢迎发邮件到 support@njbejm.cn 聊聊。