整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

手把手教你使用Python网络爬虫获取B站视频选集内容(附源码)

家好,我是Python进阶者。

前言

前几天雪球兄在Python交流群里分享了一个获取B站视频选集的Python代码,小编觉得非常奈斯,这里整理成一篇小文章,分享给大家学习。

关于雪球兄,大家应该都熟悉了,之前他写过Python实战文章,好评如潮,没来得及看的小伙伴,可以戳这里了:

盘点常用验证码标注和识别项目线上部署三种方式——VUE前端、Java后端和Python后端部署

Python项目实战篇——常用验证码标注&识别(CNN神经网络模型训练/测试/部署)

Python项目实战篇——常用验证码标注&识别(前端+后端实现高效率数据标注)

Python项目实战篇——常用验证码标注&识别(数据采集/预处理/字符图切割)

Python项目实战篇——常用验证码标注和识别(需求分析和实现思路)

之前也有给大家分享B站的一些文章,感兴趣的话可以看看这个文章,Python网络爬虫+数据分析:手把手教你用Python网络爬虫获取B站UP主10万条数据并用Pandas库进行趣味数据分析。


一、背景引入

一提到B站,第一印象就是视频,相信很多小伙伴和我一样,都想着去利用网络爬虫技术获取B站的视频吧,但是B站视频其实没有那么好拿到的,关于B站的视频获取,之前有介绍通过you-get库进行实现,感兴趣的小伙伴可以看这篇文章:You-Get 就是这么强势!。

言归正传,经常在B站上学习的小伙伴们可能经常会遇到有的博主连载几十个,甚至几百个视频,尤其像这种编程语言、课程、工具使用等连续的教程,就会出现选集系列,如下图所示。

当然这些选集的字段我们肉眼也是可以看得到的。只是通过程序来实现的话,可能真没有想象的那么简单。那么这篇文章的目标呢,就是通过Python网络爬虫技术,基于selenium库,实现视频选集的获取。


二、具体实现

这篇文章我们用的库是selenium,这个是一个用于模拟用户登录的库,虽然给人的感觉是慢,但是在网络爬虫领域,这个库还是用的蛮多的,用它来模拟登录、获取数据屡试不爽。下面是实现视频选集采集的所有代码,欢迎大家亲自动手实践。

# coding: utf-8
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait


class Item:
    page_num = ""
    part = ""
    duration = ""


    def __init__(self, page_num, part, duration):
        self.page_num = page_num
        self.part = part
        self.duration = duration


    def get_second(self):
        str_list = self.duration.split(":")
        sum = 0
        for i, item in enumerate(str_list):
            sum += pow(60, len(str_list) - i - 1) * int(item)


        return sum


def get_bilili_page_items(url):
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')  # 设置无界面
    options.add_experimental_option('excludeSwitches', ['enable-automation'])
    # options.add_experimental_option("prefs", {"profile.managed_default_content_settings.images": 2,
    #                                           "profile.managed_default_content_settings.flash": 0})


    browser = webdriver.Chrome(options=options)
    # browser = webdriver.PhantomJS()
    print("正在打开网页...")
    browser.get(url)


    print("等待网页响应...")
    # 需要等一下,直到页面加载完成
    wait = WebDriverWait(browser, 10)
    wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@class="list-box"]/li/a')))


    print("正在获取网页数据...")
    list = browser.find_elements_by_xpath('//*[@class="list-box"]/li')
    # print(list)
    itemList = []


    second_sum = 0


    # 2.循环遍历出每一条搜索结果的标题
    for t in list:
        # print("t text:",t.text)
        element = t.find_element_by_tag_name('a')
        # print("a text:",element.text)
        arr = element.text.split('\n')
        print(" ".join(arr))
        item = Item(arr[0], arr[1], arr[2])
        second_sum += item.get_second()
        itemList.append(item)


    print("总数量:", len(itemList))
    # browser.page_source


    print("总时长/分钟:", round(second_sum / 60, 2))
    print("总时长/小时:", round(second_sum / 3600.0, 2))


    browser.close()


    return itemList




get_bilili_page_items("https://www.bilibili.com/video/BV1Eb411u7Fw")

这里用到的选择器是xpath,利用视频示例是B站的《高等数学》同济版 全程教学视频(宋浩老师)视频选集,大家如果想抓取其他视频选集的话,只需要更改上述代码的最后一行的URL链接即可。


三、常见问题

在运行过程中小伙伴们应该会经常遇到这个问题,如下图所示。

这个是因为谷歌驱动版本问题导致的,只需要根据提示,去下载对应的驱动版本即可,驱动下载链接:

https://chromedriver.storage.googleapis.com/index.html


四、总结

我是Python进阶者。这篇文章主要给大家介绍了B站视频选集内容的获取方法,基于网络爬虫,通过selenium库和xpath选择器进行实现,并且给大家例举了常见问题的处理方法。小伙伴们,快快用实践一下吧!如果在学习过程中,有遇到任何问题,欢迎加我好友,我拉你进Python学习交流群共同探讨学习。

不久看到一篇关于BibiBili视频地址解析的源码,正好有兴趣,就自己也研究了研究,记录一下。

1.以http://www.bilibili.com/video/av12535537/ 此视频为例用HttpWatch抓包,很容易就能看出视频的地址是从https://interface.bilibili.com/playurl?quality=4&player=1&cid=20627240&ts=1504780011&sign=f37df88434579a4b009a2bee708a2a71&qn=112里获取的

按照常例分析下参数:

cid很容易猜测出是视频的ID号,在网页源码里面也能找到

Quality和qn是视频的清晰度

ts则是时间戳,这个也不用多说

关键就是这个sign,在HttpWatch中是完全搜不到任何相关的信息的,但网页访问了http://static.hdslb.com/play.swf这个文件,那么sign算法很可能就在这个播放器里。

2.下载这个SWF文件后,载入AS3 Sorcerer,代码一大片不是很好找

那就保存到文本里,在文本中查找”视频地址”这个关键字。

很快就能定位到关键的函数LoadPreview和LoadCidVideo那么接着搜索LoadPreview和LoadCidVideo定位到函数部分,可以看到代码如下

那么关键算法就是在getSign和getSign_v2里面了

[mw_shl_code=actionscript3,true]public function getSign(_arg_1:String):String

{

var _local_2:int;

var _local_4 = null;

var _local_7:int;

var _local_8:int;

var _local_6:int;

var _local_5:int;

var _local_3:int = ESP;

_local_2 = _local_3;

_local_3 = (_local_3 - 48);

_local_5 = 16;

_local_6 = (_local_2 - 37);

ESP = (_local_3 & -16);

_local_7 = CModule.mallocString(_arg_1);

_local_8 = _arg_1.length;

do

{

var _local_9:int = (L__2E_str2 - _local_5);

_local_9 = li8((_local_9 + 16)) /*FlasCC (Alchemy)*/ ;

_local_3 = (_local_3 - 16);

si32(_local_9, (_local_3 + 4)); //FlasCC (Alchemy)

si32(_local_6, _local_3); //FlasCC (Alchemy)

ESP = _local_3;

F_sprintf();

_local_3 = (_local_3 + 16);

_local_5 = (_local_5 + -1);

_local_6 = (_local_6 + 2);

} while (_local_5 != 0);

_local_3 = (_local_3 - 16);

_local_9 = (_local_2 - 4);

si32(_local_9, (_local_3 + 12)); //FlasCC (Alchemy)

_local_9 = (_local_2 - 37);

si32(_local_9, (_local_3 + 8)); //FlasCC (Alchemy)

si32(_local_8, (_local_3 + 4)); //FlasCC (Alchemy)

si32(_local_7, _local_3); //FlasCC (Alchemy)

ESP = _local_3;

F_get_sign();

_local_3 = (_local_3 + 16);

_local_5 = eax;

_local_9 = li32((_local_2 - 4)) /*FlasCC (Alchemy)*/ ;

_local_4 = CModule.readString(_local_5, _local_9);

if (_local_7 != 0)

{

_local_3 = (_local_3 - 16);

si32(_local_7, _local_3); //FlasCC (Alchemy)

ESP = _local_3;

F_IDAlloc();

_local_3 = (_local_3 + 16);

};

if (_local_5 != 0)

{

_local_3 = (_local_3 - 16);

si32(_local_5, _local_3); //FlasCC (Alchemy)

ESP = _local_3;

F_idalloc();

_local_3 = (_local_3 + 16);

};

var _local_10 = _local_4;

_local_3 = _local_2;

ESP = _local_3;

return (_local_10);

}

}//package com.bilibili.interfaces[/mw_shl_code]

[mw_shl_code=actionscript3,true]public function getSign_v2(_arg_1:String, _arg_2:int):String

{

var _local_12:*;

var _local_3:int;

var _local_5 = null;

var _local_6:int;

var _local_11:int;

var _local_8:int;

var _local_10:int;

var _local_9:int;

var _local_4:int = ESP;

_local_3 = _local_4;

_local_4 = (_local_4 - 48);

_local_6 = _arg_2;

if (_local_6 >= 5)

{

_local_12 = _arg_1;

}

else

{

_local_8 = (L__2E_str2 + (_local_6 << 4));

_local_9 = 16;

_local_10 = (_local_3 - 37);

ESP = (_local_4 & -16);

_local_6 = CModule.mallocString(_arg_1);

_local_11 = _arg_1.length;

do

{

var _local_7:int = (_local_8 - _local_9);

_local_7 = li8((_local_7 + 16)) /*FlasCC (Alchemy)*/ ;

_local_4 = (_local_4 - 16);

si32(_local_7, (_local_4 + 4)); //FlasCC (Alchemy)

si32(_local_10, _local_4); //FlasCC (Alchemy)

ESP = _local_4;

F_sprintf();

_local_4 = (_local_4 + 16);

_local_9 = (_local_9 + -1);

_local_10 = (_local_10 + 2);

} while (_local_9 != 0);

_local_4 = (_local_4 - 16);

_local_7 = (_local_3 - 4);

si32(_local_7, (_local_4 + 12)); //FlasCC (Alchemy)

_local_7 = (_local_3 - 37);

si32(_local_7, (_local_4 + 8)); //FlasCC (Alchemy)

si32(_local_11, (_local_4 + 4)); //FlasCC (Alchemy)

si32(_local_6, _local_4); //FlasCC (Alchemy)

ESP = _local_4;

F_get_sign();

_local_4 = (_local_4 + 16);

_local_10 = eax;

_local_7 = li32((_local_3 - 4)) /*FlasCC (Alchemy)*/ ;

_local_5 = CModule.readString(_local_10, _local_7);

if (_local_6 != 0)

{

_local_4 = (_local_4 - 16);

si32(_local_6, _local_4); //FlasCC (Alchemy)

ESP = _local_4;

F_idalloc();

_local_4 = (_local_4 + 16);

};

if (_local_10 != 0)

{

_local_4 = (_local_4 - 16);

si32(_local_10, _local_4); //FlasCC (Alchemy)

ESP = _local_4;

F_idalloc();

_local_4 = (_local_4 + 16);

};

_local_12 = _local_5;

};

_local_4 = _local_3;

ESP = _local_4;

return (_local_12);

}

}//package com.bilibili.interfaces[/mw_shl_code]

可以看到,二者代码非常相似,而且光看代码无法找到有用的信息,那么就得想办法去调试了

3.目前我只知道的办法是:使用JPEXS反编译软件来修改SWF代码(插Log),然后通过Fiddler劫持替换SWF,通过显示Log信息来进行调试

要显示log信息就得安装debug版本的flash

http://www.adobe.com/support/flashplayer/debug_downloads.html

为方便阅读日志信息可以安装Cygwin

具体步骤我也是参考了http://blog.csdn.net/hot_vc/article/details/50600717这篇文章

————————————————————————————————————————————————————————————————————————————

将SWF载入JPEXS

要想修改SWF的源码就得去修改PCODE,第一次接触这种也不是很懂,觉得有点类似于C#的IL代码

通过AS3代码和PCODE代码的一一进行对比,还是能找到一些规律的

为了查看代码中变量值,用到以下代码

[mw_shl_code=javascript,true]findpropstrict Qname(PackageNamespace(""),"trace")

getlocal 5

callpropvoid Qname(PackageNamespace(""),"trace") 1[/mw_shl_code]

这个代码就相当于trace(_loc5_);

修改保存后利用Fiddler来劫持替换我们的SWF

这样Cygwin就能输出我们想要看到的结果了。

其中有些变量的值是地址,而不是数据,为了读取地址中的数据,利用到了这句代码CModule.readString(地址,读取长度);

按照原PCODE的格式,写出插LOG时的PCODE

[mw_shl_code=asm,true]pushint 60

setlocal 8

getlex Qname(PackageNamespace("com.bilibili.interfaces"),"CModule")

getlocal 7

getlocal 8

callproperty Qname(PackageNamespace(""),"readString") 2

coerce_s

setlocal 5

findpropstrict Qname(PackageNamespace(""),"trace")

getlocal 5

callpropvoid Qname(PackageNamespace(""),"trace") 1[/mw_shl_code]

此代码相当于

_loc8_:int = 60;

_loc5_:* = CModule.readString(_loc7_,_loc8_);

trace(_loc5_);

4.其实关键算法在F_get_sign()中,因为从F_get_sign()函数中出来后,eax中就已经存放带着sign的视频URL的地址了。

F_get_sign()函数过于复杂,总之通过不断地插这两种Log查看变量信息,最终找到了算法

由这个地方可以看出是MD5,还有一个位置能够准确地查看到进行MD5处理的数据(已经忘了.....)

算法如下:

五种清晰度

qn=16,quality=1

qn=32,quality=5

qn=48,quality=2

qn=64,quality=2

qn=80,quality=3

qn=112,quality=4

三种类型的视频bili2,bangumi,movie

bili2:

time = F_clock_gettime();//相当于取一个时间随机数

sign = MD5(“cid=” + cid + “&player=1&qn=112&quality=4&ts=” + time + “1c15888dc316e05a15fdd0a02ed6584f”)

url = “https://interface.bilibili.com/playurl?player=1&qn=112&cid=” + cid + “&quality=4&ts=” + time + “&sign=” + sign

bangumi:

time = F_clock_gettime();//相当于取一个时间随机数

sign = MD5(“cid=” + cid + “&module=bangumi” + “&player=1” + “&qn=112” + “&quality=4” + “&ts=” + time + “9b288147e5474dd2aa67085f716c560d”)

url = “https://bangumi.bilibili.com/player/web_api/playurl?cid=” + cid + “&player=1” + “&module=bangumi” + “&qn=112” + “&quality=4&ts=” + time + “&sign=” + sign

movie:

time = F_clock_gettime();//相当于取一个时间随机数

sign = MD5(“cid=” + cid + “&module=movie” + “&player=1” + “&qn=112” + “&quality=4” + “&ts=” + time + “9b288147e5474dd2aa67085f716c560d”)

url = “https://bangumi.bilibili.com/player/web_api/playurl?cid=” + cid + “&player=1” + “&module=movie” + “&qn=112” + “&quality=4&ts=” + time + “&sign=” + sign

也就只分析到了这里

顺便说下,有些tx.acgvideo.com开头的视频无法下载是因为服务器检查了Refer,只要在协议头里面加入Refer:[media]https://static.hdslb.com/play.swf[/media],就能下载了

水平有限,分析若有什么错误,大家看着办吧

欢迎大家在评论区留下你的意见。谢谢 诸葛商学院

家好,今天小编就以B站为例,带大家爬取视频,学会之后你也能爬取你想要的视频!或不多说,上正文

爬虫用的好,牢饭吃的早!

本文仅作知识分享,切勿用于违法行为!

下载仓库

git@github.com:inspurer/PythonSpider.git
或者直接下载:https://github.com/inspurer/PythonSpider/tree/master/bilibili

替换

随便打开一个b站的界面,比如

将url复制到代码中去,运行代码,稍等一会儿,上述图中的视频就被下载下来了。

How to Code

分析网页源码

按f12浏览器开发者工具 ,通过一番审查,我们定位到视频的url在网页源代码的位置如下:

window.__playinfo__={
"from":"local",
"result":"suee",
"quality":32,
"format":"flv480",
"timelength":408884,
"accept_format":"flv720,flv480,flv360",
"accept_description":["高清 720P","清晰 480P","流畅 360P"],
"accept_quality":[64,32,15],
"video_codecid":7,
"video_project":true,
"seek_param":"start",
"seek_type":"offset",
"durl":[{"order":1,"length":408884,"size":42782550,"ahead":"EhA=","vhead":"AWQAHv/hAB5nZAAerNlA2D3n//AoACfxAAADAAEAAAMAMA8WLZYBAAVo6+zyPA==",
"url":"http://upos-hz-mirrorkodo.acgvideo.com/upgcxcode/48/61/45596148/45596148-1-32.flv?e=ig8euxZM2rNcNbRa7b4VhoMz7WhjhwdEto8g5X10ugNcXBlqNxHxNEVE5XREto8KqJZHUa6m5J0SqE85tZvEuENvNC8xNEVE9EKE9IMvXBvE2ENvNCImNEVEK9GVqJIwqa80WXIekXRE9IB5QK==&deadline=1543136253&dynamic=1&gen=playurl&oi=1862807981&os=kodo&platform=pc&rate=176800&trid=69ea1a81ac21448f9e2189ef479a2d6d&uipk=5&uipv=5&um_deadline=1543136253&um_sign=c479f3fd3075b359d0a04e5eb584ac55&upsig=1c05ca3838af92d2c1411cf3000e8345http://upos-hz-mirrorkodo.acgvideo.com/upgcxcode/48/61/45596148/45596148-1-32.flv?e=ig8euxZM2rNcNbRa7b4VhoMz7WhjhwdEto8g5X10ugNcXBlqNxHxNEVE5XREto8KqJZHUa6m5J0SqE85tZvEuENvNC8xNEVE9EKE9IMvXBvE2ENvNCImNEVEK9GVqJIwqa80WXIekXRE9IB5QK==&deadline=1543136253&dynamic=1&gen=playurl&oi=1862807981&os=kodo&platform=pc&rate=176800&trid=69ea1a81ac21448f9e2189ef479a2d6d&uipk=5&uipv=5&um_deadline=1543136253&um_sign=c479f3fd3075b359d0a04e5eb584ac55&upsig=1c05ca3838af92d2c1411cf3000e8345","backup_url":["http://upos-hz-mirrorcos.acgvideo.com/upgcxcode/48/61/45596148/45596148-1-32.flv?um_deadline=1543136253&platform=pc&rate=176800&oi=1862807981&um_sign=c479f3fd3075b359d0a04e5eb584ac55&gen=playurl&os=cos&trid=69ea1a81ac21448f9e2189ef479a2d6d"]}]}

最后的url就是我们想要的结果。
如果在浏览器中查找不方便的话,我们可以把通过代码把网页源码输出到本地

response = requests.get(url='https://www.bilibili.com/video/av26522634', headers= self.getHtmlHeaders)
        print(response.status_code)
        if response.status_code == 200:
            print(response.text)

为了伪装成浏览器,我们需要在reqests添加Headers
这个Headers需要我们去浏览器中手动获取
切换到NetWork标签下,再选择Headers,

   self.getHtmlHeaders={
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        'Accept-Encoding': 'gzip, deflate, br',
        'Accept-Language': 'zh-CN,zh;q = 0.9'
    }

这里只选择了几个关键的

解析得到视频地址

根据上一步分析,我们得到了网页的源码,并在源码中定位到了视频地址,接下来,我们就用代码自动获取这个地址了

#用正则、json得到视频url;用pq失败后的无奈之举
pattern = r'\<script\>window\.__playinfo__=(.*?)\</script\>'
result = re.findall(pattern, html)[0]
temp = json.loads(result)
#temp['durl']是一个列表,里面有很多字典
#video_url = temp['durl']
for item in temp['data']['durl']:
	if 'url' in item.keys():
		video_url = item['url']

顺便获取下视频的名字:

#用pq解析得到视频标题
doc = pq(html)
video_title = doc('#viewbox_report > h1 > span').text()然后组合返回下:

然后组合返回下:

return{
 'title': video_title,
 'url': video_url
 }

下载视频

通过在开发者工具中搜索关键词,比如上面得到的视频url,我们可以定位到在浏览器中真正下载视频的请求在哪

然后把它的Headers添加到reqests中,就可以下载视频了

 with open(filename, "wb") as f:
 	 f.write(requests.get(url=url, headers=self.downloadVideoHeaders, stream=True, verify=False).content)

你下载的视频在本地播放不了,请不要试图修改源代码中保存文件的格式由.flv改成.mp4,因为b站的视频本来就是flv格式的,需要用特殊的视频播放器播放,这里推荐一个无毒无害的KMPlayer,链接:https://pan.baidu.com/s/1O4-Uia04Vm-jbUjyrVWfkw 提取码:4l11

结尾

最后多说一句,小编是一名python开发工程师,这里有我自己整理了一套最新的python系统学习教程,包括从基础的python脚本到web开发、爬虫、数据分析、数据可视化、机器学习等。想要这些资料的可以关注小编,并在后台私信小编:“01”即可领取。


本文的文字及图片来源于网络加上自己的想法,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。