commit 47ba116ba13068b231ebc2a2ec442b4ed9bb1475 Author: theluyuan <1162963624@qq.com> Date: Sat Feb 21 19:53:04 2026 +0800 Initial commit: 项目初始代码 Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b638bc6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +.venv/ +env/ +.env.local + +# 敏感/本地数据(勿提交) +cookies.txt + +# 调试生成的截图 +click_position.png + +# IDE / 编辑器 +.idea/ +.vscode/ +*.swp +*.swo + +# 系统 +.DS_Store +Thumbs.db diff --git a/box.png b/box.png new file mode 100644 index 0000000..1e96f58 Binary files /dev/null and b/box.png differ diff --git a/cookies_loader.py b/cookies_loader.py new file mode 100644 index 0000000..c2d4994 --- /dev/null +++ b/cookies_loader.py @@ -0,0 +1,112 @@ +# 从根目录 cookies.txt 加载并注入 cookies(支持 Netscape 与 nodriver 原生格式) + +from pathlib import Path + +# 项目根目录(本文件所在目录的父目录即 audcf) +ROOT_DIR = Path(__file__).resolve().parent +COOKIES_FILE = ROOT_DIR / "cookies.txt" + + +def get_cookies_path() -> Path: + return COOKIES_FILE + + +def cookies_file_exists() -> bool: + return COOKIES_FILE.is_file() + + +def is_netscape_format(path: Path) -> bool: + """根据首行判断是否为 Netscape cookies.txt 格式。""" + if not path.is_file(): + return False + try: + with open(path, "r", encoding="utf-8", errors="ignore") as f: + first = f.readline().strip() + return first.startswith("# Netscape") or first.startswith("# HTTP Cookie File") + except Exception: + return False + + +def parse_netscape_cookies(path: Path) -> list[dict]: + """ + 解析 Netscape 格式 cookies.txt(含 Cookie-Editor 等导出的多行注释)。 + 每行(非注释):domain, subdomain, path, secure, expires, name, value(制表符分隔;value 可含制表符故用 maxsplit=6) + 返回字典列表供转为 CookieParam。 + """ + cookies = [] + with open(path, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split("\t", 6) + if len(parts) < 7: + continue + domain, _subdomain, path_val, secure_val, expires_str, name, value = parts + secure = secure_val.upper() == "TRUE" + try: + expires = float(expires_str) if expires_str and expires_str != "0" else None + except ValueError: + expires = None + cookies.append({ + "name": name, + "value": value, + "domain": domain, + "path": path_val or "/", + "expires": expires, + "http_only": False, + "secure": secure, + "same_site": "Lax", + }) + return cookies + + +async def load_cookies_nodriver_native(browser, path: Path) -> bool: + """ + 使用 nodriver 原生格式加载 cookies(browser.cookies.load)。 + 成功返回 True,失败返回 False。 + """ + try: + await browser.cookies.load(file=str(path)) + return True + except Exception: + return False + + +def _dict_to_cookie_param(d: dict): + """将解析后的 cookie 字典转为 nodriver CDP 所需的 CookieParam。""" + from nodriver.cdp import network + expires = d.get("expires") + if expires is not None: + expires = network.TimeSinceEpoch(expires) + same_site = d.get("same_site") + if same_site: + same_site = network.CookieSameSite(same_site) + return network.CookieParam( + name=d["name"], + value=d["value"], + domain=d.get("domain"), + path=d.get("path") or "/", + secure=d.get("secure"), + http_only=d.get("http_only"), + same_site=same_site, + expires=expires, + ) + + +async def inject_netscape_cookies(tab, path: Path) -> bool: + """ + 解析 Netscape 格式的 cookies.txt 并通过 CDP 注入到当前 tab。 + 应在已打开目标域页面后调用。 + 成功返回 True,失败返回 False。 + """ + try: + from nodriver import cdp + cookies_list = parse_netscape_cookies(path) + if not cookies_list: + return True + cookies_param = [_dict_to_cookie_param(d) for d in cookies_list] + await tab.send(cdp.storage.set_cookies(cookies=cookies_param)) + return True + except Exception: + return False diff --git a/main.py b/main.py new file mode 100644 index 0000000..86effaa --- /dev/null +++ b/main.py @@ -0,0 +1,93 @@ +""" +使用 Nodriver 访问 Cloudflare 保护的 audiences.me/torrents.php, +启动时从根目录 cookies.txt 加载 Cookie,采用等待自动通过 + 选择器点击 Turnstile 过验证。 +""" + +import nodriver as uc +from cookies_loader import ( + get_cookies_path, + cookies_file_exists, + is_netscape_format, + load_cookies_nodriver_native, + inject_netscape_cookies, +) + +TARGET_URL = "https://dash.cloudflare.com/sign-up" + + +async def try_click_cf_turnstile(page): + """ + 通过截图+模板匹配定位 CF Turnstile 复选框并点击(与语言无关)。 + 复选框在跨域 iframe + Shadow DOM 内,用选择器点不到,需用坐标点击。 + 依赖:pip install opencv-python + """ + try: + # 用自定义模板 box.png 在截图中匹配复选框区域,返回中心坐标(截图是设备像素) + pos = await page.template_location(template_image="box.png") + if pos is None: + return + x, y = int(pos[0]), int(pos[1]) + # 保存带点击位置标记的截图(便于调试) + try: + await page.save_screenshot("click_position.png") + import cv2 + im = cv2.imread("click_position.png") + if im is not None: + r = max(15, min(im.shape[:2]) // 30) + cv2.circle(im, (x, y), r, (0, 0, 255), 3) + cv2.line(im, (x - r - 5, y), (x + r + 5, y), (0, 0, 255), 2) + cv2.line(im, (x, y - r - 5), (x, y + r + 5), (0, 0, 255), 2) + cv2.imwrite("click_position.png", im) + except Exception: + pass + # 截图坐标是设备像素,CDP 鼠标事件要 CSS 像素,需除以 devicePixelRatio + try: + raw = await page.evaluate("window.devicePixelRatio") + dpr = float(getattr(raw, "value", raw) if hasattr(raw, "value") else raw) if raw is not None else 1.0 + except Exception: + dpr = 1.0 + if dpr <= 0: + dpr = 1.0 + x_css, y_css = x / dpr, y / dpr + # 先模拟人类移动再点击,提高通过率 + await page.mouse_move(x_css, y_css, steps=10) + await page.sleep(0.3) + await page.mouse_click(x_css, y_css) + await page.sleep(3) + except Exception: + pass + + +async def main(): + # 建议非无头模式以提高 CF 通过率 + browser = await uc.start(headless=False) + path = get_cookies_path() + + if cookies_file_exists(): + if is_netscape_format(path): + # Netscape 格式:先打开目标页再注入 cookies,再重新打开使 cookies 生效(避免 reload 导致 Not attached) + page = await browser.get(TARGET_URL) + await inject_netscape_cookies(page, path) + page = await browser.get(TARGET_URL) + else: + # 先尝试 nodriver 原生格式加载,再打开目标页 + await load_cookies_nodriver_native(browser, path) + page = await browser.get(TARGET_URL) + else: + page = await browser.get(TARGET_URL) + + # 等待 CF 自动通过(约 3~8 秒) + await page.sleep(15) + # 若仍为挑战页,尝试点击 Turnstile + await try_click_cf_turnstile(page) + await page.sleep(3) + + # 后续可在此对 page 做操作,或保存截图等 + # await page.save_screenshot("bypass_result.png") + # content = await page.get_content() + + return page + + +if __name__ == "__main__": + uc.loop().run_until_complete(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..819d361 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +nodriver