吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 117|回复: 3
上一主题 下一主题
收起左侧

[其他原创] 分享一个通过浏览器自动化输入抖音短信验证码的方法

  [复制链接]
跳转到指定楼层
楼主
wangsir9527 发表于 2026-9-22 19:28 回帖奖励


抖音网页登录自动化:XPath 找不到验证码输入框的解决方法
最近在给一个客户端程序增加抖音自动化搜索功能,登录流程大致是:

  • 客户端打开浏览器并进入抖音;

  • 获取登录二维码;

  • 将二维码发送到个人微信;

  • 用户在手机微信中查看二维码,再使用抖音 App 扫码;

  • 如果抖音要求身份验证,用户在微信中选择短信验证;

  • 用户收到短信后,再把验证码发给客户端;

  • 客户端自动填写验证码并完成登录。

整体流程并不复杂,但卡在了最后一步:验证码输入框怎么都填不进去。一开始使用 XPath
最初的写法类似这样:input_box = page.locator(    '//input[@placeholder="请输入验证码"]')input_box.fill(code)
或者:input_box = page.locator(    '//input[contains(@placeholder, "验证码")]')input_box.fill(code)
但是实际运行时经常出现以下情况:

  • 找不到元素;

  • 找到元素但无法输入;

  • fill() 没有报错,但页面没有变化;

  • 输入内容填到了错误的输入框;

  • 扫码后页面结构发生变化,之前的 XPath 失效。
实际页面中有多个验证码输入框
后来通过页面结构发现,抖音登录页面并不只有一个验证码输入框。
普通登录弹窗本身就存在一个:请输入验证码
扫码后,抖音又会弹出一个身份验证窗口,里面同样存在:请输入验证码
也就是说,全页面直接查找时,可能会命中后面那个隐藏的输入框,而不是当前显示的身份验证输入框。
这也是之前 XPath 一直失败的主要原因。
问题不是验证码内容不对,而是定位到了错误的 DOM 元素。改用 Playwright 的可访问性定位
[Python] 纯文本查看 复制代码
#!/usr/bin/env python3
"""通过扫码和短信验证码登录抖音。

二维码由用户在抖音 App 中扫描,短信验证码由用户输入。脚本不会读取、绕过或自动
处理验证码/人脸验证,也不会默认保存会话。
"""

from __future__ import annotations

import argparse
import re
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Optional


class DouyinLoginError(RuntimeError):
    """抖音登录流程失败。"""


@dataclass(frozen=True)
class LoginOptions:
    """登录流程配置。"""

    url: str = "https://www.douyin.com/"
    browser_channel: str = "chrome"
    executable_path: Optional[str] = None
    headless: bool = False
    timeout_ms: int = 30_000
    qr_timeout_seconds: int = 600
    storage_state: Optional[Path] = None
    keep_open: bool = False
    pause_on_error: bool = False


def _is_visible(locator, timeout_ms: int = 500) -> bool:
    """判断元素是否可见,并把超时转换为 False。"""

    try:
        return locator.is_visible(timeout=timeout_ms)
    except Exception:
        return False


def _wait_for(locator, timeout_seconds: float) -> bool:
    """轮询等待元素出现。"""

    deadline = time.monotonic() + timeout_seconds
    while time.monotonic() < deadline:
        if _is_visible(locator, timeout_ms=300):
            return True
        locator.page.wait_for_timeout(300)
    return False


def _has_visible_login_entry(page) -> bool:
    """判断页面顶部是否仍显示未登录入口。"""

    login = page.get_by_text("登录", exact=True)
    for index in range(login.count()):
        if _is_visible(login.nth(index), timeout_ms=300):
            return True
    return False


def _wait_for_login_success(page, verification_article, timeout_seconds: float) -> bool:
    """等待验证弹窗消失或页面顶部切换为已登录状态。"""

    deadline = time.monotonic() + timeout_seconds
    while time.monotonic() < deadline:
        if not _is_visible(verification_article, timeout_ms=300):
            return True
        if not _has_visible_login_entry(page):
            return True
        page.wait_for_timeout(300)
    return False


def _verification_article(page):
    """找到身份验证弹窗对应的 article,避免命中后面的登录表单。"""

    articles = page.locator("article")
    for index in range(articles.count()):
        article = articles.nth(index)
        if not _is_visible(article, timeout_ms=300):
            continue
        try:
            text = article.inner_text(timeout=500)
        except Exception:
            continue
        if "身份验证" in text or "短信已发送至" in text:
            return article
    return None


def _wait_for_verification_article(page, timeout_seconds: float):
    """等待扫码后的身份验证弹窗出现。"""

    deadline = time.monotonic() + timeout_seconds
    while time.monotonic() < deadline:
        article = _verification_article(page)
        if article is not None:
            return article
        page.wait_for_timeout(500)
    return None


def _wait_for_sms_input(page, timeout_seconds: float):
    """只在身份验证弹窗内寻找验证码输入框。"""

    deadline = time.monotonic() + timeout_seconds
    while time.monotonic() < deadline:
        article = _verification_article(page)
        if article is not None:
            otp = article.get_by_role("textbox", name="请输入验证码").last
            if _is_visible(otp, timeout_ms=300):
                return otp
        page.wait_for_timeout(300)
    return None


def _ensure_login_dialog(page) -> None:
    """确保首页的登录弹窗已打开。"""

    if _is_visible(page.get_by_text("扫码登录", exact=True)):
        return

    login = page.get_by_text("登录", exact=True)
    if login.count() == 0:
        raise DouyinLoginError("未找到抖音登录入口")
    login.last.click()

    if not _wait_for(page.get_by_text("扫码登录", exact=True), 10):
        raise DouyinLoginError("抖音登录弹窗未出现")


def _wait_for_scan_or_login(page, timeout_seconds: int) -> None:
    """等待用户扫码后进入验证步骤。"""

    error = page.get_by_text("错误次数过多或验证码过期,请稍后重试", exact=True)

    deadline = time.monotonic() + timeout_seconds
    while time.monotonic() < deadline:
        if _is_visible(error):
            raise DouyinLoginError("抖音提示验证码过期或错误次数过多,请稍后重试")
        if _verification_article(page) is not None:
            return
        page.wait_for_timeout(500)

    raise DouyinLoginError("等待扫码超时,请重新运行程序并扫描二维码")


def _select_sms_verification(page):
    """选择短信验证,并等待验证码输入框。"""

    article = _wait_for_verification_article(page, 30)
    if article is None:
        raise DouyinLoginError("身份验证弹窗未出现")

    sms_choice = article.get_by_text("接收短信验证码", exact=True)
    if not _wait_for(sms_choice, 10):
        raise DouyinLoginError("身份验证弹窗中未找到“接收短信验证码”选项")
    sms_choice.click()
    page.wait_for_timeout(500)

    # 某些版本会在选择方式后再显示一个“发送短信验证”按钮。
    article = _verification_article(page) or article
    send_sms = article.get_by_text("发送短信验证", exact=True)
    if _is_visible(send_sms):
        send_sms.click()

    otp = _wait_for_sms_input(page, 30)
    if otp is None:
        # 页面版本差异较大时保留人工兜底,但不再错误地读取后面的隐藏输入框。
        print("请在浏览器中点击“接收短信验证码”,等待短信输入框出现。")
        otp = _wait_for_sms_input(page, 120)
    if otp is None:
        raise DouyinLoginError("短信验证码输入框未出现,可能需要稍后重试")
    verification_article = _verification_article(page) or article
    return otp, verification_article


def _read_code() -> str:
    """从终端读取短信验证码,不记录到日志。"""

    code = input("请输入抖音短信验证码(不会保存):").strip()
    if not re.fullmatch(r"\d{4,8}", code):
        raise DouyinLoginError("验证码应为 4 至 8 位数字")
    return code


def login_douyin(options: LoginOptions = LoginOptions()):
    """打开隔离浏览器上下文并完成抖音登录。

    返回 ``(playwright, browser, context, page)``,调用方负责关闭这些对象。
    """

    try:
        from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
        from playwright.sync_api import sync_playwright
    except ImportError as exc:
        raise DouyinLoginError(
            "缺少 Playwright,请先安装:pip install 'douyin-mcp-server[browser]'"
        ) from exc

    pw = sync_playwright().start()
    launch_args = {"headless": options.headless}
    if options.browser_channel:
        launch_args["channel"] = options.browser_channel
    if options.executable_path:
        launch_args["executable_path"] = options.executable_path

    try:
        browser = pw.chromium.launch(**launch_args)
        context = browser.new_context()
        context.set_default_timeout(options.timeout_ms)
        page = context.new_page()
        page.goto(options.url, wait_until="domcontentloaded")
        _ensure_login_dialog(page)
        print("请用抖音 App 扫描浏览器中的二维码。")
        _wait_for_scan_or_login(page, options.qr_timeout_seconds)
        otp, verification_article = _select_sms_verification(page)

        code = _read_code()
        otp.fill(code)
        page.get_by_text("验证", exact=True).last.click()

        error = page.get_by_text("错误次数过多或验证码过期,请稍后重试", exact=True)
        if _wait_for(error, 3):
            raise DouyinLoginError("验证码已过期或错误次数过多,请稍后重试")

        # 抖音验证成功后会先执行动画和状态刷新,不能用“立即可见”判断失败。
        if not _wait_for_login_success(page, verification_article, 15):
            raise DouyinLoginError("验证码验证未完成,请检查抖音页面提示")

        if options.storage_state:
            options.storage_state.parent.mkdir(parents=True, exist_ok=True)
            context.storage_state(path=str(options.storage_state))
            print(f"登录会话已保存到:{options.storage_state}")

        print("抖音登录成功。")
        return pw, browser, context, page
    except PlaywrightTimeoutError as exc:
        if options.pause_on_error:
            input("浏览器仍保持打开,按 Enter 后关闭……")
        pw.stop()
        raise DouyinLoginError("抖音页面操作超时,请检查网络或稍后重试") from exc
    except Exception:
        if options.pause_on_error:
            input("浏览器仍保持打开,按 Enter 后关闭……")
        pw.stop()
        raise


def _parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="抖音扫码 + 短信验证码登录")
    parser.add_argument("--url", default="https://www.douyin.com/", help="抖音入口 URL")
    parser.add_argument(
        "--browser-channel",
        default="chrome",
        help="Playwright 浏览器 channel,默认使用已安装的 Google Chrome",
    )
    parser.add_argument("--executable-path", help="自定义 Chrome/Chromium 可执行文件路径")
    parser.add_argument("--qr-timeout", type=int, default=600, help="等待扫码秒数,默认 10 分钟")
    parser.add_argument(
        "--storage-state",
        type=Path,
        help="可选:保存登录 cookies 的 JSON 文件(请妥善保护)",
    )
    parser.add_argument(
        "--keep-open",
        action="store_true",
        help="登录成功后保持浏览器窗口,按 Enter 后退出",
    )
    return parser.parse_args(argv)


def main(argv: Optional[list[str]] = None) -> int:
    args = _parse_args(argv)
    options = LoginOptions(
        url=args.url,
        browser_channel=args.browser_channel,
        executable_path=args.executable_path,
        qr_timeout_seconds=args.qr_timeout,
        storage_state=args.storage_state,
        keep_open=args.keep_open,
        pause_on_error=True,
    )

    try:
        pw, browser, context, _page = login_douyin(options)
    except (DouyinLoginError, KeyboardInterrupt) as exc:
        print(f"登录失败:{exc}", file=sys.stderr)
        return 1

    try:
        if options.keep_open:
            input("按 Enter 关闭浏览器并结束程序……")
    finally:
        context.close()
        browser.close()
        pw.stop()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
最终效果
目前这套流程已经可以稳定完成:打开 Chrome    ↓进入抖音    ↓显示二维码    ↓二维码发送到微信    ↓用户使用抖音 App 扫码    ↓等待身份验证弹窗    ↓点击“接收短信验证码”    ↓等待短信验证码输入框    ↓用户通过微信发送验证码    ↓客户端填写验证码    ↓点击“验证”    ↓等待登录状态完成总结
这次问题的核心并不是 XPath 语法错误,而是页面存在多个相同输入框,并且扫码后 DOM 会动态变化。
相比直接写 XPath,比较稳妥的方式是:先定位语义明确的弹窗    ↓再在弹窗内部查找控件    ↓使用 role、label、placeholder 等语义定位    ↓等待元素真正可见    ↓最后再执行输入操作
最终使用的关键代码是:verification_article = find_verification_article(page)otp = verification_article.get_by_role(    "textbox",    name="请输入验证码").lastotp.fill(code)
这比直接对整个页面使用 XPath 稳定很多,也更容易应对抖音页面结构变化。

免费评分

参与人数 2吾爱币 +2 热心值 +2 收起 理由
wjdjp + 1 + 1 谢谢@Thanks!
shiyue2925 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
Zense111 发表于 2026-9-22 23:30
感谢分享
3#
hubo7766 发表于 2026-9-22 23:46
浏览器自动化处理验证码这个思路确实不错,不用手动输验证码省了好多事。不过现在各个平台的反自动化检测都挺严的,这种方法可能用一段时间就会被识别出来,需要经常调整策略。能把这个过程分享出来确实很实用,做自动化测试的小伙伴肯定用得上。
4#
xinxiu 发表于 2026-9-22 23:47
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-23 03:33

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表