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

175 lines
6.9 KiB
Python
Raw 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.

"""抓取快手验证页的网络协议 + JS bundle评估"扣 JS 纯算"可行性。
落盘:
- out/captcha_net/timeline.json —— 所有 request(method/url/post_data head/headers)
与 response(status/content-type/url) 时间线
- out/captcha_net/js_*.js —— 页面加载的所有外部脚本源码
重点看:
1. challenge 拉图请求(哪个端点、什么参数、返回什么);
2. kSecretApiVerify 的请求体是明文距离/轨迹,还是加密 blob
3. 构造 verify 体的 JS 能否单独扒出来 + 是否依赖 DOM/window补环境成本
用法:
uv run python -m tools.captcha_net --mobile 17666663175
uv run python -m tools.captcha_net --url 'https://...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_net"
def _fresh_error_url(mobile: str, device_profile: str, base_url: str) -> str:
cmd = [
sys.executable, "-m", "tools.sms_login_cli",
"--mobile", mobile, "--base-url", base_url,
"--code", "000000", "--transport", "okhttp4-android10",
]
print("[net] 取新鲜 error_url ...")
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("[net] 未提取到 error_url")
return m.group(0)
def capture(error_url: str, *, wait: int) -> None:
import shutil
import subprocess as sp
import tempfile
from core.captcha_assist import _find_system_chromium, _free_local_port, _wait_for_cdp_endpoint
from playwright.sync_api import sync_playwright
OUT_DIR.mkdir(parents=True, exist_ok=True)
for f in OUT_DIR.glob("js_*.js"):
f.unlink()
browser_path = _find_system_chromium()
port = _free_local_port()
profile_dir = tempfile.mkdtemp(prefix="ksjsb-net-")
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,
)
timeline: list[dict] = []
js_urls: list[str] = []
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()
def on_request(req: object) -> None:
url = str(getattr(req, "url", "") or "")
method = str(getattr(req, "method", "") or "")
post = getattr(req, "post_data", None)
if isinstance(post, (bytes, bytearray)):
post = post.decode("utf-8", "replace")
headers = dict(getattr(req, "headers", {}) or {})
if any(k in url for k in ("captcha", "sliding", "kSecret", "verify", "/zt/", "/wd/")):
timeline.append({
"kind": "req", "method": method, "url": url,
"post_head": (post or "")[:600],
"header_keys": sorted(headers),
})
if url.endswith(".js") or ".js?" in url:
js_urls.append(url)
def on_response(resp: object) -> None:
url = str(getattr(resp, "url", "") or "")
if any(k in url for k in ("captcha", "sliding", "kSecret", "verify", "/zt/", "/wd/")):
headers = dict(getattr(resp, "headers", {}) or {})
timeline.append({
"kind": "resp", "url": url,
"status": int(getattr(resp, "status", 0) or 0),
"content_type": headers.get("content-type", ""),
})
page.on("request", on_request)
page.on("response", on_response)
print(f"[net] goto {error_url[:90]}...")
page.goto(error_url, wait_until="domcontentloaded", timeout=30000)
time.sleep(min(max(wait, 2), 12))
# 列出页面实际加载的脚本 src逐个落盘
scripts = page.evaluate(
"Array.from(document.scripts).map(s=>s.src).filter(Boolean)"
) or []
# 合并 on_request 捕获到的 .js含动态注入的
all_js = list(dict.fromkeys([*scripts, *js_urls]))
print(f"[net] 脚本数={len(all_js)},开始下载源码")
from curl_cffi.requests import get as cffi_get
for idx, src in enumerate(all_js):
try:
r = cffi_get(src, timeout=20, headers={"Referer": error_url})
body = r.content or b""
except Exception as exc: # noqa: BLE001
print(f" [{idx}] 下载失败 {src[:80]}: {exc.__class__.__name__}")
continue
name = f"js_{idx:02d}_{re.sub(r'[^a-z0-9]+','_',src.lower())[-40:]}.js"
(OUT_DIR / name).write_bytes(body)
print(f" [{idx}] {len(body)}B {src[:90]}")
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)
(OUT_DIR / "timeline.json").write_text(
json.dumps(timeline, ensure_ascii=False, indent=2), encoding="utf-8"
)
print("\n=== 协议时间线captcha 相关)===")
for t in timeline:
if t["kind"] == "req":
print(f" REQ {t['method']} {t['url'][:95]}")
if t.get("post_head"):
print(f" post: {t['post_head'][:300]}")
else:
print(f" RESP[{t['status']}] {t.get('content_type','')} {t['url'][:95]}")
print(f"\n[net] 落盘目录: {OUT_DIR}")
def main() -> int:
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=8)
args = ap.parse_args()
error_url = args.url or _fresh_error_url(args.mobile, args.device_profile, args.base_url)
print(f"[net] error_url = {error_url}")
capture(error_url, wait=args.wait)
return 0
if __name__ == "__main__":
raise SystemExit(main())