Files
2026-08-31 10:55:44 +08:00

140 lines
5.3 KiB
Python

"""斗鱼 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: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
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,
)
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,
)
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