mirror of
https://github.com/NanmiCoder/MediaCrawler.git
synced 2026-09-21 10:08:12 +08:00
fix(bili): 登录态判定改为校验 cookie 有效性,避免静默降级到 480P
check_login_state 原先只判断 SESSDATA 是否存在。过期会话的 SESSDATA 会一直 留在浏览器里,于是 pong 报"账号未登录"后进入的登录流程会被立刻判成成功: 既不等待扫码,又把这份死 cookie 灌给 API client。 后果是隐蔽的:详情和评论两个接口不要求登录,照常爬到;而 playurl 依据 Cookie 决定清晰度,未登录状态会静默封顶在 480P,不报任何错。 - login.py: 抽出 is_login_cookie_refreshed(),要求 SESSDATA 被扫码换发成新值 才算登录成功;进入登录流程前记录旧值,残留死 cookie 时输出告警 - core.py: 登录流程结束后补一次 pong 校验,仍失败则明确报错退出, 不再带着未登录状态跑完全程 - 新增 tests/test_bilibili_login_state.py 覆盖上述分支
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
# import random # Removed as we now use fixed config.CRAWLER_MAX_SLEEP_SEC intervals
|
||||
from asyncio import Task
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
@@ -113,6 +114,15 @@ class BilibiliCrawler(AbstractCrawler):
|
||||
browser_context=self.browser_context,
|
||||
urls=self.cookie_urls,
|
||||
)
|
||||
# 登录流程结束后再校验一次,宁可在这里失败也不要带着未登录状态跑完全程:
|
||||
# B 站的清晰度由 playurl 依据 Cookie 决定,死会话不会报错,
|
||||
# 只会把带 get_media 的视频静默限制在 480P
|
||||
if not await self.bili_client.pong():
|
||||
utils.logger.error(
|
||||
"[BilibiliCrawler.start] 登录流程结束后仍未登录:请确认扫码已完成、"
|
||||
"或 config.COOKIES 是否有效。已终止,否则视频会被静默限制在 480P。"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
crawler_type_var.set(config.CRAWLER_TYPE)
|
||||
if config.CRAWLER_TYPE == "search":
|
||||
|
||||
@@ -48,6 +48,18 @@ LOGIN_ENTRY_SELECTORS = [
|
||||
]
|
||||
|
||||
|
||||
def is_login_cookie_refreshed(cookie_dict: dict, sessdata_before_login: str) -> bool:
|
||||
"""判断浏览器里的 SESSDATA 是否已经是扫码换发后的新值。
|
||||
|
||||
过期的 SESSDATA 同样会留在浏览器里,所以"cookie 存在"不等于"已登录"。
|
||||
只判断存在会把死会话判成登录成功:既跳过扫码,又把这份死 cookie 灌给
|
||||
API client —— B 站 playurl 依据 Cookie 决定清晰度,结果是详情和评论照常拿到
|
||||
(这两个接口不要求登录),视频却被静默限制在 480P。
|
||||
"""
|
||||
sessdata = cookie_dict.get("SESSDATA", "")
|
||||
return bool(sessdata) and sessdata != sessdata_before_login
|
||||
|
||||
|
||||
class BilibiliLogin(AbstractLogin):
|
||||
def __init__(self,
|
||||
login_type: str,
|
||||
@@ -61,6 +73,8 @@ class BilibiliLogin(AbstractLogin):
|
||||
self.context_page = context_page
|
||||
self.login_phone = login_phone
|
||||
self.cookie_str = cookie_str
|
||||
# 进入登录流程前浏览器里已有的 SESSDATA,用于区分"cookie 存在"与"cookie 有效"
|
||||
self._sessdata_before_login: str = ""
|
||||
|
||||
async def begin(self):
|
||||
"""Start login bilibili"""
|
||||
@@ -79,19 +93,27 @@ class BilibiliLogin(AbstractLogin):
|
||||
async def check_login_state(self) -> bool:
|
||||
"""
|
||||
Check if the current login status is successful and return True otherwise return False
|
||||
retry decorator will retry 20 times if the return value is False, and the retry interval is 1 second
|
||||
retry decorator will retry 600 times if the return value is False, and the retry interval is 1 second
|
||||
if max retry times reached, raise RetryError
|
||||
"""
|
||||
current_cookie = await self.browser_context.cookies()
|
||||
_, cookie_dict = utils.convert_cookies(current_cookie)
|
||||
if cookie_dict.get("SESSDATA", "") or cookie_dict.get("DedeUserID"):
|
||||
return True
|
||||
return False
|
||||
return is_login_cookie_refreshed(cookie_dict, self._sessdata_before_login)
|
||||
|
||||
async def login_by_qrcode(self):
|
||||
"""login bilibili website and keep webdriver login state"""
|
||||
utils.logger.info("[BilibiliLogin.login_by_qrcode] Begin login bilibili by qrcode ...")
|
||||
|
||||
# 记下扫码前的 SESSDATA:扫码成功后它会被换发成新值,
|
||||
# check_login_state 靠这个差值判断"真的登录了"而不是"只剩一份过期 cookie"
|
||||
_, cookie_dict = utils.convert_cookies(await self.browser_context.cookies())
|
||||
self._sessdata_before_login = cookie_dict.get("SESSDATA", "")
|
||||
if self._sessdata_before_login:
|
||||
utils.logger.warning(
|
||||
"[BilibiliLogin.login_by_qrcode] 浏览器中残留了 SESSDATA,但接口校验为未登录,"
|
||||
"该会话已失效;本次必须重新扫码换发新 cookie 才会继续 ..."
|
||||
)
|
||||
|
||||
# click login button
|
||||
login_entry_selector = ", ".join(LOGIN_ENTRY_SELECTORS)
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2025 relakkes@gmail.com
|
||||
#
|
||||
# This file is part of MediaCrawler project.
|
||||
# Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/tests/test_bilibili_login_state.py
|
||||
# GitHub: https://github.com/NanmiCoder
|
||||
# Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1
|
||||
#
|
||||
|
||||
"""B 站登录态判定的回归测试。
|
||||
|
||||
回归背景:``check_login_state`` 曾经只判断 SESSDATA **是否存在**。当浏览器里
|
||||
残留一份过期 SESSDATA 时(B 站不会主动清掉它),流程会被判成"登录成功":
|
||||
既跳过扫码,又把这份死 cookie 灌给 API client。后果是详情和评论照常爬到
|
||||
(这两个接口不要求登录),但 playurl 依据 Cookie 定清晰度,视频被静默限制在 480P。
|
||||
|
||||
这里只测判定逻辑本身,不启浏览器、不发网络请求。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from media_platform.bilibili.login import is_login_cookie_refreshed
|
||||
|
||||
STALE = "sessdata-remaining-from-expired-session"
|
||||
FRESH = "sessdata-reissued-by-qrcode-scan"
|
||||
|
||||
|
||||
def test_stale_sessdata_is_not_treated_as_logged_in() -> None:
|
||||
"""残留的过期 SESSDATA 不能算登录成功(本次回归的核心)"""
|
||||
assert is_login_cookie_refreshed({"SESSDATA": STALE}, STALE) is False
|
||||
|
||||
|
||||
def test_absent_sessdata_is_not_logged_in() -> None:
|
||||
"""完全没有 SESSDATA 时未登录;只有 DedeUserID 不足以证明会话有效"""
|
||||
assert is_login_cookie_refreshed({}, "") is False
|
||||
assert is_login_cookie_refreshed({"DedeUserID": "434377496"}, "") is False
|
||||
|
||||
|
||||
def test_refreshed_sessdata_is_logged_in() -> None:
|
||||
"""扫码换发成新值之后才放行"""
|
||||
assert is_login_cookie_refreshed({"SESSDATA": FRESH}, STALE) is True
|
||||
|
||||
|
||||
def test_sessdata_appearing_from_nothing_is_logged_in() -> None:
|
||||
"""本来就没有 SESSDATA(真未登录)时,扫码写进来的任意值都算成功"""
|
||||
assert is_login_cookie_refreshed({"SESSDATA": FRESH}, "") is True
|
||||
Reference in New Issue
Block a user