Files
2026-07-13 12:29:17 +08:00

136 lines
5.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""B站视频数据 + 评论抓取(公开接口,无需登录、无需签名)。
与 douyin-session / xhs-explore 不同:B站的视频统计与评论都是**公开数据**——
`view` 接口不需要 wbi 签名,评论走 `x/v2/reply` 老接口(按热度)。因此本 adapter
是纯 httpx,零登录、零浏览器,clone 下来即可用。
接口:
- 视频数据:https://api.bilibili.com/x/web-interface/view?bvid=<BV号>
- 评论: https://api.bilibili.com/x/v2/reply?type=1&oid=<aid>&sort=2 sort=2 热度)
"""
from __future__ import annotations
import re
import sys
import time
import httpx
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
)
HEADERS = {"User-Agent": UA, "Referer": "https://www.bilibili.com"}
VIEW_API = "https://api.bilibili.com/x/web-interface/view"
REPLY_API = "https://api.bilibili.com/x/v2/reply"
def normalize_bvid(raw: str) -> str:
"""从 BV 号或 B站视频 URL(含 b23.tv 短链跳转后)里提取 BV 号。"""
m = re.search(r"(BV[0-9A-Za-z]{10})", raw or "")
return m.group(1) if m else (raw or "").strip()
def _client() -> httpx.Client:
return httpx.Client(headers=HEADERS, timeout=20, follow_redirects=True)
def fetch_video(client: httpx.Client, bvid: str) -> dict:
"""拉视频信息 + 统计。返回归一化后的 dict。"""
resp = client.get(VIEW_API, params={"bvid": bvid})
resp.raise_for_status()
j = resp.json()
if j.get("code") != 0:
raise RuntimeError(f"view 接口失败 code={j.get('code')} msg={j.get('message')}")
d = j.get("data") or {}
stat = d.get("stat") or {}
return {
"bvid": d.get("bvid") or bvid,
"aid": d.get("aid"),
"title": d.get("title") or "",
"desc": d.get("desc") or "",
"owner": (d.get("owner") or {}).get("name") or "",
"pubdate": d.get("pubdate") or 0,
"duration_s": d.get("duration") or 0,
"play_count": stat.get("view") or 0,
"like_count": stat.get("like") or 0,
"coin_count": stat.get("coin") or 0,
"favorite_count": stat.get("favorite") or 0,
"share_count": stat.get("share") or 0,
"comment_count": stat.get("reply") or 0,
"danmaku_count": stat.get("danmaku") or 0,
"raw": d,
}
def fetch_comments(client: httpx.Client, aid: int, max_count: int = 50) -> list[dict]:
"""按热度(sort=2)翻页抓评论,最多 max_count 条。"""
out: list[dict] = []
seen: set[str] = set()
pn = 1
for _ in range(60): # 翻页上限保护
if len(out) >= max_count:
break
try:
j = client.get(
REPLY_API,
params={"type": 1, "oid": aid, "sort": 2, "pn": pn, "ps": 20},
).json()
except Exception as exc:
print(f"[警告] 评论请求异常(停止):{exc}")
break
if j.get("code") != 0:
print(f"[警告] 评论接口 code={j.get('code')} msg={j.get('message')}(停止)")
break
reps = (j.get("data") or {}).get("replies") or []
if not reps:
break
for c in reps:
nc = _normalize_comment(c)
if nc["cid"] in seen:
continue
seen.add(nc["cid"])
out.append(nc)
pn += 1
time.sleep(0.4)
out.sort(key=lambda x: x["digg_count"], reverse=True)
return out[:max_count]
def _normalize_comment(c: dict) -> dict:
member = c.get("member") or {}
content = c.get("content") or {}
loc = (c.get("reply_control") or {}).get("location") or ""
return {
"cid": str(c.get("rpid") or ""),
"text": content.get("message") or "",
"digg_count": c.get("like") or 0,
"reply_comment_total": c.get("rcount") or 0,
"create_time": c.get("ctime") or 0,
"user_name": member.get("uname") or "",
"ip_label": loc.replace("IP属地:", "").replace("IP属地:", "").strip() if loc else "",
}
def fetch_all(bvid: str, max_comments: int = 50) -> dict:
"""一次拉完视频数据 + 热门评论。"""
bvid = normalize_bvid(bvid)
with _client() as client:
print(f" → 拉取视频数据 {bvid}")
video = fetch_video(client, bvid)
print(f" ✓ {video['title'][:40]}(播放 {video['play_count']}")
print(" → 拉取热门评论")
if video.get("aid") is None:
print("[警告] 视频数据缺少 aid 字段,无法拉取评论")
comments = []
else:
comments = fetch_comments(client, video["aid"], max_count=max_comments)
print(f" ✓ {len(comments)} 条评论")
return {"video": video, "comments": comments}
if __name__ == "__main__":
# 与 douyin-session / xhs-explore 的接口保持一致;B站公开数据无需登录。
if len(sys.argv) > 1 and sys.argv[1] == "login":
print("[B站] 视频数据与评论均为公开接口,无需登录,直接 /cheat-retro 即可。")