用PHP提取B站视频封面

Lentinel 发布于 3 天前 66 次阅读


一时心血来潮,想写个小工具来提取B站视频的封面。很多发游戏实况的UP,他视频封面搞一些插画美图啥的,又不放出来,我只好自己提取封面然后自己搜图了。

先尝试用爬虫获取B站的封面。但是B站是有反爬的,分析网页搞半天没啥用。但是我寻思B站这么大的网站肯定一堆人试过爬它,就直接开搜准备照抄,果然找到了一些贴子讲相关内容,不过比较意外的是几乎找不到直接的成品开源代码。总之,可以通过B站的 api 和视频 BV 号来直接爬取视频相关信息:

# 根据BV号获取cid
https://api.bilibili.com/x/player/pagelist?bvid=(bvid)
# 根据BV号和cid获取视频播放列表
https://api.bilibili.com/x/player/playurl?cid=(cid)&qn=(qn)&bvid=(bvid)
# 根据BV号和cid获取aid
https://api.bilibili.com/x/web-interface/view?cid=(cid)&bvid=(bvid)
# 根据av号获取封面
https://api.bilibili.com/x/web-interface/view?aid=(aid)
# 根据BV号获取封面
https://api.bilibili.com/x/web-interface/view?bvid=(bvid)

web-interface/view这个接口信息最全,且支持 aid 和 bvid 双向查询,最终选用了它。

爬取过程中的数据基本上都是 JSON 数据,从返回的 JSON 中只提取我们需要的 title(标题)和 pic(封面图)。

BV1dNkjBGEgF为例,抓取到的 JSON 数据都有:

{
    "code": 0,
    "message": "OK",
    "ttl": 1,
    "data": {
        "bvid": "BV1dNkjBGEgF",
        "aid": 115925613354775,
        "videos": 1,
        "tid": 17,
        "tid_v2": 2069,
        "tname": "",
        "tname_v2": "",
        "copyright": 1,
        "pic": "http://i1.hdslb.com/bfs/archive/4e489460fe371c2e7e9066de2b2f492917aa219a.jpg",
        "title": "红警敌人最失败的外交!遇上我最成功的外交!",
        "pubdate": 1768966200,
        "ctime": 1768884593,
        "desc": "红警敌人最失败的外交!遇上我最成功的外交!",
        "desc_v2": [
            {
                "raw_text": "红警敌人最失败的外交!遇上我最成功的外交!",
                "type": 1,
                "biz_id": 0
            }
        ],
        "state": 0,
        "duration": 800,
        "rights": {
            "bp": 0,
            "elec": 0,
            "download": 1,
            "movie": 0,
            "pay": 0,
            "hd5": 0,
            "no_reprint": 1,
            "autoplay": 1,
            "ugc_pay": 0,
            "is_cooperation": 0,
            "ugc_pay_preview": 0,
            "no_background": 0,
            "clean_mode": 0,
            "is_stein_gate": 0,
            "is_360": 0,
            "no_share": 0,
            "arc_pay": 0,
            "free_watch": 0
        },
        "owner": {
            "mid": 483246073,
            "name": "红警魔鬼蓝天",
            "face": "https://i1.hdslb.com/bfs/face/09978726cc291d0a4aeff8f2fd6022687012150c.jpg"
        },
        "stat": {
            "aid": 115925613354775,
            "view": 75554,
            "danmaku": 582,
            "reply": 341,
            "favorite": 308,
            "coin": 973,
            "share": 45,
            "now_rank": 0,
            "his_rank": 0,
            "like": 5108,
            "dislike": 0,
            "evaluation": "",
            "vt": 0
        },
        "argue_info": {
            "argue_msg": "",
            "argue_type": 0,
            "argue_link": ""
        },
        "dynamic": "",
        "cid": 35497774466,
        "dimension": {
            "width": 1920,
            "height": 1080,
            "rotate": 0
        },
        "premiere": null,
        "teenage_mode": 1,
        "is_chargeable_season": false,
        "is_story": false,
        "is_upower_exclusive": false,
        "is_upower_play": false,
        "is_upower_preview": false,
        "enable_vt": 0,
        "vt_display": "",
        "is_upower_exclusive_with_qa": false,
        "no_cache": false,
        "pages": [
            {
                "cid": 35497774466,
                "page": 1,
                "from": "vupload",
                "part": "红警敌人最失败的外交!遇上我最成功的外交!",
                "duration": 800,
                "vid": "",
                "weblink": "",
                "dimension": {
                    "width": 1920,
                    "height": 1080,
                    "rotate": 0
                },
                "first_frame": "http://i1.hdslb.com/bfs/storyff/_000037hhw5j982wfv22s8usrng6ifhb_firsti.jpg",
                "ctime": 1768884593
            }
        ],
        "subtitle": {
            "allow_submit": false,
            "list": []
        },
        "is_season_display": false,
        "user_garb": {
            "url_image_ani_cut": ""
        },
        "honor_reply": {},
        "like_icon": "",
        "need_jump_bv": false,
        "disable_show_up_info": false,
        "is_story_play": 1,
        "is_view_self": false
    }
}

各字段都代表什么还是比较清楚的,不赘述了。

至于视频链接,我们只需要通过正则匹配,
抓取 BV 号:/BV([a-zA-Z0-9]+)/
抓取 AV 号:/av([0-9]+)/i(忽略大小写)。

前端 <img> 标签要加上 referrerpolicy="no-referrer",因为B站服务器会检查 HTTP 请求头中的 Referer,发现是外站请求就拦截,导致提取到的图片无法正常显示。

而手机分享出来的 b23.tv 链接本身不包含视频 ID,它是一个 302 重定向链接,如果不处理,正则会匹配失败。解决方法是,使用 wp_remote_head(只请求头信息,不下载网页,速度快),获取 HTTP 响应头里的 Location 字段,拿到真实的跳转长链接后,再丢给上面的正则去处理。

最后举个栗子:

    //如果是短链,先获取跳转后的真实地址
if (strpos($url, 'b23.tv')) {
    $url = get_real_url($url); 
}

    //正则提取视频ID
if (preg_match('/BV.../', $url)) {
    //调用B站api
    $api = "https://api.bilibili.com/x/web-interface/view?bvid=" . $bvid;
    $data = request($api);

    //返回 JSON 给前端
    echo json_encode([
        'title' => $data['title'],
        'pic'   => $data['pic']
    ]);
}

本来想用 Python 做个微服务,后来一想 WordPress 本身就是 PHP 框架,功能足够,不必舍近求远,反正我们的代码不涉及网站数据库,很安全了。成品就部署在我的博客里