b站鬼畜区一直是本人的快乐源泉,这次选取鬼畜巨头波澜哥的原版素材来简单分析波澜哥为何能成为鬼畜巨头之一!?
其实这个难度并不高,本文会展示完整的爬取过程。读完本文,有Python和爬虫基础的朋友可以学会爬取b站弹幕并做出词云。
0 踩点
链接https://www.bilibili.com/video/av10729179
第一件事都是去网页踩点,把网页套路摸清之后再写代码。
打开网页,右键-检查,打开开发者工具。点击左上角 鼠标+方框 的图标,再点击网页中的弹幕,这样可以快速找到弹幕在element里的位置。
滑动弹幕,观察element面板的变化。
弹幕共有1000条,看起来element在不断更新,始终维持着只显示20条弹幕。这1000条弹幕只有20条显示在element里,我们爬一次就只能获取这20条。
换个方法,切换到Network面板。滑动弹幕,观察面板的变化。
然而Network面板没有任何变化...这说明这些弹幕并不是后来动态加载的,注意不是后来!如果在加载之后再打开Network,Network是截取不到的!
求助舍友之后得知可能是这些弹幕是早就全部加载出来了,保存在某个地方,但是没有全部显示出来。用户想看哪20条,才把这20条显示出来。那我们就要找这1000条弹幕放在了什么地方。
1 找到弹幕
把开发者工具保持在打开状态,刷新网页,所有的元素都会重新加载。Network面板会把所有文件都截取下来,当然也包括我们要找的弹幕。
但是截取到的文件有300个之多!那有没有办法很快地把弹幕文件找出来?没有!只能一个个找,并且观察文件名和preview来推测哪个有可能是弹幕文件。
经过一番寻找我放弃了,于是去CSDN查找资料。发现,b站有一个接口存放弹幕。就是下面这个↓
看到网址里面有dm两个字母,应该是"danmu"的意思了。
藏得好深啊,在preview也是一片空白的,相当鸡贼。
打开网址。
奇怪的是用Chrome打开,所有弹幕都连在一起...但是用火狐打开就正常了。估计是浏览器的问题。
结构很简单!所有弹幕都在d节点里,只需要获取d节点里面的文本内容就行了。
2 截取oid
这里还有个坑,视频的av号是10729179,网址的oid参数却是17701749,两者不一样?!
那我们就想办法搞到oid参数。
我们先获取视频网页的内容,打印出来,在打印出来的内容中搜索oid(17701749)。看前后有什么特征,以确定oid的位置。
私信小编01 领取此项目完整代码哦!
我们可推测,每个视频都会出现".avgvedio/upgcxcode"这一字符串,并且它的后面总是跟着oid参数。我们就可以先搜索出".avgvedio/upgcxcode"所在的位置,再数它和oid相差了多少个字符,找到oid的位置。
def get_oid(): response = requests.get(av_url, headers=headers) text = response.text start = text.find('.acgvideo.com/upgcxcode') end = text[start+30:].find('/') oid = text[start+30:start+30+end] return oid
注意oid的长度不一定为8。因为oid后面总是'/',我们可以找到'/'的位置,再把oid截取出来。
这里有很多个'/',但我们找的是oid之后的第一个'/'。所以从下标start+30的位置开始找'/'。
这一步需要多次的尝试和用其他的视频验证。"avgvedio/upgcxcode"和start+30是多次尝试得出的。我一开始以为oid的长度固定是8,在用其他视频测试时发现并不是,所以用了新的方法找oid的结束位置。
虽然有点麻烦但我们还是通过av号得到了oid!
3 获取d节点
向网址https://api.bilibili.com/x/v1/dm/list.so?oid=17701749发送请求,获取其HTML文档,然后把文档传入解析器。
解析器是帮助我们处理HTML的工具。字符串格式的HTML文档经过解析器的处理之后,就不再是字符串。每个节点都成了一个实例对象,都有相应的方法满足我们的操作需求。节点之间用树结构组织起来。
有了解析器的帮助我们的工作会变得简单许多。这个网页结构很简单,用正则甚至直接字符串查找都可以。
def main(): # 获取response dm_url = dm_url_base + get_oid() response = requests.get(dm_url) # 设置编码格式,否则无法显示中文 response.encoding = 'utf-8' # 传入解析器 soup = BeautifulSoup(response.text, 'lxml') # 找到所有的d节点,保存到dms列表中 dms = soup.find_all('d') # 把dms列表的内容保存到文件 save_dm(dms) # 生成词云 get_word_cloud()
4 保存到文件
save_dm()函数。
def save_dm(dms): with open(FILE_NAME, 'w', encoding='utf-8') as f: for dm in dms: # .string 获取节点的文本内容 text = dm.string f.write(text + '')
打开文件看看~
成功提取并保存弹幕。但这样1000条弹幕看着是非常吃力的,所以我们的下一步工作就是:把弹幕做成词云!
5 生成词云
先学习一下使用wordcwloud包生成词云的步骤。
- 生成词云类的一个实例对象
- wc = wordcloud.WordCloud()
- 把文本加载到对象中
- wc.generate(text)
- text是str类型的
- 保存成图片
- wc.to_file(file_name)
第2第3步都很简单。第1步生成对象时可以传入许多参数,这里说一下部分参数的作用。
- width:宽度
- height:高度
- min_font_size:词云的结果就是大大小小的词语。词语的字号越大,表示该词语出现的出现频率越高。频率过低的词语,字号会很小,就会被这个参数限制。某个词语的字号如果小于这个参数,就不会出现在生成的词云图片中。
- max_font_size:和min_font_size类似。限制的是字号大,也就是频率高的词语。
- font_step:相邻频率词语的字号相差大小。
- font_path:选用的字体的路径,有默认字体。但默认字体显示不出中文。需要到C:WindowsFonts这个文件夹下找一个字体。
- max_words:词云显示的最大词语数量,默认200。
- stop_words:词云不会显示的词语。
- background_color:词云的背景颜色,默认为黑色。
- scale:缩放比例,如果词云图片不清晰,设置更高的scale也会使图片清晰。
- mask:词云的形状,默认是长方形。这是最好玩的一个参数,这个参数让词云更加生动和多样。生成词云的时候,会在传入的图片非纯白色的地方填词。需要借用其他包把图片转换成矩阵表示。把矩阵作为参数传给mask。这里使用numpy包的np类的array()方法。
from PIL import Imagefrom numpy import npimg = Image.open(file_name)img_matrix = np.array(img)
- 传入的矩阵中,只有像素值(255, 255, 255)即纯白的点不会填入词语,其余的点都会。
def get_word_cloud(): with open(FILE_NAME, 'r', encoding='utf-8') as f: img_matrix = Image.open(IMG_PATH) mask = np.array(img_matrix) wc = WordCloud(mask=mask, scale=4, font_path=r'C:WindowsFontssimhei.ttf', background_color='white', max_font_size=40, min_font_size=2) text = f.read() wc.generate(text) wc.to_file(av_ID + 'wc.png')
看结果。
似乎有点问题?这是词云吗?不是,这是句云好不好!都是以句为单位显示出来的,不利于我们对关键词的分析。
为什么会这样呢?因为wordcloud是针对英文做词云的。英文语法中单词都以空格或其他符号分开,分词非常容易。
中文里一个句子的所有词语都是连在一起的。以英文语法的思维处理中文就行不通了。
办法当然也是有的,用别人写好的分词包就行啦。(再喊一句开源大法好!)
这里使用中文分词包jieba。导入jieba包之后直接调用cut()方法分词,然后模仿英文语法在两个词之间加一个空格。
def transform(text): word_list = jieba.cut(text) # 分词后在单独个体之间加上空格 result = ' '.join(word_list) return result
jieba有一个用人民日报等素材训练得出的中文大词典,其他细节我懒得研究,所以不多讲了。还想吐槽一下为什么这个包叫jieba...是结巴的意思?
在generate()之前加一句:
text = transform(text)
这么处理之后:
(分词之后也还是好多的哈哈哈....)
大功告成啦!看词云我们发现,最大的词是“哈哈哈”,“哈哈哈哈”也不小,一定是这个视频太搞笑了!
第二大的是”时代“、”前来“、”考古“,这几个词说明即使经过了相当一段时间,b站小伙伴们还是会回头再看这个视频,可能是太魔性太洗脑了难以忘怀。说明波澜哥能经受住考验,其巨头地位不易被新兴的鬼畜明星动摇。
既然我们已经把这一大串功能完成了,做其他视频的弹幕词云也不过是改个av号的事嘛。
(番剧的网页结构有所不同,本文代码需要稍加修改才能爬番剧的弹幕)
面筋哥鬼畜原版视频。
乔碧萝翻车视频。
...
住手你们不要再打了啦!
↓
改革春风吹满地。嗷??
6 无题
爬取和储存和读取的时候都遇到了编码错误!关于编码问题,encoding='utf-8'解决一切。对编码这一块还是不懂原理,导致每次遇到问题都只能碰运气解决。
headers不用设置太多参数,一个User-Agent就够了。参数太多反而会被403禁止访问。也不太懂为什么...
为什么我执着要通过av号得到oid?如果只爬一个视频,我们确实可以人工去找到弹幕网址,但如果需要爬取多个视频的弹幕呢?这时候就必须让程序去干了。
比如,我们可以先在搜索框输入某个人名,然后爬取搜索结果的av号,然后再通过av号爬取视频的弹幕,大规模地分析相关信息。这样的难度会大一点点,有时间会把这个思路实现写出来!
得知Python之父Guido退休,在此表达对大佬的崇拜和感谢,也祝他生活健康愉快!