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

115 lines
5.7 KiB
Python

"""探针: 打开活验证码页, dump 滑块 iframe 的 DOM 结构 + 元素几何, 供写自动求解器参考。
取新鲜 error_url -> 系统浏览器 CDP -> 截屏 + dump frames + 候选滑块元素 bbox。
落盘 out/captcha_dom/
"""
from __future__ import annotations
import json, re, subprocess, sys, time, shutil, tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
OUT = ROOT / "out" / "captcha_dom"
def fresh_error_url(mobile, profile, base):
cmd = [sys.executable, "-m", "tools.sms_login_cli", "--mobile", mobile,
"--code", "000000", "--base-url", base,
"--transport", "okhttp4-android10"]
print("[probe] 取新鲜 error_url ...")
p = subprocess.run(cmd, capture_output=True, text=True, cwd=str(ROOT))
out = p.stdout + p.stderr
m = re.search(r'https://app\.m\.kuaishou\.com/verify/captcha\.html\?[^"\s\\]+', out)
if not m:
print(out[-1500:]); raise SystemExit("[probe] 未提取到 error_url")
return m.group(0)
def main():
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
OUT.mkdir(parents=True, exist_ok=True)
url = fresh_error_url("17666663175", "out/sms_device_latest.json", "https://az1-api.ksapisrv.com")
print("[probe] error_url =", url[:90])
browser = _find_system_chromium(); port = _free_local_port()
prof = tempfile.mkdtemp(prefix="ksjsb-dom-")
proc = sp.Popen([browser, f"--remote-debugging-port={port}", "--remote-debugging-address=127.0.0.1",
f"--user-data-dir={prof}", "--no-first-run", "--no-default-browser-check",
"--window-size=430,920", "about:blank"], stdout=sp.DEVNULL, stderr=sp.DEVNULL)
try:
ep = _wait_for_cdp_endpoint(port, timeout=20)
with sync_playwright() as pw:
b = pw.chromium.connect_over_cdp(ep, timeout=20000)
page = b.contexts[0].pages[0] if b.contexts[0].pages else b.contexts[0].new_page()
page.goto(url, wait_until="domcontentloaded", timeout=30000)
# 等 captcha iframe 出现并加载
print("[probe] 等 iframe 加载...")
time.sleep(6)
dump = {"main_url": page.url, "frames": []}
for i, fr in enumerate(page.frames):
f = {"i": i, "url": fr.url[:160], "elements": []}
if "captcha" in fr.url or "sliding" in fr.url or "verify" in fr.url:
try:
f["body_html_head"] = (fr.content() or "")[:2500]
except Exception as e:
f["body_html_head_err"] = str(e)
# 找候选滑块元素
try:
cand = fr.evaluate("""() => {
const out = [];
const sel = 'div, img, span, canvas';
document.querySelectorAll(sel).forEach(el => {
const r = el.getBoundingClientRect();
const cls = el.getAttribute('class')||'';
const id = el.getAttribute('id')||'';
const role = el.getAttribute('data-role')||'';
const sty = el.getAttribute('style')||'';
const key = (cls+' '+id+' '+role).toLowerCase();
if (r.width>0 && r.height>0 && r.bottom>0 && r.right>0 &&
/(slide|slider|btn|track|cut|piece|drag|knob|selec)/.test(key)) {
out.push({tag:el.tagName, cls, id, role,
x:Math.round(r.x), y:Math.round(r.y),
w:Math.round(r.width), h:Math.round(r.height),
bg:(sty.match(/background[^;]*/)||[''])[0].slice(0,60)});
}
});
// 也 dump 所有 img 元素(背景图/缺口图)
document.querySelectorAll('img,canvas').forEach(el => {
const r = el.getBoundingClientRect();
if (r.width>0 && r.height>0) out.push({tag:el.tagName,
src:(el.getAttribute('src')||'').slice(0,80),
x:Math.round(r.x), y:Math.round(r.y),
w:Math.round(r.width), h:Math.round(r.height)});
});
return out;
}""")
f["elements"] = cand
except Exception as e:
f["elements_err"] = str(e)
dump["frames"].append(f)
(OUT / "dom.json").write_text(json.dumps(dump, ensure_ascii=False, indent=2), encoding="utf-8")
try: page.screenshot(path=str(OUT / "page.png"), full_page=True)
except Exception: pass
print("[probe] done ->", OUT)
# 打印候选滑块元素
for f in dump["frames"]:
if f["elements"]:
print(f"\n=== frame {f['i']} {f['url'][:60]} ===")
if f.get("body_html_head"):
print(" HTML head:\n", f["body_html_head"][:1500])
print(" 候选元素:")
for e in f["elements"][:25]:
print(" ", json.dumps(e, ensure_ascii=False))
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=3)
except sp.TimeoutExpired: proc.kill()
shutil.rmtree(prof, ignore_errors=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())