场景:抓取B站首页/分区视频的封面图 + 完整元数据(标题/UP主/播放/弹幕/点赞/时长)
结论:无需登录,无头浏览器 dump DOM 拿封面,BV号 + view API 拿元数据,零成本。
一、方案总览
text
B站首页 https://www.bilibili.com
│
① 无头浏览器 --dump-dom(免登录)
│
▼
DOM 内的 SSR 数据(window.__pinia)
包含:封面URL / BV号 / 标题 / UP主名 / 分区
│
② 提取封面URL → 按hash去重 → 下载(WebP)
③ 提取BV号 → 调用 view API → 完整元数据
两个数据源的分工:
| 数据 | 来源 | 需登录 |
|---|---|---|
| 封面图 | DOM 内 src="//iX.hdslb.com/bfs/archive/*.jpg@..." |
否 |
| 播放/弹幕/点赞/UP主 | api.bilibili.com/x/web-interface/view?bvid=xxx |
否 |
| 分区/时长/简介 | 同上 view API | 否 |
首页 DOM 里标题、封面URL、BV号全都有(SSR 注入),但播放量/弹幕数不直接出现,用 API 补全最干净。
二、工具准备
本机全部已有,无需安装:
bash
/usr/bin/google-chrome # 无头浏览器
curl # 下载
python3 # 解析
无需 cwebp / ImageMagick:B站 CDN 本身提供 WebP 版,直接下载 .webp 后缀的 URL 即可。
三、抓取首页 DOM
bash
/usr/bin/google-chrome --headless=new --disable-gpu --no-sandbox \
--dump-dom "https://www.bilibili.com" > /tmp/bili_home.html
-
--headless=new:新版无头模式 -
--dump-dom:渲染后输出完整 HTML(JS 已执行) -
输出约 300-400KB,主要包含
window.__piniaJSON 和视频卡片 HTML
四、提取视频列表
python
import re
html = open('/tmp/bili_home.html', encoding='utf-8').read()
# 从 <img src> 提取封面URL(含尺寸后缀)
covers = re.findall(r'//(i\d+\.hdslb\.com/bfs/archive/[^"\s<>\']+)', html)
covers = [f'https://{u}' for u in covers]
# 从 SSR 数据提取 BV号
bvids = re.findall(r'(BV[0-9a-zA-Z]{10})', html)
bvids = list(dict.fromkeys(bvids)) # 去重
SSR 数据形态(__pinia 压缩后参数列表扁平交错,靠正则即可)。
五、封面 URL 格式与去重
同一封面三种格式
text
https://i0.hdslb.com/bfs/archive/{hash}.jpg@672w_378h_1c_!web-home-common-cover ← JPG
https://i0.hdslb.com/bfs/archive/{hash}.jpg@672w_378h_1c_!web-home-common-cover.webp ← WebP
https://i0.hdslb.com/bfs/archive/{hash}.jpg@672w_378h_1c_!web-home-common-cover.avif ← AVIF
大小实测(672×378)
| hash 前8位 | JPG | WebP | AVIF | AVIF/JPG |
|---|---|---|---|---|
| 139e17b6 | 63.9KB | 42.8KB | 30.9KB | 48% |
| e51b677d | 35.6KB | 20.5KB | 12.9KB | 36% |
结论: AVIF 最小,WebP 居中,JPG 最大。实际选 WebP(兼容性好、无专利风险)。
去重逻辑(关键)
首页 DOM 里轮播图/推荐流/横幅多个区域,同一封面会出现多次且格式不同,按 hash 去重:
python
def dedup(covers):
seen = {}
for u in covers:
h = u.split('/')[-1].split('@')[0]
fmt = 'webp' if u.endswith('.webp') else ('avif' if u.endswith('.avif') else 'jpg')
if h not in seen or fmt == 'webp':
seen[h] = u
return list(seen.values())
实测不去重会得到 37 个文件,去重后只剩 12-14 张唯一封面。
六、下载封面(统一 WebP)
直接请求 .webp 后缀 URL,服务端已转好:
bash
curl -s -L -o video_01.webp \
"https://i0.hdslb.com/bfs/archive/{hash}.jpg@672w_378h_1c_!web-home-common-cover.webp"
七、view API 获取元数据
对每个 BV 号调用:
text
GET https://api.bilibili.com/x/web-interface/view?bvid={BVxxx}
bash
curl -s "https://api.bilibili.com/x/web-interface/view?bvid=BV1Vy8r6JE9z" \
-H "User-Agent: Mozilla/5.0" \
-H "Referer: https://www.bilibili.com/"
返回字段:
| 字段 | 路径 | 说明 |
|---|---|---|
| 标题 | data.title |
|
| bvid/aid/cid | data.bvid/aid/cid |
cid 可用于后续视频流 |
| UP主 | data.owner.name / data.owner.mid |
|
| 分区 | data.tname |
|
| 播放 | data.stat.view |
|
| 弹幕 | data.stat.danmaku |
|
| 评论/点赞/投币/收藏/分享 | data.stat.reply/like/coin/favorite/share |
|
| 时长 | data.duration |
秒 |
| 发布时间 | data.pubdate |
Unix 时间戳 |
| 封面原图 | data.pic |
高清大图 |
注意:逐条请求加 time.sleep(0.3),必须带 Referer。
八、完整脚本
python
#!/usr/bin/env python3
"""
B站首页封面 + 元数据抓取脚本
输出:./out/*.webp 封面 + ./out/metadata.json
"""
import re, os, json, time, subprocess, urllib.request
OUT = os.path.join(os.path.expanduser("~"), "bili_out")
os.makedirs(OUT, exist_ok=True)
UA = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0",
"Referer": "https://www.bilibili.com/"}
# 1. 无头浏览器抓 DOM
subprocess.run([
"/usr/bin/google-chrome", "--headless=new", "--disable-gpu", "--no-sandbox",
"--dump-dom", "https://www.bilibili.com",
], stdout=open("/tmp/bili_home.html", "w"), stderr=subprocess.DEVNULL)
html = open("/tmp/bili_home.html", encoding="utf-8").read()
# 2. 提取 + 去重封面
covers = re.findall(r'//(i\d+\.hdslb\.com/bfs/archive/[^"\s<>\']+)', html)
covers = [f"https://{u}" for u in covers]
def dedup(covers):
seen = {}
for u in covers:
h = u.split("/")[-1].split("@")[0]
fmt = "webp" if u.endswith(".webp") else ("avif" if u.endswith(".avif") else "jpg")
if h not in seen or fmt == "webp":
seen[h] = u
return list(seen.values())
unique = dedup(covers)
# 3. 下载封面
for i, url in enumerate(unique):
h = url.split("/")[-1].split("@")[0]
webp_url = url + ".webp" # 简化
fname = os.path.join(OUT, f"video_{i+1:02d}.webp")
try:
req = urllib.request.Request(webp_url, headers=UA)
with urllib.request.urlopen(req, timeout=15) as r:
data = r.read()
if len(data) < 500: # fallback 原图
req = urllib.request.Request(url, headers=UA)
with urllib.request.urlopen(req, timeout=15) as r:
data = r.read()
with open(fname, "wb") as f:
f.write(data)
except Exception as e:
print(f"FAIL {h[:12]}: {e}")
# 4. 提取 BV号 + 调 API
bvids = list(dict.fromkeys(re.findall(r"(BV[0-9a-zA-Z]{10})", html)))
meta = []
for bvid in bvids[:20]:
try:
req = urllib.request.Request(
f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}", headers=UA)
with urllib.request.urlopen(req, timeout=10) as r:
d = json.loads(r.read())
v = d["data"]
meta.append({
"bvid": bvid,
"title": v.get("title"),
"owner": v.get("owner", {}).get("name"),
"view": v.get("stat", {}).get("view"),
"danmaku": v.get("stat", {}).get("danmaku"),
"like": v.get("stat", {}).get("like"),
"duration": v.get("duration"),
"pubdate": v.get("pubdate"),
})
time.sleep(0.3)
except Exception as e:
print(f"{bvid} fail: {e}")
with open(os.path.join(OUT, "metadata.json"), "w", encoding="utf-8") as f:
json.dump(meta, f, ensure_ascii=False, indent=2)
九、踩坑记录
| 坑 | 现象 | 解法 |
|---|---|---|
| 封面重复 | 下出 37 个文件 | 按 hash 去重 |
| 空文件 | 38B 的 webp | 该封面无 webp 变体,fallback 原图 |
| 变体后缀位置 | .webp 在 @尺寸后缀 后面 |
示例:xx.jpg@672w...cover.webp |
| API 403 | 没带 Referer | 加 Referer: https://www.bilibili.com/ |
| BV 重复 | 每个视频出现 2 次 | dict.fromkeys() 去重 |
| /tmp 被清理 | 文件消失 | 输出到 ~/ 下 |
十、实测结果
text
✓ 提取 37 条URL,去重后 12 张封面
✓ 提取 12 个BV号
# | 标题 | UP主 | 播放 | 弹幕 | 时长
─────────────────────────────────────────────────────────────
1 | 全国首测!头文字D主角车,谁最快? | 极速拍档 | 1,133,775 | 8,718 | 30:54
2 | 《星布谷地》连接PV | 星布谷地 | 1,078,926 | 1,479 | 04:58
十一、扩展玩法
-
任意 BV 号元数据 :
view?bvid=xxx直接可用,不限首页 -
高清封面 :API 返回
data.pic无裁剪原图 -
分区热门 :换 URL
https://www.bilibili.com/v/popular -
配合视频流 :拿到
cid后调 playurl(需 WBI 签名) -
定时任务:cron 每天跑一次
一句话总结
首页 DOM 的 SSR 数据 = 免费的内容清单(封面URL+BV号+标题),view API = 免费的数据补全(播放/弹幕/点赞/UP主)。两者叠加 = 一套零登录、零成本、无依赖的采集管线。