拆分补CK逻辑
This commit is contained in:
@@ -0,0 +1,137 @@
|
|||||||
|
"""斗鱼 Web Cookie 补齐逻辑。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
class CookieEnricher:
|
||||||
|
"""补齐登录后 Web 侧需要的 Cookie。"""
|
||||||
|
|
||||||
|
CSRF_API = "https://www.douyu.com/japi/carnival/nc/common/generateCsrf"
|
||||||
|
CSRF_REFERER = "https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId=9263298"
|
||||||
|
ACF_CCN_API = "https://www.douyu.com/curl/csrfNlApi/getCsrfCookie"
|
||||||
|
ACF_CCN_REFERER = "https://www.douyu.com/pages/ord-task-center?clientType=web&panelSource=1&rid=0"
|
||||||
|
TIMEOUT = (5, 10)
|
||||||
|
RETRIES = 3
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
session: requests.Session,
|
||||||
|
request_func: Callable[..., requests.Response],
|
||||||
|
ensure_not_stopped: Callable[[], None],
|
||||||
|
sleep_interruptible: Callable[[float], None],
|
||||||
|
):
|
||||||
|
self.session = session
|
||||||
|
self.request = request_func
|
||||||
|
self.ensure_not_stopped = ensure_not_stopped
|
||||||
|
self.sleep_interruptible = sleep_interruptible
|
||||||
|
|
||||||
|
def enrich_with_retry(self, max_attempts: int = RETRIES) -> None:
|
||||||
|
"""独立重试补齐 cvl_csrf_token 和 acf_ccn,不触发整条登录链路重跑。"""
|
||||||
|
last_error: Exception | None = None
|
||||||
|
for attempt in range(1, max_attempts + 1):
|
||||||
|
self.ensure_not_stopped()
|
||||||
|
try:
|
||||||
|
logger.info(f"补CK尝试 {attempt}/{max_attempts}...")
|
||||||
|
self.generate_csrf_cookie()
|
||||||
|
self.generate_acf_ccn_cookie()
|
||||||
|
logger.info("补CK完成")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
logger.warning(f"补CK失败 {attempt}/{max_attempts}: {e}")
|
||||||
|
if attempt < max_attempts:
|
||||||
|
self.sleep_interruptible(1)
|
||||||
|
raise ValueError(f"已重试 {max_attempts} 次仍未补齐CK: {last_error}") from last_error
|
||||||
|
|
||||||
|
def generate_csrf_cookie(self) -> str:
|
||||||
|
"""访问 generateCsrf 接口,从 Set-Cookie 中同步 cvl_csrf_token。"""
|
||||||
|
logger.info("补齐CSRF Cookie...")
|
||||||
|
headers = {
|
||||||
|
'Accept': 'application/json, text/plain, */*',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.6,en;q=0.5',
|
||||||
|
'Origin': 'https://www.douyu.com',
|
||||||
|
'Referer': self.CSRF_REFERER,
|
||||||
|
'Sec-Fetch-Dest': 'empty',
|
||||||
|
'Sec-Fetch-Mode': 'cors',
|
||||||
|
'Sec-Fetch-Site': 'same-origin',
|
||||||
|
# 覆盖登录接口的默认表单头,尽量贴近浏览器抓包。
|
||||||
|
'Content-Type': None,
|
||||||
|
'X-Requested-With': None,
|
||||||
|
}
|
||||||
|
response = self.request(
|
||||||
|
'post',
|
||||||
|
self.CSRF_API,
|
||||||
|
headers=headers,
|
||||||
|
timeout=self.TIMEOUT,
|
||||||
|
max_retries=1,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
body = response.text.strip()
|
||||||
|
if not body:
|
||||||
|
raise ValueError("生成CSRF失败: 响应为空")
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = response.json()
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
preview = body[:200].replace("\n", "\\n")
|
||||||
|
raise ValueError(f"生成CSRF失败: 响应不是有效 JSON: {preview}") from exc
|
||||||
|
|
||||||
|
if payload.get('error') != 0:
|
||||||
|
raise ValueError(f"生成CSRF失败: {payload.get('msg', '未知错误')}")
|
||||||
|
|
||||||
|
cookies = self.session.cookies.get_dict()
|
||||||
|
csrf_token = cookies.get('cvl_csrf_token', '')
|
||||||
|
if not csrf_token:
|
||||||
|
raise ValueError("生成CSRF失败: 响应没有 cvl_csrf_token")
|
||||||
|
|
||||||
|
logger.info("CSRF Cookie已补齐")
|
||||||
|
return csrf_token
|
||||||
|
|
||||||
|
def generate_acf_ccn_cookie(self) -> str:
|
||||||
|
"""访问 getCsrfCookie 接口,从 Set-Cookie 中同步 acf_ccn。"""
|
||||||
|
logger.info("补齐 acf_ccn Cookie...")
|
||||||
|
headers = {
|
||||||
|
'Accept': 'application/json, text/plain, */*',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
'Priority': 'u=1, i',
|
||||||
|
'Referer': self.ACF_CCN_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': 'empty',
|
||||||
|
'Sec-Fetch-Mode': 'cors',
|
||||||
|
'Sec-Fetch-Site': 'same-origin',
|
||||||
|
# 该接口抓包没有 Origin、表单 Content-Type 和 X-Requested-With。
|
||||||
|
'Origin': None,
|
||||||
|
'Content-Type': None,
|
||||||
|
'X-Requested-With': None,
|
||||||
|
}
|
||||||
|
response = self.request(
|
||||||
|
'get',
|
||||||
|
self.ACF_CCN_API,
|
||||||
|
headers=headers,
|
||||||
|
timeout=self.TIMEOUT,
|
||||||
|
max_retries=1,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
cookies = self.session.cookies.get_dict()
|
||||||
|
acf_ccn = cookies.get('acf_ccn', '') or response.cookies.get('acf_ccn', '')
|
||||||
|
if acf_ccn and not cookies.get('acf_ccn'):
|
||||||
|
# 极少数情况下响应 Cookie 未合入 get_dict,手动补到斗鱼域名下。
|
||||||
|
self.session.cookies.set('acf_ccn', acf_ccn, domain='.douyu.com', path='/')
|
||||||
|
|
||||||
|
if not acf_ccn:
|
||||||
|
raise ValueError("补齐 acf_ccn 失败: 响应没有 acf_ccn")
|
||||||
|
|
||||||
|
logger.info("acf_ccn Cookie已补齐")
|
||||||
|
return acf_ccn
|
||||||
+7
-129
@@ -9,6 +9,7 @@ from typing import Mapping, Optional, Protocol, Tuple
|
|||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from .cookie_enricher import CookieEnricher
|
||||||
from .crypto import encrypt_password, encrypt_nickname_or_phone
|
from .crypto import encrypt_password, encrypt_nickname_or_phone
|
||||||
from .email_verifier import EmailVerifier
|
from .email_verifier import EmailVerifier
|
||||||
from .proxy import ProxyManager, get_proxy_manager
|
from .proxy import ProxyManager, get_proxy_manager
|
||||||
@@ -54,12 +55,6 @@ 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"
|
||||||
CSRF_API = "https://www.douyu.com/japi/carnival/nc/common/generateCsrf"
|
|
||||||
CSRF_REFERER = "https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId=9263298"
|
|
||||||
ACF_CCN_API = "https://www.douyu.com/curl/csrfNlApi/getCsrfCookie"
|
|
||||||
ACF_CCN_REFERER = "https://www.douyu.com/pages/ord-task-center?clientType=web&panelSource=1&rid=0"
|
|
||||||
COOKIE_ENRICH_TIMEOUT = (5, 10)
|
|
||||||
COOKIE_ENRICH_RETRIES = 3
|
|
||||||
LOGIN_REFERER = (
|
LOGIN_REFERER = (
|
||||||
"https://passport.douyu.com/index/login?"
|
"https://passport.douyu.com/index/login?"
|
||||||
"passport_reg_callback=PASSPORT_REG_SUCCESS_CALLBACK&"
|
"passport_reg_callback=PASSPORT_REG_SUCCESS_CALLBACK&"
|
||||||
@@ -739,7 +734,12 @@ class DouyuLogin:
|
|||||||
# 登录成功后补齐 Web 侧 Cookie。补 CK 失败只影响完整度,不回滚已成功的登录态。
|
# 登录成功后补齐 Web 侧 Cookie。补 CK 失败只影响完整度,不回滚已成功的登录态。
|
||||||
self._cookie_enrich_error = ""
|
self._cookie_enrich_error = ""
|
||||||
try:
|
try:
|
||||||
self._enrich_web_cookies_with_retry()
|
CookieEnricher(
|
||||||
|
session=self.session,
|
||||||
|
request_func=self._request,
|
||||||
|
ensure_not_stopped=self._ensure_not_stopped,
|
||||||
|
sleep_interruptible=self._sleep_interruptible,
|
||||||
|
).enrich_with_retry()
|
||||||
except InterruptedError:
|
except InterruptedError:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -748,115 +748,6 @@ class DouyuLogin:
|
|||||||
|
|
||||||
return self._format_cookie_string()
|
return self._format_cookie_string()
|
||||||
|
|
||||||
def _enrich_web_cookies_with_retry(self, max_attempts: int = COOKIE_ENRICH_RETRIES) -> None:
|
|
||||||
"""独立重试补齐 cvl_csrf_token 和 acf_ccn,不触发整条登录链路重跑。"""
|
|
||||||
last_error: Exception | None = None
|
|
||||||
for attempt in range(1, max_attempts + 1):
|
|
||||||
self._ensure_not_stopped()
|
|
||||||
try:
|
|
||||||
logger.info(f"补CK尝试 {attempt}/{max_attempts}...")
|
|
||||||
self._generate_csrf_cookie()
|
|
||||||
self._generate_acf_ccn_cookie()
|
|
||||||
logger.info("补CK完成")
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
logger.warning(f"补CK失败 {attempt}/{max_attempts}: {e}")
|
|
||||||
if attempt < max_attempts:
|
|
||||||
self._sleep_interruptible(1)
|
|
||||||
raise ValueError(f"已重试 {max_attempts} 次仍未补齐CK: {last_error}") from last_error
|
|
||||||
|
|
||||||
def _generate_csrf_cookie(self) -> str:
|
|
||||||
"""
|
|
||||||
访问斗鱼 generateCsrf 接口,从 Set-Cookie 中同步 cvl_csrf_token。
|
|
||||||
|
|
||||||
requests.Session 会自动接收响应中的 Set-Cookie,最终导出时会保留到 CK。
|
|
||||||
"""
|
|
||||||
logger.info("补齐CSRF Cookie...")
|
|
||||||
headers = {
|
|
||||||
'Accept': 'application/json, text/plain, */*',
|
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.6,en;q=0.5',
|
|
||||||
'Origin': 'https://www.douyu.com',
|
|
||||||
'Referer': self.CSRF_REFERER,
|
|
||||||
'Sec-Fetch-Dest': 'empty',
|
|
||||||
'Sec-Fetch-Mode': 'cors',
|
|
||||||
'Sec-Fetch-Site': 'same-origin',
|
|
||||||
# 覆盖登录接口的默认表单头,尽量贴近浏览器抓包。
|
|
||||||
'Content-Type': None,
|
|
||||||
'X-Requested-With': None,
|
|
||||||
}
|
|
||||||
response = self._request(
|
|
||||||
'post',
|
|
||||||
self.CSRF_API,
|
|
||||||
headers=headers,
|
|
||||||
timeout=self.COOKIE_ENRICH_TIMEOUT,
|
|
||||||
max_retries=1,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
body = response.text.strip()
|
|
||||||
if not body:
|
|
||||||
raise ValueError("生成CSRF失败: 响应为空")
|
|
||||||
|
|
||||||
try:
|
|
||||||
payload = response.json()
|
|
||||||
except json.JSONDecodeError as exc:
|
|
||||||
preview = body[:200].replace("\n", "\\n")
|
|
||||||
raise ValueError(f"生成CSRF失败: 响应不是有效 JSON: {preview}") from exc
|
|
||||||
|
|
||||||
if payload.get('error') != 0:
|
|
||||||
raise ValueError(f"生成CSRF失败: {payload.get('msg', '未知错误')}")
|
|
||||||
|
|
||||||
cookies = self.session.cookies.get_dict()
|
|
||||||
csrf_token = cookies.get('cvl_csrf_token', '')
|
|
||||||
if not csrf_token:
|
|
||||||
raise ValueError("生成CSRF失败: 响应没有 cvl_csrf_token")
|
|
||||||
|
|
||||||
logger.info("CSRF Cookie已补齐")
|
|
||||||
return csrf_token
|
|
||||||
|
|
||||||
def _generate_acf_ccn_cookie(self) -> str:
|
|
||||||
"""访问 getCsrfCookie 接口,从 Set-Cookie 中同步 acf_ccn。"""
|
|
||||||
logger.info("补齐 acf_ccn Cookie...")
|
|
||||||
headers = {
|
|
||||||
'Accept': 'application/json, text/plain, */*',
|
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Pragma': 'no-cache',
|
|
||||||
'Priority': 'u=1, i',
|
|
||||||
'Referer': self.ACF_CCN_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': 'empty',
|
|
||||||
'Sec-Fetch-Mode': 'cors',
|
|
||||||
'Sec-Fetch-Site': 'same-origin',
|
|
||||||
# 该接口抓包没有 Origin、表单 Content-Type 和 X-Requested-With。
|
|
||||||
'Origin': None,
|
|
||||||
'Content-Type': None,
|
|
||||||
'X-Requested-With': None,
|
|
||||||
}
|
|
||||||
response = self._request(
|
|
||||||
'get',
|
|
||||||
self.ACF_CCN_API,
|
|
||||||
headers=headers,
|
|
||||||
timeout=self.COOKIE_ENRICH_TIMEOUT,
|
|
||||||
max_retries=1,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
cookies = self.session.cookies.get_dict()
|
|
||||||
acf_ccn = cookies.get('acf_ccn', '') or response.cookies.get('acf_ccn', '')
|
|
||||||
if acf_ccn and not cookies.get('acf_ccn'):
|
|
||||||
# 极少数情况下响应 Cookie 未合入 get_dict,手动补到斗鱼域名下。
|
|
||||||
self.session.cookies.set('acf_ccn', acf_ccn, domain='.douyu.com', path='/')
|
|
||||||
|
|
||||||
if not acf_ccn:
|
|
||||||
raise ValueError("补齐 acf_ccn 失败: 响应没有 acf_ccn")
|
|
||||||
|
|
||||||
logger.info("acf_ccn Cookie已补齐")
|
|
||||||
return acf_ccn
|
|
||||||
|
|
||||||
def _format_cookie_string(self) -> str:
|
def _format_cookie_string(self) -> str:
|
||||||
"""把当前 session 中的 Cookie 格式化为可导出的 CK 字符串。"""
|
"""把当前 session 中的 Cookie 格式化为可导出的 CK 字符串。"""
|
||||||
cookies = self.session.cookies.get_dict()
|
cookies = self.session.cookies.get_dict()
|
||||||
@@ -865,16 +756,3 @@ class DouyuLogin:
|
|||||||
cookie_str = '; '.join([f"{k}={v}" for k, v in cookies.items()])
|
cookie_str = '; '.join([f"{k}={v}" for k, v in cookies.items()])
|
||||||
|
|
||||||
return cookie_str
|
return cookie_str
|
||||||
|
|
||||||
def save_cookie(self, cookie: str, filepath: str) -> None:
|
|
||||||
"""保存Cookie到文件"""
|
|
||||||
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump({
|
|
||||||
'username': self.account.username,
|
|
||||||
'cookie': cookie,
|
|
||||||
'timestamp': int(time.time()),
|
|
||||||
}, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
logger.info(f"Cookie已保存到: {filepath}")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user