• Python 网络爬虫实战:爬取 B站《全职高手》20万条评论数据


    本周我们的目标是:B站(哔哩哔哩弹幕网 https://www.bilibili.com )视频评论数据。

    我们都知道,B站有很多号称“镇站之宝”的视频,拥有着数量极其恐怖的评论和弹幕。所以这次我们的目标就是,爬取B站视频的评论数据,分析其为何会深受大家喜爱。

    首先去调研一下,B站评论数量最多的视频是哪一个。。。好在已经有大佬已经统计过了,我们来看一哈!

    ​【B站大数据可视化】B站评论数最多的视频究竟是?来自 <https://www.bilibili.com/video/av34900167/>

     

    嗯?《全职高手》,有点意思,第一集和最后一集分别占据了评论数量排行榜的第二名和第一名,远超了其他很多很火的番。那好,就拿它下手吧,看看它到底强在哪儿。

    废话不多说,先去B站看看这部神剧到底有多好看 https://www.bilibili.com/bangumi/play/ep107656

    额,需要开通大会员才能观看。。。

    好吧,不看就不看,不过好在虽然视频看不了,评论却是可以看的。

    感受到它的恐怖了吗?63w6条的评论!9千多页!果然是不同凡响啊。

    接下来,我们就开始编写爬虫,爬取这些数据吧。

     


    使用爬虫爬取网页一般分为四个阶段:分析目标网页,获取网页内容,提取关键信息,输出保存。

    1. 分析目标网页

    • 首先观察评论区结构,发现评论区为鼠标点击翻页形式,共 9399 页,每一页有 20 条评论,每条评论中包含 用户名、评论内容、评论楼层、时间日期、点赞数等信息展示。

    • 接着我们按 F12 召唤出开发者工具,切换到Network。然后用鼠标点击评论翻页,观察这个过程有什么变化,并以此来制定我们的爬取策略。

    • 我们不难发现,整个过程中 URL 不变,说明评论区翻页不是通过 URL 控制。而在每翻一页的时候,网页会向服务器发出这样的请求(请看 Request URL)。

    • 点击 Preview 栏,可以切换到预览页面,也就是说,可以看到这个请求返回的结果是什么。下面是该请求返回的 json 文件,包含了在 replies 里包含了本页的评论数据。在这个 json 文件里,我们可以发现,这里面包含了太多的信息,除了网页上展示的信息,还有很多没展示出来的信息也有,简直是挖到宝了。不过,我们这里用不到,通通忽略掉,只挑我们关注的部分就好了。

    2. 获取网页内容

    网页内容分析完毕,可以正式写代码爬了。

     1 import requests
     2 
     3 def fetchURL(url):
     4     '''
     5     功能:访问 url 的网页,获取网页内容并返回
     6     参数:
     7         url :目标网页的 url
     8     返回:目标网页的 html 内容
     9     '''
    10     headers = {
    11         'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
    12         'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
    13     }
    14     
    15     try:
    16         r = requests.get(url,headers=headers)
    17         r.raise_for_status()
    18         print(r.url)
    19         return r.text
    20     except requests.HTTPError as e:
    21         print(e)
    22         print("HTTPError")
    23     except requests.RequestException as e:
    24         print(e)
    25     except:
    26         print("Unknown Error !")
    27         
    28 
    29 if __name__ == '__main__':
    30     url = 'https://api.bilibili.com/x/v2/reply?callback=jQuery172020326544171595695_1541502273311&jsonp=jsonp&pn=2&type=1&oid=11357166&sort=0&_=1541502312050'
    31     html = fetchURL(url)
    32     print(html)

    不过,在运行过后,你会发现,403 错误,服务器拒绝了我们的访问。

    运行结果:

    403 Client Error: Forbidden for url: https://api.bilibili.com/x/v2/reply?callback=jQuery172020326544171595695_1541502273311&jsonp=jsonp&pn=2&type=1&oid=11357166&sort=0&_=1541502312050
    HTTPError
    None

    同样的,这个请求放浏览器地址栏里面直接打开,会变403,什么也访问不到。

    这是我们本次爬虫遇到的第一个坑。在浏览器中能正常返回响应,但是直接打开请求链接时,却会被服务器拒绝。(我第一反应是 cookie ,将浏览器中的 cookie 放入爬虫的请求头中,重新访问,发现没用),或许这也算是一个小的反爬虫机制吧。

    网上查阅资料之后,我找到了解决的方法(虽然不了解原理),原请求的 URL 参数如下:

    callback = jQuery1720913511919053787_1541340948898
    jsonp = jsonp
    pn = 2
    type = 1
    oid = 11357166&sort=0
    _ = 1541341035236

    其中,真正有用的参数只有三个:pn(页数),type(=1)和oid(视频id)。删除其余不必要的参数之后,用新整理出的url去访问,成功获取到评论数据。

    https://api.bilibili.com/x/v2/reply?type=1&oid=11357166&pn=2

    然后,在主函数中,通过写一个 for 循环,通过改变 pn 的值,获取每一页的评论数据。

    1 if __name__ == '__main__':
    2     for page in range(0,9400):
    3         url = 'https://api.bilibili.com/x/v2/reply?type=1&oid=11357166&pn=' + str(page)
    4         html = fetchURL(url)

    3. 提取关键信息

    通过 json 库对获取到的响应内容进行解析,然后提取我们需要的内容:楼层,用户名,性别,时间,评价,点赞数,回复数。

     1 import json
     2 import time
     3 
     4 def parserHtml(html):
     5     '''
     6     功能:根据参数 html 给定的内存型 HTML 文件,尝试解析其结构,获取所需内容
     7     参数:
     8             html:类似文件的内存 HTML 文本对象
     9     '''
    10     s = json.loads(html)
    11 
    12     for i in range(20):
    13         comment = s['data']['replies'][i]
    14 
    15         # 楼层,用户名,性别,时间,评价,点赞数,回复数
    16         floor = comment['floor']
    17         username = comment['member']['uname']
    18         sex = comment['member']['sex']
    19         ctime = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(comment['ctime']))
    20         content = comment['content']['message']
    21         likes = comment['like']
    22         rcounts = comment['rcount']
    23 
    24         print('--'+str(floor) + ':' + username + '('+sex+')' + ':'+ctime)
    25         print(content)
    26         print('like : '+ str(likes) + '      ' + 'replies : ' + str(rcounts))
    27         print('  ')
    部分运行结果如下:
    --204187:day可可铃(保密):2018-11-05 18:16:22
    太太又出本了,这次真的木钱了(´;ω;`)
    like : 1      replies : 0
      
    --204186:长夜未央233(女):2018-11-05 16:24:52
    12区打卡
    like : 2      replies : 0
      
    --204185:果然还是人渣一枚(男):2018-11-05 13:48:09
    貌似忘来了好几天
    like : 1      replies : 1
      
    --204183:day可可铃(保密):2018-11-05 13:12:38
    要准备去学校了,万恶的期中考试( ´_ゝ`)
    like : 2      replies : 0
      
    --204182:拾秋以叶(保密):2018-11-05 12:04:19
    11月5日打卡( ̄▽ ̄)
    like : 1      replies : 0
      
    --204181:芝米士哒(女):2018-11-05 07:53:43
    这次是真的错过了一个亿[蛆音娘_扶额]
    like : 2      replies : 1
    
    

    4. 保存输出

    我们把这些数据以 csv 的格式保存于本地,即完成了本次爬虫的全部任务。下面附上爬虫的全部代码。

      1 import requests
      2 import json
      3 import time
      4 
      5 def fetchURL(url):
      6     '''
      7     功能:访问 url 的网页,获取网页内容并返回
      8     参数:
      9         url :目标网页的 url
     10     返回:目标网页的 html 内容
     11     '''
     12     headers = {
     13         'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
     14         'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
     15     }
     16     
     17     try:
     18         r = requests.get(url,headers=headers)
     19         r.raise_for_status()
     20         print(r.url)
     21         return r.text
     22     except requests.HTTPError as e:
     23         print(e)
     24         print("HTTPError")
     25     except requests.RequestException as e:
     26         print(e)
     27     except:
     28         print("Unknown Error !")
     29         
     30 
     31 def parserHtml(html):
     32     '''
     33     功能:根据参数 html 给定的内存型 HTML 文件,尝试解析其结构,获取所需内容
     34     参数:
     35             html:类似文件的内存 HTML 文本对象
     36     '''
     37     try:
     38         s = json.loads(html)
     39     except:
     40         print('error')
     41         
     42     commentlist = []
     43     hlist = []
     44 
     45     hlist.append("序号")
     46     hlist.append("名字")
     47     hlist.append("性别")
     48     hlist.append("时间")
     49     hlist.append("评论")
     50     hlist.append("点赞数")
     51     hlist.append("回复数")
     52 
     53     #commentlist.append(hlist)
     54 
     55     # 楼层,用户名,性别,时间,评价,点赞数,回复数
     56     for i in range(20):
     57         comment = s['data']['replies'][i]
     58         blist = []
     59 
     60         floor = comment['floor']
     61         username = comment['member']['uname']
     62         sex = comment['member']['sex']
     63         ctime = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(comment['ctime']))
     64         content = comment['content']['message']
     65         likes = comment['like']
     66         rcounts = comment['rcount']
     67 
     68         blist.append(floor)
     69         blist.append(username)
     70         blist.append(sex)
     71         blist.append(ctime)
     72         blist.append(content)
     73         blist.append(likes)
     74         blist.append(rcounts)
     75 
     76         commentlist.append(blist)
     77 
     78     writePage(commentlist)
     79     print('---'*20)
     80 
     81 def writePage(urating):
     82     '''
     83         Function : To write the content of html into a local file
     84         html : The response content
     85         filename : the local filename to be used stored the response
     86     '''
     87     
     88     import pandas as pd
     89     dataframe = pd.DataFrame(urating)
     90     dataframe.to_csv('Bilibili_comment5-1000条.csv', mode='a', index=False, sep=',', header=False)
     91 
     92 
     93 if __name__ == '__main__':
     94     for page in range(0,9400):
     95         url = 'https://api.bilibili.com/x/v2/reply?type=1&oid=11357166&pn=' + str(page)
     96         html = fetchURL(url)
     97         parserHtml(html)
     98 
     99         # 为了降低被封ip的风险,每爬20页便歇5秒。
    100         if page%20 == 0:
    101             time.sleep(5)

    写在最后

    在爬取过程中,还是遇到了很多的小坑的。

    1. 请求的 url 不能直接用,需要对参数进行筛选整理后才能访问。

    2. 爬取过程其实并不顺利,因为如果爬取期间如果有用户发表评论,则请求返回的响应会为空导致程序出错。所以在实际爬取过程中,记录爬取的位置,以便出错之后从该位置继续爬。(并且,挑选深夜一两点这种发帖人数少的时间段,可以极大程度的减少程序出错的机率)

    3. 爬取到的数据有多处不一致,其实这个不算是坑,不过这里还是讲一下,免得产生困惑。

            a. 就是评论区楼层只到了20多万,但是评论数量却有63万多条,这个不一致主要是由于B站的评论是可以回复的,回复的评论也会计算到总评论数里。我们这里只爬楼层的评论,而评论的回复则忽略,只统计回复数即可。

            b. 评论区楼层在20万条左右,但是我们最后爬取下来的数据只有18万条左右,反复检查爬虫程序及原网站后发现,这个属于正常现象,因为有删评论的情况,评论删除之后,后面的楼层并不会重新排序,而是就这样把删掉的那层空下了。导致楼层数和评论数不一致。


     如果文章中有哪里没有讲明白,或者讲解有误的地方,欢迎在评论区批评指正,或者扫描下面的二维码,加我微信,大家一起学习交流,共同进步。

  • 相关阅读:
    linux环境下MongoDB的部署及应用
    Memcache,Redis,MongoDB三种非关系型数据库的对比
    什么是事务
    umount卸载目录的时候,提示正忙
    Maven私服Nexus3.x环境部署应用
    执行yum提示error: rpmdb: BDB0113 Thread/process 9060/139773561796608 failed: BDB1507 Thread died in Berkeley DB library
    vim 常用
    nginx的部署和配置
    linux系统异常关机导致报文件系统只读Read-only file system的解决方法
    js拖拽
  • 原文地址:https://www.cnblogs.com/smartcrane/p/13172825.html
Copyright © 2020-2023  润新知