登录 ok 了
This commit is contained in:
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 6.5 KiB |
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# 账号配置
|
# 账号配置
|
||||||
accounts:
|
accounts:
|
||||||
- username: "9006787894"
|
- username: "用户9006787894"
|
||||||
password: "a778899"
|
password: "a778899"
|
||||||
email: "jtmjcm@bdhg.xyz"
|
email: "jtmjcm@bdhg.xyz"
|
||||||
email_password: "www123"
|
email_password: "www123"
|
||||||
@@ -21,7 +21,7 @@ proxy:
|
|||||||
# 极验配置
|
# 极验配置
|
||||||
geetest:
|
geetest:
|
||||||
# 最大重试次数(遇到点选验证码时重试)
|
# 最大重试次数(遇到点选验证码时重试)
|
||||||
max_retries: 5
|
max_retries: 10
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
log:
|
log:
|
||||||
|
|||||||
+8
-3
@@ -5,6 +5,7 @@ import hashlib
|
|||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from Crypto.Cipher import PKCS1_v1_5
|
from Crypto.Cipher import PKCS1_v1_5
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Cipher import ARC4
|
||||||
|
|
||||||
|
|
||||||
# 斗鱼RSA公钥(从JS中提取)
|
# 斗鱼RSA公钥(从JS中提取)
|
||||||
@@ -18,6 +19,9 @@ o2kz+dJoGnZM0b99VwIDAQAB
|
|||||||
# 斗鱼AES密钥(16位)
|
# 斗鱼AES密钥(16位)
|
||||||
DOUYU_AES_KEY = "1234567890abcdef"
|
DOUYU_AES_KEY = "1234567890abcdef"
|
||||||
|
|
||||||
|
# 斗鱼登录页 cryptoData 使用的 RC4 密钥
|
||||||
|
DOUYU_RC4_KEY = "7TkbRSEWvVWebXbr"
|
||||||
|
|
||||||
|
|
||||||
def md5(text: str) -> str:
|
def md5(text: str) -> str:
|
||||||
"""MD5加密"""
|
"""MD5加密"""
|
||||||
@@ -58,6 +62,7 @@ def encrypt_password(password: str) -> str:
|
|||||||
|
|
||||||
def encrypt_nickname_or_phone(text: str) -> str:
|
def encrypt_nickname_or_phone(text: str) -> str:
|
||||||
"""加密昵称或手机号"""
|
"""加密昵称或手机号"""
|
||||||
# 斗鱼使用特殊的Base64编码
|
key = DOUYU_RC4_KEY.encode('utf-8')
|
||||||
encoded = base64.b64encode(text.encode('utf-8')).decode('utf-8')
|
cipher = ARC4.new(key)
|
||||||
return encoded
|
encrypted = cipher.encrypt(text.encode('utf-8'))
|
||||||
|
return base64.b64encode(encrypted).decode('utf-8')
|
||||||
|
|||||||
+103
-85
@@ -19,10 +19,13 @@ import sys
|
|||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
from geetest import run_solver
|
from geetest import run_solver
|
||||||
from geetest.solver import (
|
from geetest.solver import (
|
||||||
_generate_seed, get_w1, get_w2, get_w3,
|
_generate_seed, get_w1, get_w2,
|
||||||
|
)
|
||||||
|
from geetest.network import (
|
||||||
|
get_js_address,
|
||||||
|
get_c_s,
|
||||||
|
req_fullpage_validate,
|
||||||
)
|
)
|
||||||
from geetest.network import get_js_address, get_c_s, req_slide, get_picture, req_end
|
|
||||||
from geetest.imaging import download_picture
|
|
||||||
|
|
||||||
|
|
||||||
class LoginResult:
|
class LoginResult:
|
||||||
@@ -44,7 +47,16 @@ class DouyuLogin:
|
|||||||
VERIFY_API = "https://passport.douyu.com/wgapi/member/passport/remotelogin/verify"
|
VERIFY_API = "https://passport.douyu.com/wgapi/member/passport/remotelogin/verify"
|
||||||
LOGIN_CALLBACK_API = "https://www.douyu.com/api/passport/login"
|
LOGIN_CALLBACK_API = "https://www.douyu.com/api/passport/login"
|
||||||
WEBLOGIN_API = "https://msg.douyu.com/webLogin"
|
WEBLOGIN_API = "https://msg.douyu.com/webLogin"
|
||||||
REQUEST_TIMEOUT = (3.05, 12)
|
LOGIN_REFERER = (
|
||||||
|
"https://passport.douyu.com/index/login?"
|
||||||
|
"passport_reg_callback=PASSPORT_REG_SUCCESS_CALLBACK&"
|
||||||
|
"passport_login_callback=PASSPORT_LOGIN_SUCCESS_CALLBACK&"
|
||||||
|
"passport_close_callback=PASSPORT_CLOSE_CALLBACK&"
|
||||||
|
"passport_dp_callback=PASSPORT_DP_CALLBACK&"
|
||||||
|
"type=login&client_id=1&"
|
||||||
|
"state=https%3A%2F%2Fwww.douyu.com%2Fdirectory"
|
||||||
|
)
|
||||||
|
REQUEST_TIMEOUT = (10, 30)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -61,7 +73,7 @@ class DouyuLogin:
|
|||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
|
|
||||||
# 初始化代理管理器
|
# 初始化代理管理器
|
||||||
self.proxy_manager = get_proxy_manager(proxy_api_url)
|
self.proxy_manager = get_proxy_manager(proxy_api_url) if proxy_api_url else None
|
||||||
|
|
||||||
self._setup_session()
|
self._setup_session()
|
||||||
|
|
||||||
@@ -74,8 +86,8 @@ class DouyuLogin:
|
|||||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
'Referer': 'https://www.douyu.com/',
|
'Referer': self.LOGIN_REFERER,
|
||||||
'Origin': 'https://www.douyu.com',
|
'Origin': 'https://passport.douyu.com',
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,15 +117,19 @@ class DouyuLogin:
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# 从代理管理器获取代理
|
# 从代理管理器获取代理
|
||||||
new_proxy = self.proxy_manager.get_proxy()
|
if self.proxy_manager:
|
||||||
if new_proxy:
|
new_proxy = self.proxy_manager.get_proxy()
|
||||||
self.session.proxies = {
|
if new_proxy:
|
||||||
'http': new_proxy,
|
self.session.proxies = {
|
||||||
'https': new_proxy,
|
'http': new_proxy,
|
||||||
}
|
'https': new_proxy,
|
||||||
|
}
|
||||||
|
|
||||||
def _refresh_proxy(self) -> Optional[str]:
|
def _refresh_proxy(self) -> Optional[str]:
|
||||||
"""刷新代理IP"""
|
"""刷新代理IP"""
|
||||||
|
if not self.proxy_manager:
|
||||||
|
return None
|
||||||
|
|
||||||
new_proxy = self.proxy_manager.get_proxy()
|
new_proxy = self.proxy_manager.get_proxy()
|
||||||
if new_proxy:
|
if new_proxy:
|
||||||
self._apply_proxy(new_proxy)
|
self._apply_proxy(new_proxy)
|
||||||
@@ -125,32 +141,47 @@ class DouyuLogin:
|
|||||||
parsed = urlsplit(url)
|
parsed = urlsplit(url)
|
||||||
return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))
|
return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))
|
||||||
|
|
||||||
def _request(self, method: str, url: str, **kwargs) -> requests.Response:
|
def _request(self, method: str, url: str, max_retries: int = 3, **kwargs) -> requests.Response:
|
||||||
"""统一发送请求,附带分段超时和更明确的错误信息。"""
|
"""
|
||||||
|
统一发送请求,附带分段超时和更明确的错误信息。
|
||||||
|
代理连接失败时自动重试获取新的代理IP。
|
||||||
|
"""
|
||||||
timeout = kwargs.pop('timeout', self.timeout)
|
timeout = kwargs.pop('timeout', self.timeout)
|
||||||
safe_url = self._safe_url(url)
|
safe_url = self._safe_url(url)
|
||||||
started = time.monotonic()
|
|
||||||
|
|
||||||
try:
|
for attempt in range(max_retries):
|
||||||
response = self.session.request(method, url, timeout=timeout, **kwargs)
|
started = time.monotonic()
|
||||||
except requests.Timeout as exc:
|
|
||||||
elapsed = time.monotonic() - started
|
|
||||||
raise TimeoutError(
|
|
||||||
f"{method.upper()} {safe_url} 超时,耗时 {elapsed:.1f}s,"
|
|
||||||
f"timeout={timeout}"
|
|
||||||
) from exc
|
|
||||||
except requests.RequestException as exc:
|
|
||||||
elapsed = time.monotonic() - started
|
|
||||||
raise ConnectionError(
|
|
||||||
f"{method.upper()} {safe_url} 请求失败,耗时 {elapsed:.1f}s: {exc}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
elapsed = time.monotonic() - started
|
try:
|
||||||
logger.debug(
|
response = self.session.request(method, url, timeout=timeout, **kwargs)
|
||||||
f"{method.upper()} {safe_url} -> {response.status_code} "
|
elapsed = time.monotonic() - started
|
||||||
f"({elapsed:.2f}s)"
|
logger.debug(
|
||||||
)
|
f"{method.upper()} {safe_url} -> {response.status_code} "
|
||||||
return response
|
f"({elapsed:.2f}s)"
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
except requests.Timeout as exc:
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
raise TimeoutError(
|
||||||
|
f"{method.upper()} {safe_url} 超时,耗时 {elapsed:.1f}s,"
|
||||||
|
f"timeout={timeout}"
|
||||||
|
) from exc
|
||||||
|
except requests.ProxyError as exc:
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
logger.warning(f"代理连接失败,尝试 {attempt + 1}/{max_retries}: {exc}")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
# 刷新代理
|
||||||
|
self._refresh_proxy()
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
raise ConnectionError(
|
||||||
|
f"{method.upper()} {safe_url} 代理连接失败,已重试 {max_retries} 次"
|
||||||
|
) from exc
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
raise ConnectionError(
|
||||||
|
f"{method.upper()} {safe_url} 请求失败,耗时 {elapsed:.1f}s: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict:
|
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict:
|
||||||
"""请求 JSON 接口,并在响应异常时输出可定位的信息。"""
|
"""请求 JSON 接口,并在响应异常时输出可定位的信息。"""
|
||||||
@@ -181,8 +212,8 @@ class DouyuLogin:
|
|||||||
logger.info("步骤1: 第一次登录,获取极验参数...")
|
logger.info("步骤1: 第一次登录,获取极验参数...")
|
||||||
gt, challenge, code_token, initial_cookies = self._first_login()
|
gt, challenge, code_token, initial_cookies = self._first_login()
|
||||||
|
|
||||||
# 2️⃣ 极验滑块验证
|
# 2️⃣ 极验 fullpage 验证
|
||||||
logger.info("步骤2: 极验滑块验证...")
|
logger.info("步骤2: 极验 fullpage 验证...")
|
||||||
validate, seccode = self._solve_geetest(gt, challenge)
|
validate, seccode = self._solve_geetest(gt, challenge)
|
||||||
|
|
||||||
# 3️⃣ 第二次登录(带极验)
|
# 3️⃣ 第二次登录(带极验)
|
||||||
@@ -229,7 +260,12 @@ class DouyuLogin:
|
|||||||
'password': encrypted_password,
|
'password': encrypted_password,
|
||||||
'biz_type': '1',
|
'biz_type': '1',
|
||||||
'room_id': '0',
|
'room_id': '0',
|
||||||
'redirect_url': 'https://www.douyu.com/',
|
'redirect_url': self.LOGIN_REFERER,
|
||||||
|
't': str(int(time.time() * 1000)),
|
||||||
|
'client_id': '1',
|
||||||
|
'did': '',
|
||||||
|
'lang': '',
|
||||||
|
'isMultiAccount': '0',
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = self._request_json(
|
payload = self._request_json(
|
||||||
@@ -262,7 +298,7 @@ class DouyuLogin:
|
|||||||
|
|
||||||
def _solve_geetest(self, gt: str, challenge: str) -> Tuple[str, str]:
|
def _solve_geetest(self, gt: str, challenge: str) -> Tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
解决极验滑块验证(带重试机制)
|
解决极验 fullpage 验证(带重试机制)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
gt: 极验gt参数
|
gt: 极验gt参数
|
||||||
@@ -271,70 +307,41 @@ class DouyuLogin:
|
|||||||
Returns:
|
Returns:
|
||||||
(validate, seccode)
|
(validate, seccode)
|
||||||
"""
|
"""
|
||||||
logger.info("开始极验滑块验证...")
|
logger.info("开始极验 fullpage 验证...")
|
||||||
|
|
||||||
for attempt in range(self.max_geetest_retries):
|
for attempt in range(self.max_geetest_retries):
|
||||||
try:
|
try:
|
||||||
logger.info(f"极验验证尝试 {attempt + 1}/{self.max_geetest_retries}")
|
logger.info(f"极验验证尝试 {attempt + 1}/{self.max_geetest_retries}")
|
||||||
|
|
||||||
# 使用geetest-v3-silde-crack的完整流程
|
# 按斗鱼登录页 HAR:fullpage 智能检测流程,不进入图片滑块。
|
||||||
str_16 = _generate_seed()
|
str_16 = _generate_seed()
|
||||||
|
proxies = dict(self.session.proxies)
|
||||||
|
|
||||||
# 获取JS地址
|
# 获取JS地址
|
||||||
get_js_address(gt)
|
get_js_address(gt, proxies=proxies)
|
||||||
|
|
||||||
# 获取第一个w值
|
# 获取第一个w值
|
||||||
w1 = get_w1(gt, challenge, str_16)
|
w1 = get_w1(gt, challenge, str_16)
|
||||||
|
|
||||||
# 获取c和s
|
# 获取c和s
|
||||||
c, s = get_c_s(gt, challenge, w1)
|
c, s = get_c_s(gt, challenge, w1, proxies=proxies)
|
||||||
|
|
||||||
# 获取第二个w值
|
# 获取第二个w值
|
||||||
w2 = get_w2(gt, challenge, c, s, str_16)
|
w2 = get_w2(gt, challenge, c, s, str_16)
|
||||||
|
|
||||||
# 请求滑块
|
# HAR 中 ajax.php 直接返回 validate。
|
||||||
req_slide(gt, challenge, w2)
|
result = req_fullpage_validate(gt, challenge, w2, proxies=proxies)
|
||||||
|
|
||||||
# 获取图片
|
# 从 fullpage ajax.php 响应中提取 validate
|
||||||
result = get_picture(gt, challenge)
|
|
||||||
|
|
||||||
# 检查返回的数据结构
|
|
||||||
if len(result) == 6:
|
|
||||||
bg, fullbg, c, s, slice, challenge_new = result
|
|
||||||
else:
|
|
||||||
# 如果返回的数据结构不对,可能是点选验证码,需要重试
|
|
||||||
logger.warning(f"返回数据结构异常,可能是点选验证码,重试中...")
|
|
||||||
# 刷新代理
|
|
||||||
self._refresh_proxy()
|
|
||||||
time.sleep(1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 检查是否是滑块验证码(bg和slice不为空)
|
|
||||||
if not bg or not slice:
|
|
||||||
logger.warning(f"未获取到滑块图片,可能是点选验证码,重试中...")
|
|
||||||
# 刷新代理
|
|
||||||
self._refresh_proxy()
|
|
||||||
time.sleep(1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 识别缺口位置
|
|
||||||
hkjl = download_picture(bg, fullbg, slice)
|
|
||||||
|
|
||||||
# 获取第三个w值
|
|
||||||
w3 = get_w3(str_16, challenge_new, hkjl, c, s, gt)
|
|
||||||
|
|
||||||
# 最后验证
|
|
||||||
result = req_end(gt, challenge_new, w3)
|
|
||||||
|
|
||||||
# 从req_end的响应中提取validate
|
|
||||||
if isinstance(result, dict):
|
if isinstance(result, dict):
|
||||||
validate = result.get('validate', '')
|
data = result.get('data', {})
|
||||||
success = result.get('success', 0)
|
validate = data.get('validate', '') or result.get('validate', '')
|
||||||
message = result.get('message', '')
|
success = data.get('result') == 'success' or result.get('success') == 1
|
||||||
|
message = data.get('result') or result.get('message', '')
|
||||||
|
|
||||||
if success == 1 and validate:
|
if success and validate:
|
||||||
seccode = f"{validate}|jordan"
|
seccode = f"{validate}|jordan"
|
||||||
logger.success(f"极验滑块验证成功! validate={validate[:20]}...")
|
logger.success(f"极验 fullpage 验证成功! validate={validate[:20]}...")
|
||||||
return validate, seccode
|
return validate, seccode
|
||||||
else:
|
else:
|
||||||
logger.warning(f"极验验证失败: {message},重试中...")
|
logger.warning(f"极验验证失败: {message},重试中...")
|
||||||
@@ -346,14 +353,15 @@ class DouyuLogin:
|
|||||||
validate = str(result)
|
validate = str(result)
|
||||||
if validate:
|
if validate:
|
||||||
seccode = f"{validate}|jordan"
|
seccode = f"{validate}|jordan"
|
||||||
logger.success(f"极验滑块验证成功! validate={validate[:20]}...")
|
logger.success(f"极验 fullpage 验证成功! validate={validate[:20]}...")
|
||||||
return validate, seccode
|
return validate, seccode
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"极验验证异常: {e},重试中...")
|
logger.warning(f"极验验证异常: {e},重试中...")
|
||||||
# 刷新代理
|
# 刷新代理
|
||||||
self._refresh_proxy()
|
self._refresh_proxy()
|
||||||
time.sleep(1)
|
# 增加等待时间,避免请求过于频繁
|
||||||
|
time.sleep(2)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raise ValueError(f"极验验证失败,已重试 {self.max_geetest_retries} 次")
|
raise ValueError(f"极验验证失败,已重试 {self.max_geetest_retries} 次")
|
||||||
@@ -363,6 +371,13 @@ class DouyuLogin:
|
|||||||
"""
|
"""
|
||||||
第二次登录(带极验验证)
|
第二次登录(带极验验证)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
gt: 极验gt参数
|
||||||
|
challenge: 第一次登录返回的challenge
|
||||||
|
validate: 极验验证返回的validate
|
||||||
|
seccode: 极验验证返回的seccode
|
||||||
|
code_token: 第一次登录返回的code_token
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
remote_code: 用于邮箱验证的code
|
remote_code: 用于邮箱验证的code
|
||||||
"""
|
"""
|
||||||
@@ -370,6 +385,7 @@ class DouyuLogin:
|
|||||||
encrypted_password = encrypt_password(self.account.password)
|
encrypted_password = encrypt_password(self.account.password)
|
||||||
|
|
||||||
# 参考HAR文件中的完整参数
|
# 参考HAR文件中的完整参数
|
||||||
|
# 注意:geetest_challenge应该使用第一次登录返回的challenge
|
||||||
data = {
|
data = {
|
||||||
'type': '1',
|
'type': '1',
|
||||||
'nicknameOrPhoneEncrypt': encrypted_username,
|
'nicknameOrPhoneEncrypt': encrypted_username,
|
||||||
@@ -386,7 +402,7 @@ class DouyuLogin:
|
|||||||
'code_data[geetest_seccode]': seccode,
|
'code_data[geetest_seccode]': seccode,
|
||||||
'code_data[gt_version]': 'v3',
|
'code_data[gt_version]': 'v3',
|
||||||
'code_data[code]': '',
|
'code_data[code]': '',
|
||||||
'redirect_url': 'https://www.douyu.com/',
|
'redirect_url': self.LOGIN_REFERER,
|
||||||
't': str(int(time.time() * 1000)),
|
't': str(int(time.time() * 1000)),
|
||||||
'client_id': '1',
|
'client_id': '1',
|
||||||
'did': '',
|
'did': '',
|
||||||
@@ -395,6 +411,8 @@ class DouyuLogin:
|
|||||||
'biz_type': '1',
|
'biz_type': '1',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug(f"第二次登录参数: challenge={challenge[:20]}..., validate={validate[:20]}...")
|
||||||
|
|
||||||
payload = self._request_json(
|
payload = self._request_json(
|
||||||
'post',
|
'post',
|
||||||
self.LOGIN_API,
|
self.LOGIN_API,
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 6.2 KiB |
+84
-43
@@ -2,9 +2,40 @@ import time
|
|||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Tuple
|
from typing import Mapping, Optional, Tuple
|
||||||
|
|
||||||
REQUEST_TIMEOUT = (3.05, 12)
|
REQUEST_TIMEOUT = (3.05, 12)
|
||||||
|
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:
|
def _parse_json_response(response: requests.Response, source: str) -> dict:
|
||||||
@@ -83,18 +114,18 @@ def get_challenge_gt() -> Tuple[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_js_address(gt:str) -> dict:
|
def get_js_address(gt: str, proxies: Optional[Mapping[str, str]] = None) -> dict:
|
||||||
headers = {
|
headers = {
|
||||||
'accept': '*/*',
|
'accept': '*/*',
|
||||||
'accept-language': 'zh-CN,zh;q=0.9',
|
'accept-language': 'zh-CN,zh;q=0.9',
|
||||||
'referer': 'https://demos.geetest.com/',
|
'referer': PASSPORT_REFERER,
|
||||||
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
|
'sec-ch-ua': '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
|
||||||
'sec-ch-ua-mobile': '?0',
|
'sec-ch-ua-mobile': '?0',
|
||||||
'sec-ch-ua-platform': '"Windows"',
|
'sec-ch-ua-platform': '"macOS"',
|
||||||
'sec-fetch-dest': 'script',
|
'sec-fetch-dest': 'script',
|
||||||
'sec-fetch-mode': 'no-cors',
|
'sec-fetch-mode': 'no-cors',
|
||||||
'sec-fetch-site': 'same-site',
|
'sec-fetch-site': 'cross-site',
|
||||||
'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',
|
'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 = {
|
params = {
|
||||||
@@ -102,62 +133,72 @@ def get_js_address(gt:str) -> dict:
|
|||||||
'callback': 'geetest_' + str(int(round(time.time() * 1000))),
|
'callback': 'geetest_' + str(int(round(time.time() * 1000))),
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.get(
|
response = _get(
|
||||||
'https://apiv6.geetest.com/gettype.php',
|
'https://api.geetest.com/gettype.php',
|
||||||
params=params,
|
params=params,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=REQUEST_TIMEOUT,
|
proxies=proxies,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
match = re.search(r'\((.*)\)$', response.text)
|
return _parse_jsonp_response(response, "极验 gettype")
|
||||||
if match:
|
|
||||||
json_str = match.group(1)
|
|
||||||
return json.loads(json_str)
|
|
||||||
else:
|
|
||||||
raise ValueError("无法解析 JSONP 响应")
|
|
||||||
|
|
||||||
def get_c_s(gt:str, challenge:str, w:str) -> Tuple[list[int],str]:
|
def get_c_s(
|
||||||
|
gt: str,
|
||||||
|
challenge: str,
|
||||||
|
w: str,
|
||||||
|
proxies: Optional[Mapping[str, str]] = None,
|
||||||
|
) -> Tuple[list[int], str]:
|
||||||
headers = {
|
headers = {
|
||||||
'accept': '*/*',
|
'accept': '*/*',
|
||||||
'accept-language': 'zh-CN,zh;q=0.9',
|
'accept-language': 'zh-CN,zh;q=0.9',
|
||||||
'referer': 'https://demos.geetest.com/',
|
'referer': PASSPORT_REFERER,
|
||||||
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
|
'sec-ch-ua': '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
|
||||||
'sec-ch-ua-mobile': '?0',
|
'sec-ch-ua-mobile': '?0',
|
||||||
'sec-ch-ua-platform': '"Windows"',
|
'sec-ch-ua-platform': '"macOS"',
|
||||||
'sec-fetch-dest': 'script',
|
'sec-fetch-dest': 'script',
|
||||||
'sec-fetch-mode': 'no-cors',
|
'sec-fetch-mode': 'no-cors',
|
||||||
'sec-fetch-site': 'same-site',
|
'sec-fetch-site': 'cross-site',
|
||||||
'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',
|
'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 = requests.get(
|
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(
|
'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))),
|
int(round(time.time() * 1000))),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=REQUEST_TIMEOUT,
|
proxies=proxies,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
data = _parse_jsonp_response(response, "极验 get.php")
|
||||||
|
return data['data']['c'], data['data']['s']
|
||||||
|
|
||||||
# 检查是否是错误响应
|
|
||||||
if response.text.startswith('{'):
|
|
||||||
# 直接JSON响应
|
|
||||||
data = json.loads(response.text)
|
|
||||||
if 'status' in data and data['status'] == 'error':
|
|
||||||
raise ValueError(f"极验请求失败: {data.get('user_error', data.get('error', '未知错误'))}")
|
|
||||||
|
|
||||||
match = re.search(r'\((.*)\)$', response.text)
|
def req_fullpage_validate(
|
||||||
if match:
|
gt: str,
|
||||||
json_str = match.group(1)
|
challenge: str,
|
||||||
data = json.loads(json_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(
|
||||||
if 'status' in data and data['status'] == 'error':
|
'https://api.geetest.com/ajax.php?gt=' + gt + '&challenge=' + challenge + '&lang=zh-cn&pt=0&client_type=web&w=' + w + '&callback=geetest_' + str(
|
||||||
raise ValueError(f"极验请求失败: {data.get('user_error', data.get('error', '未知错误'))}")
|
int(round(time.time() * 1000))),
|
||||||
|
headers=headers,
|
||||||
return data['data']['c'], data['data']['s']
|
proxies=proxies,
|
||||||
else:
|
)
|
||||||
raise ValueError("无法解析 JSONP 响应")
|
return _parse_jsonp_response(response, "极验 fullpage ajax.php")
|
||||||
|
|
||||||
def req_slide(gt:str, challenge:str, w2:str) -> None:
|
def req_slide(gt:str, challenge:str, w2:str) -> None:
|
||||||
headers = {
|
headers = {
|
||||||
|
|||||||
Reference in New Issue
Block a user