必须用真实浏览器自动化完成首次登录和短信接收,因网站依赖JavaScript渲染、设备指纹等前端校验;验证码需通过API(如Twilio/云片)预置通道接收并注入,再移交登录态至requests时须逐项设置Cookie属性并同步UA等头部信息。

不能“安全绕过”短信验证码二次验证——这不是技术问题,而是设计约束。真正可行的路径是:把验证码环节从自动化流程中剥离,交由可控通道接收并注入,同时确保整个会话上下文不被网站识别为异常。
为什么 requests + 手动填验证码行不通
短信验证码本质是服务端对「人机分离」的强制确认:它要求操作者能访问独立通信信道(手机),且该信道与 HTTP 请求发起环境物理隔离。用 requests 模拟 POST 提交 captcha 字段,前提是验证码已知;而验证码生成、发送、过期逻辑全在服务端控制,客户端无权读取或预测。
常见错误现象包括:
- 提交后返回
{"code":400,"msg":"验证码错误或已失效"} - 请求被 302 重定向到新验证码页,形成死循环
- IP 被临时加入风控队列,后续请求直接触发滑块验证
必须用浏览器自动化完成首次登录和短信接收
只有真实浏览器环境才能满足网站对「用户行为一致性」的要求:JavaScript 渲染、事件触发、localStorage 写入、证书指纹、Canvas/WebGL 指纹等。Selenium 或 Playwright 不是为了“点按钮”,而是为了通过整套前端校验链。
立即学习“Python免费学习笔记(深入)”;
实操建议:
- 启动浏览器时启用
--disable-blink-features=AutomationControlled并 patchnavigator.webdriver - 登录流程中,等待
input[name="phone"]可见后才输入手机号,避免因 DOM 未就绪导致空提交 - 短信到达后,不要用 OCR 识别短信内容——延迟高、准确率低;改用 Twilio 或 云片等平台的 API 接收,通过
messages.list(date_sent_after=datetime.now()-timedelta(minutes=1))拉取最新条目 - 拿到验证码后,用
driver.find_element(By.NAME, "sms_code").send_keys(code)输入,再显式等待document.readyState === "complete"
如何把浏览器登录态安全移交 requests
移交不是复制 Cookie 字符串那么简单。现代网站常校验 SameSite、Secure、HttpOnly 属性,还可能绑定 TLS 会话 ID 或 UA Hash。直接复用 driver.get_cookies() 返回的字典,大概率在第二步请求时被清空会话。
正确做法是:
- 登录成功后,调用
driver.execute_script("return document.cookie")获取原始 cookie 字符串(含所有属性) - 用正则提取
key=value对,过滤掉Path=/、Domain=.example.com等非值字段 - 构造
requests.Session(),用s.cookies.set(name, value, domain="example.com")逐个注入,而非s.cookies.update({}) - 首次移交请求必须带上完整
User-Agent和Accept头,且与浏览器中一致;可从driver.execute_script("return navigator.userAgent")动态获取
短信验证码本身不可预测,但接收通道可以预置
你无法控制对方服务器何时发码、发什么码,但可以控制“谁来收”和“怎么取”。硬编码手机号+人工查收,只适用于调试;生产环境必须对接虚拟号平台。
关键细节:
- Twilio 的
incoming_phone_numbers需提前购买并验证,不能临时注册;国内推荐云片或容联云,API 响应更快(平均 - 不要在每次请求前新建 client 实例,复用连接池,否则可能触发平台频率限制
- 收到短信后立即调用
delete_message(sid),防止重复消费;部分平台需手动归档,否则 72 小时后自动清除 - 若目标网站对同一手机号限频(如 1 小时 1 次),需准备至少 3 个号码轮换,存入 Redis 按
phone:rate_limit计数
最易被忽略的一点:短信验证码页面往往带有隐藏字段(如 csrf_token、timestamp、sign),这些值在浏览器中由 JS 动态生成,且与当前会话强绑定。截图看 HTML 源码找不到它们,必须用 driver.execute_script("return document.querySelector('input[name=\"sign\"]').value") 实时提取——漏掉任意一个,提交必失败。


















