|
| 1 | +""" |
| 2 | +viralens · enrich_bilibili.py(可选步骤)—— B 站「三连」数据补全:赞 / 硬币 / 收藏 / 分享。 |
| 3 | +
|
| 4 | +UP 主投稿列表接口(vlist)只给 播放/弹幕/评论;三连要逐条视频调 view 接口, |
| 5 | +N=40 条/人 × 节流 ≈ 半分钟/人,所以做成可选步骤,不进默认流水线: |
| 6 | +
|
| 7 | + python enrich_bilibili.py # 增量:只补还没有 coin 字段的视频 |
| 8 | + python enrich_bilibili.py --force # 全量重取(刷新三连 + 播放/评论/弹幕) |
| 9 | +
|
| 10 | +为什么值得跑:投币/收藏是 B 站独有的「真金白银认可」。三连率 =(赞+币+藏)/万播放, |
| 11 | +比评论率更硬的内容质量信号 —— 商业数据平台的核心指标,开源工具里还没人算。 |
| 12 | +跑完后无需其他操作:scan_signals.py / 报告会自动多出「三连率 / 投币率」维度 |
| 13 | +(features.py 检测到 coin/favorite 字段就启用),export_data.py 也会把新字段带进 CSV。 |
| 14 | +""" |
| 15 | +import asyncio |
| 16 | +import json |
| 17 | +import sys |
| 18 | + |
| 19 | +if hasattr(sys.stdout, "reconfigure"): |
| 20 | + sys.stdout.reconfigure(encoding="utf-8") |
| 21 | + |
| 22 | +from bilibili_api import video, Credential |
| 23 | + |
| 24 | +from runtime import DATA |
| 25 | +from creators import CREATORS |
| 26 | + |
| 27 | +try: |
| 28 | + from config_local import SESSDATA |
| 29 | +except ImportError: |
| 30 | + SESSDATA = "" |
| 31 | +try: |
| 32 | + from config_local import BUVID3 |
| 33 | +except ImportError: |
| 34 | + BUVID3 = "" |
| 35 | + |
| 36 | +PACE = 0.6 # 每条视频之间的节流(view 接口风控比 vlist 宽松,但仍要温和) |
| 37 | + |
| 38 | + |
| 39 | +def _cred(): |
| 40 | + """view 接口是公开的,不带登录态也能查;有 SESSDATA 更稳,有就带上。""" |
| 41 | + if SESSDATA and BUVID3: |
| 42 | + return Credential(sessdata=SESSDATA, buvid3=BUVID3) |
| 43 | + if SESSDATA: |
| 44 | + return Credential(sessdata=SESSDATA) |
| 45 | + return None |
| 46 | + |
| 47 | + |
| 48 | +async def enrich_one(v, cred, tries=3): |
| 49 | + """给一条视频补 stat 字段。失败重试(退避),最终失败返回 False、不动原记录。""" |
| 50 | + bvid = v.get("bvid") |
| 51 | + for attempt in range(tries): |
| 52 | + try: |
| 53 | + info = await video.Video(bvid=bvid, credential=cred).get_info() |
| 54 | + st = info.get("stat") or {} |
| 55 | + v["like"] = st.get("like") |
| 56 | + v["coin"] = st.get("coin") |
| 57 | + v["favorite"] = st.get("favorite") |
| 58 | + v["share"] = st.get("share") |
| 59 | + # 顺手刷新基础数据(view 接口的数字比抓取时新;"--" 等占位符不覆盖) |
| 60 | + for src, dst in (("view", "play"), ("reply", "comment"), ("danmaku", "danmaku")): |
| 61 | + if isinstance(st.get(src), int): |
| 62 | + v[dst] = st[src] |
| 63 | + return True |
| 64 | + except Exception as e: |
| 65 | + if attempt < tries - 1: |
| 66 | + await asyncio.sleep(5 * (attempt + 1)) # 5s,10s 退避(风控/网络抖动) |
| 67 | + continue |
| 68 | + print(f" · 失败 {bvid}: {type(e).__name__}: {e}") |
| 69 | + return False |
| 70 | + return False |
| 71 | + |
| 72 | + |
| 73 | +async def main(): |
| 74 | + force = "--force" in sys.argv |
| 75 | + cred = _cred() |
| 76 | + targets = [c for c in CREATORS if (c.get("platform") or "bilibili").lower() == "bilibili"] |
| 77 | + if not targets: |
| 78 | + print("creators.py 里没有 B 站创作者,无事可做") |
| 79 | + return |
| 80 | + |
| 81 | + for c in targets: |
| 82 | + p = DATA / f"{c['alias']}_videos.json" |
| 83 | + if not p.exists(): |
| 84 | + print(f" ✗ 缺 {p.name},先跑 fetch_multi.py") |
| 85 | + continue |
| 86 | + vids = json.loads(p.read_text(encoding="utf-8")) |
| 87 | + todo = [v for v in vids if v.get("bvid") and (force or v.get("coin") is None)] |
| 88 | + if not todo: |
| 89 | + print(f" ⏭ {c['name']:<16} 三连已齐,跳过(要刷新加 --force)") |
| 90 | + continue |
| 91 | + n_ok = 0 |
| 92 | + for i, v in enumerate(todo): |
| 93 | + if i: |
| 94 | + await asyncio.sleep(PACE) |
| 95 | + n_ok += await enrich_one(v, cred) |
| 96 | + if n_ok and n_ok % 10 == 0: # 边取边存,中断不丢 |
| 97 | + p.write_text(json.dumps(vids, ensure_ascii=False, indent=2), encoding="utf-8") |
| 98 | + p.write_text(json.dumps(vids, ensure_ascii=False, indent=2), encoding="utf-8") |
| 99 | + print(f" ✓ {c['name']:<16} 三连补全 {n_ok}/{len(todo)} 条") |
| 100 | + |
| 101 | + print("\n✅ 完成。重跑 scan_signals.py / 报告,会自动出现「三连率 / 投币率」维度") |
| 102 | + |
| 103 | + |
| 104 | +if __name__ == "__main__": |
| 105 | + asyncio.run(main()) |
0 commit comments