122 lines
5.8 KiB
Python
122 lines
5.8 KiB
Python
"""抓 captcha 协议的 config 响应体 + bgPic/cutPic 图片 + captchaSession(无需拖动)。
|
||
|
||
config 在 iframe 加载时自动 POST,所以不开滑块也能拿到 schema。
|
||
落盘 out/captcha_cap/: config.json, bgPic.jpg, cutPic.png, session.txt, full_timeline.json
|
||
"""
|
||
from __future__ import annotations
|
||
import argparse, 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_cap"
|
||
|
||
|
||
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("[cap] 取新鲜 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("[cap] 未提取到 error_url")
|
||
return m.group(0)
|
||
|
||
|
||
def capture(error_url, wait):
|
||
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)
|
||
browser = _find_system_chromium(); port = _free_local_port()
|
||
prof = tempfile.mkdtemp(prefix="ksjsb-cap-")
|
||
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)
|
||
blobs = {}
|
||
timeline = []
|
||
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()
|
||
|
||
def on_response(resp):
|
||
url = str(resp.url)
|
||
# 调试: 记录所有非静态资源的响应
|
||
if not re.search(r'\.(js|css|png|jpg|gif|woff|ico)(\?|$)', url) and "log/collect" not in url and "radar" not in url:
|
||
timeline.append({"url": url[:200], "status": resp.status, "ct": (resp.headers or {}).get("content-type", "")})
|
||
if any(k in url for k in ("/sliding/config", "/sliding/bgPic", "/sliding/cutPic")):
|
||
ct = (resp.headers or {}).get("content-type", "")
|
||
timeline.append({"url": url, "status": resp.status, "ct": ct})
|
||
try:
|
||
body = resp.body()
|
||
except Exception as e:
|
||
timeline[-1]["err"] = str(e); return
|
||
if "config" in url:
|
||
try:
|
||
data = json.loads(body)
|
||
except Exception:
|
||
data = {"_raw": body.decode("utf-8", "replace")[:2000]}
|
||
(OUT / "config.json").write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
blobs["config"] = True
|
||
print(f"[cap] config JSON ({len(body)}B) -> keys: {list(data.keys()) if isinstance(data, dict) else type(data)}")
|
||
elif "bgPic" in url:
|
||
(OUT / "bgPic.jpg").write_bytes(body); blobs["bgPic"] = len(body)
|
||
print(f"[cap] bgPic {len(body)}B")
|
||
elif "cutPic" in url:
|
||
(OUT / "cutPic.png").write_bytes(body); blobs["cutPic"] = len(body)
|
||
print(f"[cap] cutPic {len(body)}B")
|
||
|
||
page.on("response", on_response)
|
||
print(f"[cap] goto {error_url[:80]}...")
|
||
page.goto(error_url, wait_until="domcontentloaded", timeout=30000)
|
||
# iframe 里的 config 请求要等 iframe 加载完
|
||
for _ in range(wait * 2):
|
||
if blobs.get("config") and blobs.get("bgPic") and blobs.get("cutPic"):
|
||
break
|
||
time.sleep(0.5)
|
||
try:
|
||
page.screenshot(path=str(OUT / "page.png"), full_page=True)
|
||
except Exception as e:
|
||
print("[cap] 截图失败", e)
|
||
# 抓 captchaSession(从 iframe url 或 config 请求体)
|
||
try:
|
||
for fr in page.frames:
|
||
if "captchaSession=" in fr.url:
|
||
cs = re.search(r'captchaSession=([^&]+)', fr.url)
|
||
if cs: (OUT / "session.txt").write_text(cs.group(1), encoding="utf-8"); print("[cap] session saved")
|
||
except Exception as e:
|
||
print("[cap] session 抓取失败", e)
|
||
finally:
|
||
if proc.poll() is None:
|
||
proc.terminate()
|
||
try: proc.wait(timeout=3)
|
||
except sp.TimeoutExpired: proc.kill()
|
||
shutil.rmtree(prof, ignore_errors=True)
|
||
(OUT / "timeline.json").write_text(json.dumps(timeline, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
print(f"[cap] done. blobs={blobs} dir={OUT}")
|
||
return blobs
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--mobile", default="17666663175")
|
||
ap.add_argument("--url", default="")
|
||
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=10)
|
||
a = ap.parse_args()
|
||
url = a.url or fresh_error_url(a.mobile, a.device_profile, a.base_url)
|
||
print("[cap] error_url =", url[:90])
|
||
capture(url, a.wait)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|