import time import requests import json import re from typing import Mapping, Optional, Tuple REQUEST_TIMEOUT = (10, 30) PASSPORT_REFERER = "https://passport.douyu.com/" def _get( url: str, *, params: Optional[dict] = None, headers: Optional[dict] = None, proxies: Optional[Mapping[str, str]] = 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(int(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: Optional[Mapping[str, str]] = 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(int(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: Optional[Mapping[str, str]] = 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( int(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: Optional[Mapping[str, str]] = 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( int(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( int(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(int(round(time.time() * 1000))), } response = requests.get( 'https://api.geevisit.com/get.php', params=params, headers=headers, timeout=REQUEST_TIMEOUT, ) response.raise_for_status() print(response.text) 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(int(round(time.time() * 1000))), headers=headers, timeout=REQUEST_TIMEOUT, ) response.raise_for_status() print(response.text) match = re.search(r'\((.*)\)$', response.text) if match: json_str = match.group(1) data = json.loads(json_str) print(f"极验验证响应: {data}") # 检查验证是否成功 if data.get('success') == 1: return data else: # 如果验证失败,返回错误信息 return { 'success': 0, 'message': data.get('message', '验证失败'), 'validate': '' } else: raise ValueError("无法解析 JSONP 响应")