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:
程序员阿江(Relakkes)
2026-09-18 15:48:06 +08:00
parent 9cfc148577
commit c03ab60aac
3 changed files with 82 additions and 4 deletions
+10
View File
@@ -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":
+26 -4
View File
@@ -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:
+46
View File
@@ -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