import json import re import time from collections.abc import Mapping import requests from loguru import logger REQUEST_TIMEOUT = (10, 30) PASSPORT_REFERER = "https://passport.douyu.com/" def _get( url: str, *, params: dict | None = None, headers: dict | None = None, proxies: Mapping[str, str] | None = None, ) -> requests.Response: """发送极验 GET 请求,确保使用同一个代理出口。""" return requests.get( url, params=params, headers=headers, proxies=dict(proxies or {}), timeout=REQUEST_TIMEOUT, ) def _parse_jsonp_response(response: requests.Response, source: str) -> dict: """解析极验 JSONP 响应。""" response.raise_for_status() match = re.search(r"\((.*)\)$", response.text) if not match: raise ValueError(f"{source} 无法解析 JSONP 响应") data = json.loads(match.group(1)) if data.get("status") == "error": raise ValueError( f"{source} 失败: {data.get('user_error', data.get('error', '未知错误'))}" ) return data def _parse_json_response(response: requests.Response, source: str) -> dict: response.raise_for_status() body = response.text.strip() if not body: raise ValueError(f"{source} 返回为空,无法解析 JSON") try: return response.json() except json.JSONDecodeError as exc: preview = body[:200].replace("\n", "\\n") raise ValueError(f"{source} 返回的不是有效 JSON: {preview}") from exc def get_challenge_gt_bak() -> tuple[str, str]: headers = { "accept": "application/json, text/javascript, */*; q=0.01", "accept-language": "zh-CN,zh;q=0.9", "priority": "u=1, i", "referer": "https://demos.geetest.com/slide-float.html", "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", "x-requested-with": "XMLHttpRequest", } params = { "t": str(round(time.time() * 1000)), } response = requests.get( "https://demos.geetest.com/gt/register-slide", params=params, headers=headers, timeout=10, ) data = _parse_json_response(response, "极验 demo register-slide") return data["gt"], data["challenge"] def get_challenge_gt() -> tuple[str, str]: headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", "Accept": "application/json, text/javascript, */*; q=0.01", "Content-Type": "application/x-www-form-urlencoded", "referer": "https://passport.douyu.com/member/login?state=https%3A%2F%2Fwww.douyu.com%2Fmember%2FcpSecurity%2Fcheck_geetest_status", } data = { "type": "1", "nicknameOrPhoneEncrypt": "03ILaBwtmmCm0A==", "password": "57219dddec71c31b7647683fa5306103", "biz_type": "1", } response = requests.post( "https://passport.douyu.com/wgapi/member/passport/login", headers=headers, data=data, timeout=10, ) payload = _parse_json_response(response, "斗鱼登录接口") try: code_data = payload["data"]["geetest"]["code_data"] return code_data["gt"], code_data["challenge"] except KeyError as exc: preview = json.dumps(payload, ensure_ascii=False)[:300] raise ValueError(f"斗鱼登录接口返回中缺少极验参数: {preview}") from exc def get_js_address(gt: str, proxies: Mapping[str, str] | None = None) -> dict: headers = { "accept": "*/*", "accept-language": "zh-CN,zh;q=0.9", "referer": PASSPORT_REFERER, "sec-ch-ua": '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"macOS"', "sec-fetch-dest": "script", "sec-fetch-mode": "no-cors", "sec-fetch-site": "cross-site", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36", } params = { "gt": gt, "callback": "geetest_" + str(round(time.time() * 1000)), } response = _get( "https://api.geetest.com/gettype.php", params=params, headers=headers, proxies=proxies, ) return _parse_jsonp_response(response, "极验 gettype") def get_c_s( gt: str, challenge: str, w: str, proxies: Mapping[str, str] | None = None, ) -> tuple[list[int], str]: headers = { "accept": "*/*", "accept-language": "zh-CN,zh;q=0.9", "referer": PASSPORT_REFERER, "sec-ch-ua": '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"macOS"', "sec-fetch-dest": "script", "sec-fetch-mode": "no-cors", "sec-fetch-site": "cross-site", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36", } response = _get( "https://api.geetest.com/get.php?gt=" + gt + "&challenge=" + challenge + "&lang=zh-cn&pt=0&client_type=web&w=" + w + "&callback=geetest_" + str(round(time.time() * 1000)), headers=headers, proxies=proxies, ) data = _parse_jsonp_response(response, "极验 get.php") return data["data"]["c"], data["data"]["s"] def req_fullpage_validate( gt: str, challenge: str, w: str, proxies: Mapping[str, str] | None = None, ) -> dict: """HAR 中的 fullpage 最终校验,成功后直接返回 validate。""" headers = { "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", "Referer": PASSPORT_REFERER, "Sec-Fetch-Dest": "script", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "cross-site", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36", "sec-ch-ua": '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"macOS"', } response = _get( "https://api.geetest.com/ajax.php?gt=" + gt + "&challenge=" + challenge + "&lang=zh-cn&pt=0&client_type=web&w=" + w + "&callback=geetest_" + str(round(time.time() * 1000)), headers=headers, proxies=proxies, ) return _parse_jsonp_response(response, "极验 fullpage ajax.php") def req_slide(gt: str, challenge: str, w2: str) -> None: headers = { "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", "Referer": "https://demos.geetest.com/", "Sec-Fetch-Dest": "script", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Storage-Access": "active", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', } response = requests.get( "https://api.geevisit.com/ajax.php?gt=" + gt + "&challenge=" + challenge + "&lang=zh-cn&pt=0&client_type=web&w=" + w2 + "&callback=geetest_" + str(round(time.time() * 1000)), headers=headers, timeout=REQUEST_TIMEOUT, ) response.raise_for_status() def get_picture(gt: str, challenge: str) -> tuple[str, str, list[int], str, str, str]: headers = { "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", "Referer": "https://demos.geetest.com/", "Sec-Fetch-Dest": "script", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Storage-Access": "active", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', } params = { "is_next": "true", "type": "slide3", "gt": gt, "challenge": challenge, "lang": "zh-cn", "https": "true", "protocol": "https://", "offline": "false", "product": "embed", "api_server": "api.geevisit.com", "isPC": "true", "autoReset": "true", "width": "100%", "callback": "geetest_" + str(round(time.time() * 1000)), } response = requests.get( "https://api.geevisit.com/get.php", params=params, headers=headers, timeout=REQUEST_TIMEOUT, ) response.raise_for_status() logger.debug("极验 get.php 原始响应: {}", response.text[:500]) match = re.search(r"\((.*)\)$", response.text) if match: json_str = match.group(1) data = json.loads(json_str) if "data" in data and isinstance(data["data"], dict): # 新版极验格式 inner_data = data["data"] return ( inner_data.get("bg", ""), inner_data.get("fullbg", ""), inner_data.get("c", []), inner_data.get("s", ""), inner_data.get("slice", ""), inner_data.get("challenge", ""), ) else: # 旧版极验格式 return ( data.get("bg", ""), data.get("fullbg", ""), data.get("c", []), data.get("s", ""), data.get("slice", ""), data.get("challenge", ""), ) else: raise ValueError("无法解析 JSONP 响应") def req_end(gt: str, challenge: str, w: str) -> dict: headers = { "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", "Referer": "https://demos.geetest.com/", "Sec-Fetch-Dest": "script", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Storage-Access": "active", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', } response = requests.get( "https://api.geevisit.com/ajax.php?gt=" + gt + "&challenge=" + challenge + "&lang=zh-cn&%24_BCm=0&client_type=web&w=" + w + "&callback=geetest_" + str(round(time.time() * 1000)), headers=headers, timeout=REQUEST_TIMEOUT, ) response.raise_for_status() logger.debug("极验 ajax.php 原始响应: {}", response.text[:500]) match = re.search(r"\((.*)\)$", response.text) if match: json_str = match.group(1) data = json.loads(json_str) logger.debug("极验验证响应: {}", data) # 检查验证是否成功 if data.get("success") == 1: return data else: # 如果验证失败,返回错误信息 return { "success": 0, "message": data.get("message", "验证失败"), "validate": "", } else: raise ValueError("无法解析 JSONP 响应")