B站封面与元数据抓取完整方案

场景:抓取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.__pinia JSON 和视频卡片 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

十一、扩展玩法

  1. 任意 BV 号元数据view?bvid=xxx 直接可用,不限首页

  2. 高清封面 :API 返回 data.pic 无裁剪原图

  3. 分区热门 :换 URL https://www.bilibili.com/v/popular

  4. 配合视频流 :拿到 cid 后调 playurl(需 WBI 签名)

  5. 定时任务:cron 每天跑一次

一句话总结

首页 DOM 的 SSR 数据 = 免费的内容清单(封面URL+BV号+标题),view API = 免费的数据补全(播放/弹幕/点赞/UP主)。两者叠加 = 一套零登录、零成本、无依赖的采集管线。

相关推荐
安全小王子5 天前
nssctf_chicken_soup
网络安全·逆向·ctf·nssctf
安全小王子5 天前
nssctf——老鼠走迷宫
网络安全·逆向·ctf·nssctf
深念Y5 天前
QQ 数据库解密与聊天记录导出 — 技术文档
数据库·sqlite·手机·数据恢复·逆向·二进制·qq
安全小王子6 天前
nssctf_easyapp
网络安全·逆向·ctf·nssctf
安全小王子7 天前
nssctf_sign-ezc++
网络安全·逆向·ctf·nssctf
网安蟹佬霸7 天前
Android安全攻防实战:从APK逆向到Frida动态Hook全流程详解(附脚本)
android·前端·安全·web安全·逆向·csrf·网安
刘晴83010 天前
游戏逆向实战:我会梯云纵
逆向
k3x1n13 天前
三星安卓系统BUG排查记录:云闪付、铁路12306、个人所得税、中国移动等App,长期闪退的根本原因分析
android·逆向
_Achelous14 天前
BlueArchive蔚蓝档案国际服剧情文本取得
逆向