# -*- coding: utf-8 -*- """B 站 Cookie 与请求头工具。 从 ``favsort.py`` 抽取,供现有排序工具和看板共用,避免出现两套不一致的 身份 Cookie / 设备指纹处理逻辑。 """ from __future__ import annotations import random import urllib.parse import uuid IDENTITY_KEYS = { "sessdata", "bili_jct", "dedeuserid", "dedeuserid__ckmd5", "bili_ticket", "bili_ticket_expires", "sid", "b_lsid", } DROP_STALE = {"sid", "b_lsid"} # 会话级字段,刷新时应丢弃旧值 def strip_identity(cookie: str) -> str: """剥离账号身份类 cookie,保留 buvid3 / buvid4 / _uuid 等设备指纹。 实测结论: - ids:完全无 cookie 也能读(连私密收藏夹都返回全量); - infos:不需要账号身份,但缺设备指纹 cookie 会被 HTTP 412 拦; - sort:必须带完整 cookie + csrf,不能走这里。 """ kept = [] for part in (cookie or "").split(";"): part = part.strip() if not part: continue key = part.split("=", 1)[0].strip().lower() if key in IDENTITY_KEYS: continue kept.append(part) return "; ".join(kept) def make_headers(cfg: dict, auth: bool = False) -> dict[str, str]: """构造 B 站 Web 请求头;``auth=False`` 时去掉账号身份 cookie。""" return { "accept": "*/*", "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "cookie": cfg["cookie"] if auth else strip_identity(cfg["cookie"]), "dnt": "1", "origin": "https://www.bilibili.com", "referer": "https://www.bilibili.com/", "sec-ch-ua": '"Google Chrome";v="155", "Chromium";v="155", "Not(A:Brand";v="24"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-site", "user-agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36" ), } def parse_cookie(cookie: str) -> list[tuple[str, str]]: """把 cookie 字符串解析成 [(key, value)],保留原顺序。""" pairs = [] for part in (cookie or "").split(";"): part = part.strip() if not part or "=" not in part: continue k, v = part.split("=", 1) pairs.append((k.strip(), v.strip())) return pairs def cookie_value(cookie: str, name: str) -> str: name = name.lower() for k, v in parse_cookie(cookie): if k.lower() == name: return v return "" def cookies_from_set_cookie(headers) -> dict[str, str]: """从 Set-Cookie 头里取 name=value(忽略 Path/Domain/Expires 等属性)。""" pairs = {} for h in headers or []: first = h.split(";", 1)[0].strip() if "=" not in first: continue k, v = first.split("=", 1) pairs[k.strip()] = v.strip() return pairs def merge_cookie(old: str, new_pairs: dict[str, str]) -> str: """只替换身份类 cookie,其它字段(设备指纹等)原样保留。 新值里没有的身份字段不会被删除(例如服务端没下发新的 sid 时保留旧值), 只有 DROP_STALE 里的会话字段会在刷新时清掉。 """ new_l = { k.lower(): (k, v) for k, v in new_pairs.items() if k.lower() in IDENTITY_KEYS } out, seen = [], set() for k, v in parse_cookie(old): kl = k.lower() if kl in new_l: nk, nv = new_l[kl] out.append((nk, nv)) seen.add(kl) elif kl in DROP_STALE: continue else: out.append((k, v)) seen.add(kl) for kl, (k, v) in new_l.items(): if kl not in seen: out.append((k, v)) return "; ".join(f"{k}={v}" for k, v in out) def gen_uuid() -> str: """生成 B 站风格的 _uuid(前端本来就是本地生成的,服务端不下发)。""" suffix = "".join(random.choices("0123456789ABCDEF", k=7)) return f"{str(uuid.uuid4()).upper()}{suffix}infoc" def sessdata_expiry(cookie: str) -> int | None: """SESSDATA 形如 <值>%2C<过期时间戳>%2C<校验>,取中间的时间戳。""" value = urllib.parse.unquote(cookie_value(cookie, "SESSDATA")) parts = value.split(",") if len(parts) >= 2 and parts[1].isdigit(): return int(parts[1]) return None