ksjsb/tools/captcha_inspect.py
2026-07-30 20:25:56 +08:00

157 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""探测快手官方验证页 DOM为 ddddocr 滑块识别/拖动收集真实结构。
流程:
1. 复用 sms_login_cli --code 000000不发短信拿一个新鲜的 705 error_url
2. 用系统浏览器 CDP与正式 captcha-assisted 同路径,最不易被识别)加载;
3. 等待滑块渲染dumpiframe/img/canvas、疑似滑块元素的 bbox、保存截图与图片。
用法:
uv run python -m tools.captcha_inspect --mobile 17666663175
# 直接给已知的 error_url跳过取号
uv run python -m tools.captcha_inspect --url 'https://app.m.kuaishou.com/verify/captcha.html?key=...'
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
OUT_DIR = ROOT / "out" / "captcha_inspect"
def _fresh_error_url(mobile: str, device_profile: str, base_url: str) -> str:
cmd = [
sys.executable, "-m", "tools.sms_login_cli",
"--mobile", mobile,
"--code", "000000",
"--base-url", base_url,
"--transport", "okhttp4-android10",
]
print(f"[inspect] 取新鲜 error_url: {' '.join(cmd[:1])} ... -m tools.sms_login_cli ...")
proc = subprocess.run(cmd, capture_output=True, text=True, cwd=str(ROOT))
out = proc.stdout + proc.stderr
m = re.search(r'https://app\.m\.kuaishou\.com/verify/captcha\.html\?[^"\s\\]+', out)
if not m:
print(out[-1500:])
raise SystemExit("[inspect] 未从验证码校验输出里提取到 error_url")
return m.group(0)
def _dump_frame(frame: object, tag: str, dump: dict) -> None:
"""对单个 frame主页面或 iframe执行 JS 探测。"""
js = r"""
() => {
const grab = (el) => ({
tag: el.tagName.toLowerCase(),
id: el.id || '',
cls: el.className && el.className.toString ? el.className.toString() : '',
bbox: (() => { const r = el.getBoundingClientRect(); return {x:r.x|0,y:r.y|0,w:r.width|0,h:r.height|0}; })(),
});
const imgs = [...document.querySelectorAll('img')].map(i => ({...grab(i), src:(i.src||'').slice(0,120), nw:i.naturalWidth, nh:i.naturalHeight}));
const canvases = [...document.querySelectorAll('canvas')].map(c => ({...grab(c), cw:c.width, ch:c.height}));
const cand = [...document.querySelectorAll('[class*="slide"],[class*="slider"],[class*="block"],[class*="btn"],[class*="track"],[class*="handle"],[id*="slide"],[id*="block"],[id*="btn"]')].map(grab);
const iframes = [...document.querySelectorAll('iframe')].map(f => ({src:(f.src||'').slice(0,120), ...grab(f)}));
return {url: location.href, imgs, canvases, cand, iframes, title: document.title};
}
"""
try:
data = frame.evaluate(js)
except Exception as exc: # noqa: BLE001
dump[tag] = {"error": f"{exc.__class__.__name__}: {exc}"}
return
dump[tag] = data
def inspect(error_url: str, *, wait: int, channel: str) -> None:
from core.captcha_assist import _find_system_chromium, _free_local_port, _wait_for_cdp_endpoint
from playwright.sync_api import sync_playwright
import subprocess as sp
import shutil
import tempfile
OUT_DIR.mkdir(parents=True, exist_ok=True)
browser_path = _find_system_chromium()
port = _free_local_port()
profile_dir = tempfile.mkdtemp(prefix="ksjsb-inspect-")
proc = sp.Popen(
[
browser_path,
f"--remote-debugging-port={port}",
"--remote-debugging-address=127.0.0.1",
f"--user-data-dir={profile_dir}",
"--no-first-run",
"--no-default-browser-check",
"--window-size=430,920",
"about:blank",
],
stdout=sp.DEVNULL, stderr=sp.DEVNULL,
)
try:
endpoint = _wait_for_cdp_endpoint(port, timeout=20)
with sync_playwright() as pw:
browser = pw.chromium.connect_over_cdp(endpoint, timeout=20000)
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
print(f"[inspect] goto {error_url[:90]}...")
page.goto(error_url, wait_until="domcontentloaded", timeout=30000)
# 给滑块资源加载时间
time.sleep(min(max(wait, 2), 15))
dump: dict = {}
_dump_frame(page, "main", dump)
for idx, fr in enumerate(page.frames[1:]):
_dump_frame(fr, f"frame{idx}:{fr.url[:60]}", dump)
(OUT_DIR / "dom.json").write_text(json.dumps(dump, ensure_ascii=False, indent=2), encoding="utf-8")
try:
page.screenshot(path=str(OUT_DIR / "page.png"), full_page=True)
except Exception as exc: # noqa: BLE001
print(f"[inspect] 截图失败: {exc}")
# 抓所有 img 字节(含 data: 与 http:),供 ddddocr 分析
saved = []
for ftag, fdata in dump.items():
if not isinstance(fdata, dict):
continue
for i, img in enumerate(fdata.get("imgs") or []):
src = img.get("src") or ""
if not src:
continue
saved.append({"frame": ftag, "idx": i, "src": src[:80], "bbox": img.get("bbox")})
(OUT_DIR / "imgs_index.json").write_text(json.dumps(saved, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[inspect] dump 写入 {OUT_DIR}")
print(json.dumps(dump, ensure_ascii=False, indent=2)[:4000])
finally:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=3)
except sp.TimeoutExpired:
proc.kill()
shutil.rmtree(profile_dir, ignore_errors=True)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--mobile", default="17666663175")
ap.add_argument("--url", default="", help="直接给 error_url跳过取号")
ap.add_argument("--device-profile", default="out/sms_device_latest.json")
ap.add_argument("--base-url", default="https://az1-api.ksapisrv.com")
ap.add_argument("--wait", type=int, default=6, help="等待滑块渲染秒数")
args = ap.parse_args()
error_url = args.url or _fresh_error_url(args.mobile, args.device_profile, args.base_url)
print(f"[inspect] error_url = {error_url}")
inspect(error_url, wait=args.wait, channel="system")
return 0
if __name__ == "__main__":
raise SystemExit(main())