登录 ok 了

This commit is contained in:
yml2213
2026-06-21 22:48:14 +08:00
parent 828f9889ed
commit 4dca691890
8 changed files with 197 additions and 133 deletions
+103 -85
View File
@@ -19,10 +19,13 @@ import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
from geetest import run_solver
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:
@@ -44,7 +47,16 @@ class DouyuLogin:
VERIFY_API = "https://passport.douyu.com/wgapi/member/passport/remotelogin/verify"
LOGIN_CALLBACK_API = "https://www.douyu.com/api/passport/login"
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__(
self,
@@ -61,7 +73,7 @@ class DouyuLogin:
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()
@@ -74,8 +86,8 @@ class DouyuLogin:
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Referer': 'https://www.douyu.com/',
'Origin': 'https://www.douyu.com',
'Referer': self.LOGIN_REFERER,
'Origin': 'https://passport.douyu.com',
'X-Requested-With': 'XMLHttpRequest',
})
@@ -105,15 +117,19 @@ class DouyuLogin:
}
else:
# 从代理管理器获取代理
new_proxy = self.proxy_manager.get_proxy()
if new_proxy:
self.session.proxies = {
'http': new_proxy,
'https': new_proxy,
}
if self.proxy_manager:
new_proxy = self.proxy_manager.get_proxy()
if new_proxy:
self.session.proxies = {
'http': new_proxy,
'https': new_proxy,
}
def _refresh_proxy(self) -> Optional[str]:
"""刷新代理IP"""
if not self.proxy_manager:
return None
new_proxy = self.proxy_manager.get_proxy()
if new_proxy:
self._apply_proxy(new_proxy)
@@ -125,32 +141,47 @@ class DouyuLogin:
parsed = urlsplit(url)
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)
safe_url = self._safe_url(url)
started = time.monotonic()
try:
response = self.session.request(method, url, timeout=timeout, **kwargs)
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
for attempt in range(max_retries):
started = time.monotonic()
elapsed = time.monotonic() - started
logger.debug(
f"{method.upper()} {safe_url} -> {response.status_code} "
f"({elapsed:.2f}s)"
)
return response
try:
response = self.session.request(method, url, timeout=timeout, **kwargs)
elapsed = time.monotonic() - started
logger.debug(
f"{method.upper()} {safe_url} -> {response.status_code} "
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:
"""请求 JSON 接口,并在响应异常时输出可定位的信息。"""
@@ -181,8 +212,8 @@ class DouyuLogin:
logger.info("步骤1: 第一次登录,获取极验参数...")
gt, challenge, code_token, initial_cookies = self._first_login()
# 2️⃣ 极验滑块验证
logger.info("步骤2: 极验滑块验证...")
# 2️⃣ 极验 fullpage 验证
logger.info("步骤2: 极验 fullpage 验证...")
validate, seccode = self._solve_geetest(gt, challenge)
# 3️⃣ 第二次登录(带极验)
@@ -229,7 +260,12 @@ class DouyuLogin:
'password': encrypted_password,
'biz_type': '1',
'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(
@@ -262,7 +298,7 @@ class DouyuLogin:
def _solve_geetest(self, gt: str, challenge: str) -> Tuple[str, str]:
"""
解决极验滑块验证(带重试机制)
解决极验 fullpage 验证(带重试机制)
Args:
gt: 极验gt参数
@@ -271,70 +307,41 @@ class DouyuLogin:
Returns:
(validate, seccode)
"""
logger.info("开始极验滑块验证...")
logger.info("开始极验 fullpage 验证...")
for attempt in range(self.max_geetest_retries):
try:
logger.info(f"极验验证尝试 {attempt + 1}/{self.max_geetest_retries}")
# 使用geetest-v3-silde-crack的完整流程
# 按斗鱼登录页 HAR:fullpage 智能检测流程,不进入图片滑块。
str_16 = _generate_seed()
proxies = dict(self.session.proxies)
# 获取JS地址
get_js_address(gt)
get_js_address(gt, proxies=proxies)
# 获取第一个w值
w1 = get_w1(gt, challenge, str_16)
# 获取c和s
c, s = get_c_s(gt, challenge, w1)
c, s = get_c_s(gt, challenge, w1, proxies=proxies)
# 获取第二个w值
w2 = get_w2(gt, challenge, c, s, str_16)
# 请求滑块
req_slide(gt, challenge, w2)
# HAR 中 ajax.php 直接返回 validate。
result = req_fullpage_validate(gt, challenge, w2, proxies=proxies)
# 获取图片
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
# 从 fullpage ajax.php 响应中提取 validate
if isinstance(result, dict):
validate = result.get('validate', '')
success = result.get('success', 0)
message = result.get('message', '')
data = result.get('data', {})
validate = data.get('validate', '') or result.get('validate', '')
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"
logger.success(f"极验滑块验证成功! validate={validate[:20]}...")
logger.success(f"极验 fullpage 验证成功! validate={validate[:20]}...")
return validate, seccode
else:
logger.warning(f"极验验证失败: {message},重试中...")
@@ -346,14 +353,15 @@ class DouyuLogin:
validate = str(result)
if validate:
seccode = f"{validate}|jordan"
logger.success(f"极验滑块验证成功! validate={validate[:20]}...")
logger.success(f"极验 fullpage 验证成功! validate={validate[:20]}...")
return validate, seccode
except Exception as e:
logger.warning(f"极验验证异常: {e},重试中...")
# 刷新代理
self._refresh_proxy()
time.sleep(1)
# 增加等待时间,避免请求过于频繁
time.sleep(2)
continue
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:
remote_code: 用于邮箱验证的code
"""
@@ -370,6 +385,7 @@ class DouyuLogin:
encrypted_password = encrypt_password(self.account.password)
# 参考HAR文件中的完整参数
# 注意:geetest_challenge应该使用第一次登录返回的challenge
data = {
'type': '1',
'nicknameOrPhoneEncrypt': encrypted_username,
@@ -386,7 +402,7 @@ class DouyuLogin:
'code_data[geetest_seccode]': seccode,
'code_data[gt_version]': 'v3',
'code_data[code]': '',
'redirect_url': 'https://www.douyu.com/',
'redirect_url': self.LOGIN_REFERER,
't': str(int(time.time() * 1000)),
'client_id': '1',
'did': '',
@@ -395,6 +411,8 @@ class DouyuLogin:
'biz_type': '1',
}
logger.debug(f"第二次登录参数: challenge={challenge[:20]}..., validate={validate[:20]}...")
payload = self._request_json(
'post',
self.LOGIN_API,