bilibili(嗶哩嗶哩) 视频源地址获取原理及源码

本文距发布已经过去多年, 故不再更新正文.

评论区kslr提供的更新(2016年1月24日):

appkey='85eb6835b0a1034e';  
secretkey = '2ad42749773c441109bdc0191257a664'

sign_this = hashlib.md5(bytes('appkey=' + appkey + '&cid=' + cid + secretkey, 'utf-8')).hexdigest()  
url = 'http://interface.bilibili.com/playurl?appkey=' + appkey + '&cid=' + cid + '&sign=' + sign_this  

评论区云播网提供的新解析地址(2016年9月29日):

http://interface.bilibili.com/playurl?cid=4081368&player=1&ts=1475150943&sign=4c92c3d030555d15ba79972153a81861  

以下正文:

最近因为三次元的一些麻烦事在学习用vegas做视频,试着直接从bilibili上抓优秀的视频作为素材.

现在主流的抓视频方法有两种,分别是拖缓存和使用flvcd.前者必须视频已经完全载入,后者需要借助第三方网站,未免显得有些麻烦.

好歹我也算个技术宅,研究了一下之后成功写了一个获取视频链接的小工具(成就感GET!).

获取视频源地址的原理

首先我们要知道bilibili的视频都是从例如优酷,新浪之类的视频网站加载的,再配上弹幕,就是我们所见到的弹幕视频了.

既然视频不放在bilibili上,那肯定不是从bilibili的服务器上去加载的,于是我们打开网站的时候,B站的Flash播放器就会去分析出视频的源地址.

在视频页面的源代码中我们可以找到Flash播放器元素:

<embed height="482" width="950" pluginspage="http://www.adobe.com/shockwave/download/download.cgi?P1_Prod_Version=ShockwaveFlash" AllowScriptAccess="always" rel="noreferrer" flashvars="cid=567824" src="http://static.hdslb.com/play.swf" type="application/x-shockwave-flash" allowfullscreen="true" quality="high"></embed>  

在这里有一个参数flashvars,再查看别的视频页面的源代码,会发现每个视频都有其不同的值.

部分视频页面的源码使用的是iframe标签,其内容可能是这样:

<iframe height="490" width="950" src="https://secure.bilibili.tv/secure,cid=459194" scrolling="no" border="0" frameborder="no" framespacing="0"></iframe>  

可以发现cid=459194这个格式仍然是存在的,只是调用的方法不同而已.

反编译B站的播放器Flash文件play.swf,我们可以找到一个名为getURL()的函数:

public function getURL():String{  
  var _loc_1:*=this._appRouter.type==="bili2"?("cid:"+this._appRouter.cid):(this._appRouter.cid);
  return "http://interface.bilibili.tv/player?id="+_loc_1;
}

可以发现B站有一个interface的二级域名,它是一个供播放器调用的API,并且使用的参数是id.

在浏览器中通过抓包查看相关interface域名的内容,可以发现一条XHR请求:

Request URL:http://interface.bilibili.tv/playurl?cid=567824  
Request Method:GET  
Status Code:200 OK

Query String Parameters  
cid:567824  

请求后响应到的response内容为xml文件:

<video>  
<result>suee</result>  
<timelength>1580162</timelength>  
<framecount>47404860</framecount>  
<src>400</src>  
<durl>  
<order>1</order>  
<length>361518</length>  
<url>  
<![CDATA[

http://edge.v.iask.com/87296359.hlv?KID=sina,viask&Expires=1350316800&ssig=1TMAI1PXbG

]]>
</url>  
</durl>  
<durl>  
<order>2</order>  
<length>360516</length>  
<url>  
<![CDATA[

http://edge.v.iask.com/87297948.hlv?KID=sina,viask&Expires=1350316800&ssig=86y622qDhK

]]>
</url>  
</durl>  
<durl>  
<order>3</order>  
<length>367323</length>  
<url>  
<![CDATA[

http://edge.v.iask.com/87296361.hlv?KID=sina,viask&Expires=1350316800&ssig=m%2FUElXdEKw

]]>
</url>  
</durl>  
<durl>  
<order>4</order>  
<length>363531</length>  
<url>  
<![CDATA[

http://edge.v.iask.com/87296363.hlv?KID=sina,viask&Expires=1350316800&ssig=dhoWgT5Zij

]]>
</url>  
</durl>  
<durl>  
<order>5</order>  
<length>127208</length>  
<url>  
<![CDATA[

http://edge.v.iask.com/87296365.hlv?KID=sina,viask&Expires=1350316800&ssig=4vHTOj7rNt

]]>
</url>  
</durl>  
<ad>  
<![CDATA[ ]]>  
</ad>  
<vstr>  
<![CDATA[ 8fc217f6c4f11d6a5f12c921b2fbc303 ]]>  
</vstr>  
<vip>  
<![CDATA[ BgdRGwkADxxXCQsXVwsE ]]>  
</vip>  
<vround>20</vround>  
</video>  

其中url节点内的就是该视频的源地址,由于该视频来自于新浪,所以视频源地址是分段的(该视频分为5段).

获取视频源地址的Node.js源码

因为最近没干什么正经事,只有Windows下的Node.js可以折腾折腾,所以就用Node.js写了这个工具:

var address = process.argv[2];  
var filename = process.argv[3];

var http = require('http');  
var xml2js = require('xml2js');  
var zlib = require('zlib');  
var fs = require('fs');

function getId(url, cb) {  
    var pattern = /flashvars="([^"]+)"/;

    http.get(url, function(res) {
        var headers = res.headers;

        // 302
        if(headers['location']) {
            http.get(headers['location'], function(res) {
                var gunzip = zlib.createGunzip();

                res.pipe(gunzip);

                var html = '';

                gunzip.on('data', function(data) {
                    html += data;
                });

                gunzip.on('end', function() {
                    var result = pattern.exec(html);
                    var id = '';
                    if(!result) {
                        pattern = /secure,([^"]+)"/;
                        result = pattern.exec(html);
                    }
                    id = result[1];
                    cb(id);
                });

            });
        } else {
            var gunzip = zlib.createGunzip();

            res.pipe(gunzip);

            var html = '';

            gunzip.on('data', function(data) {
                html += data;
            });

            gunzip.on('end', function() {
                var result = pattern.exec(html);
                var id = result[1];
                cb(id);
            });

        }
    });
}

function getURL(id, cb) {  
    var url = 'http://interface.bilibili.tv/playurl?' + id;

    http.get(url, function(res) {
        var xml_raw = '';
        res.setEncoding('utf8');

        res.on('data', function(data) {
            xml_raw += data;
        });

        res.on('end', function() {
            var parser = new xml2js.Parser();
            parser.parseString(xml_raw, function(err, result) {
                if(err) {
                    console.log(err);
                }
                var list_url = [];
                for(var i in result.video.durl) {
                    list_url.push(result.video.durl[i].url[0]);
                }
                cb(list_url);
            });
        });

    });
}

getId(address, function(id) {  
    getURL(id, function(url) {
        var result = '';
        for(i in url) {
            result += url[i] + (process.platform === 'win32' ? '\r\n' : '\n');
        }
        if(filename) {
            fs.writeFileSync(filename, result);
            console.log('Done');
        } else {
            console.log(result);
        }
    });
});

在编写过程中存在几个障碍,如果有朋友想要用别的语言实现,可以参考下面的内容:

这个程序本身可以接受1~2个参数,第一个参数为视频页地址,比如http://www.bilibili.tv/video/av370930/这样的,第二个参数是将下载链接作为文本文件保存到本地所用的文件名(path/filename).

这段代码中getId这个函数会去获取视频页地址的源码并提取flashvars的值.

getURL将会从interface.bilibili.tv获得视频的源地址.

我最开始调试的时候,getId无法获取flashvars的值,之后我在response的header里发现 www.bilibili.tv 这个域名被做了302跳转,转向目标是 bilibili.kankanews.com 域名.

那么就要砍掉 www.bilibili.tv 改为 bilibili.kankanews.com .

但是 bilibili.kankanews.com 的页面是经过gzip压缩的,gunzip解压后才是可读页面.

使用zilib.createGunzip将流解压后,总算是成功获取到了flashvars.

然后就是需要解析xml,我用的是xml2js,可以直接在npm里安装(本来以为实现这个程序会很麻烦,还写了个package.json,现在想想真是囧啊).

遍历节点video->durl->url即可.

实用性

由于Node.js在国内用的人实在不算太多,打算有时间再写一个GUI打包成Windows系统的桌面软件,如果能够做成chrome插件就更好了.

就个人而言,用起来还是比较方便的.

已知问题

无节操视频的地址抓不到,原因是在getId过程中会因为你不是会员所以显示403页面,故无法获取视频id(该类视频使用flvcd也无法进行下载,有待解决).

部分视频没有flashvars,暂时无法抓取.(主要为优酷源,现已解决).

更新

新的源码

BiliGet在线视频地址获取工具

通过HTML5获取mp4视频的方法