【网络爬虫 | python】bilibili评论信息爬取(基础版)

【网络爬虫 | python】bilibili评论信息爬取(基础版),第1张

前言:用requests爬取bilibili评论区一、二级评论,并存入mysql数据库或csv文件

目标数据:姓名, 性别, 用户等级, 用户uid, 个性签名, 评论时间, 内容, 点赞, 回复, rpid

案例视频:开学啦~我终于收到了霍格沃茨的录取通知书_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV14h411n7ok

目录

一、案例分析

二、获取数据

        1.数据获取 # 一级评论。

         2.数据清洗

        3.数据获取 # 二级评论

        4.数据清洗 # 二级评论

三、数据存储

1、csv:

2、mysql

四、多线程

五、综合。

六、完整代码


一、案例分析

 首先获取评论数据api,发现所有数据都在json格式的文件中,可以用字典提取数据     

通过f12我们可以得到评论api。删除第一个和最后一个参数,得到

一级评论:https://api.bilibili.com/x/v2/reply/main?jsonp=jsonp&next=0&type=1&oid=208143004&mode=3&plat=1

  •         next:翻页
  •         oid:视频编号(aid)
  •         mode:1、2表示按热度、时间排序;  0、3表示按热度排序,并显示评论和用户信息

二级评论:https://api.bilibili.com/x/v2/reply/reply?jsonp=jsonp&pn=1&type=1&oid=208143004&ps=10&root=5453611704

  •         pn:翻页
  •         oid:视频oid
  •         root:楼主的回复的rpid
  •         ps: 单页显示数量(最大为20)

为什么要删除第一个和最后一个参数?因为我们不需要js请求,最后一个参数也没有什么影响

其余参数为固定参数,不可变

视频的oid可通过视频BV号获取,rpid可以通过一级评论获取

二、获取数据         1.数据获取 # 一级评论。

        定义bilibili类。首先通过视频BV号获取oid

import requests
import re

class Bilibili:
    def __init__(self, BV):
        self.homeUrl = "https://www.bilibili.com/video/BV14h411n7ok"
        self.oid_get(BV)
        
    # 获取视频 oid
    def oid_get(self, BV):
        # 请求视频页面
        response = requests.get(url=self.homeUrl + BV).text
        # 用正则表达式 通过视频 bv 号获取 oid
        self.oid = re.findall("\"aid\":([0-9]*),", response)[0]

        我们通过oid构造出评论数据的url。oid,mode,ps参数可以预先设置,但是翻页和二级评论的页面数是需要改变的

        为了防止被识别为爬虫程序,需要设置请求头user-agent和cookie。

        user-agent可以用fake_useragent库构造。但这个库在使用过程中出现了很多问题。

        我干脆直接复制浏览器的过来了。如果需要的话可以在

https://fake-useragent.herokuapp.com/browsers/0.1.11

 这里看看,里面有很多的user-agent,可以自己做一个useragent池。需要科学上网才能访问到。也可以在这找。把ua和cookie都复制过来(字典格式)

什么是科学上网?

import requests
import re
import queue
import time

class Bilibili:
    def __init__(self, BV, mode, cookies, page):
        self.homeUrl = "https://www.bilibili.com/video/"
        self.oid_get(BV)

        self.replyUrl = "https://api.bilibili.com/x/v2/reply/main?jsonp=jsonp&type=1&oid={oid}&mode={mode}&plat=1&next=".format(oid=self.oid, mode=mode)
        self.rreplyUrl = "https://api.bilibili.com/x/v2/reply/reply?jsonp=jsonp&type=1&oid={oid}&ps=20&root={root}&pn=".format(oid=self.oid, root="{root}")

        self.headers = {"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36"}
        self.cookies = cookies

        self.q = queue.Queue()        # 用来存放爬取的数据,通过队列可以按顺序,使用多线程存入数据库或csv文件中
                                      # 这里我们用到了队列,好处在于,可以用多线程边爬边存,按顺序先进先出
        self.count = 1        # count变量指定主楼数,区别是评论还是评论的评论

        

    # 获取视频 oid
    def oid_get(self, BV):
        response = requests.get(url=self.homeUrl + BV).text
        # 正则表达式 通过视频 bv 号获取 oid
        self.oid = re.findall("\"aid\":([0-9]*),", response)[0]

        请求函数需要传递url,page(最大页面数)

        通过requests库请求数据,需要的数据都在data->replies里面

​​

    def content_get(self, url, page):
        now = 0    # 当前页面
        while now<=page:
            print("page : <{now}>/<{page}>".format(now=now, page=page))
            response = requests.get(url=url+str(now), cookies=self.cookies, headers=self.headers, timeout=10).json()        # 把response解析为json格式,通过字典获取
            replies = response['data']['replies']     # 评论数据在data->replies 里面,每页有 20 条
            now += 1
            for reply in replies:                    # 遍历获取每一条,用reply_clean函数提取数据
                line = self.reply_clean(reply)
                self.count += 1

        我们预先定义一个reply_clean函数,用来提取数据

         2.数据清洗

        reply里面的数据很多,但大多数对我们来说都是没有用的。

    def reply_clean(self, reply):
        name = reply['member']['uname']     # 名字
        sex = reply['member']['sex']        # 性别
        if sex=="保密":
            sex = ' '
        mid = reply['member']['mid']        # 帐号的uid
        sign = reply['member']['sign']      # 标签
        rpid = reply['rpid']        # 爬二级评论要用到
        rcount = reply['rcount']    # 回复数
        level = reply['member']['level_info']['current_level']      # 等级
        like = reply['like']        # 点赞数
        content = reply['content']['message'].replace("\n","")      # 评论内容
        t = reply['ctime']
        timeArray = time.localtime(t)
        otherStyleTime = time.strftime("%Y-%m-%d %H:%M:%S", timeArray)      # 评论时间,时间戳转为标准时间格式
        return [count, name, sex, level, mid, sign, otherStyleTime, content, like, rcount, rpid]

 评论时间为时间戳,用time库把时间戳转换为给人看的时间格式。

 到此处,我们已经获取到了一级评论,并把数据返回为列表类型

        3.数据获取 # 二级评论

为了增加代码的复用性,我们可以改变一下上面两个函数

    # level_1判断是否为一级评论。如果为二级评论,则不请求下一级评论(评论的评论)
    def content_get(self, url, page, level_1=True):
        now = 0
        while now<=page:
            print("page : <{now}>/<{page}>".format(now=now, page=page))
            response = requests.get(url=url+str(now), cookies=self.cookies, headers=self.headers, timeout=10).json()
            replies = response['data']['replies']     # 评论数据在data->replies 里面,一共有 20 条
            now += 1
            for reply in replies:
                if level_1:
                    line = self.reply_clean(reply, self.count)
                    self.count += 1
                else:
                    line = self.reply_clean(reply)
                self.q.put(line)
                # 这儿我们可以筛选一下,如果一级评论有跟评,调用函数请求二级评论
                if level_1==True and line[-2] != 0:
                        # root表示rpid。在二级评论api中是root参数。page页数,由于我们把最大显示量设置为20,所以除以20。加0.5向上取整
                        self.content_get(url=self.rreplyUrl.format(root=str(line[-1])), page=int(line[-2]/20+0.5), level_1=False)       # 递归获取二级评论

        

 我们通过传递给content_get的url参数,可以分别请求一级或二级评论。 而两种评论的json格式是完全一样的。都在data->replies中。self.count参数表示主楼数

        4.数据清洗 # 二级评论
    # 这个函数可以爬一级评论也能爬二级评论
    # count 参数,看看是不是二级评论。
    def reply_clean(self, reply, count=False):
        name = reply['member']['uname']     # 名字
        sex = reply['member']['sex']        # 性别
        if sex=="保密":
            sex = ' '
        mid = reply['member']['mid']        # 帐号的uid
        sign = reply['member']['sign']      # 标签
        rpid = reply['rpid']        # 爬二级评论要用到
        rcount = reply['rcount']    # 回复数
        level = reply['member']['level_info']['current_level']      # 等级
        like = reply['like']        # 点赞数
        content = reply['content']['message'].replace("\n","")      # 评论内容
        t = reply['ctime']
        timeArray = time.localtime(t)
        otherStyleTime = time.strftime("%Y-%m-%d %H:%M:%S", timeArray)      # 评论时间,时间戳转为标准时间格式
        # 如果是二级评论,则返回数据第一个为"回复",否则为楼号
        # 二级评论没有回复数rcount,三级评论都显示为 回复xxx @谁谁谁
        if count:
            return [count, name, sex, level, mid, sign, otherStyleTime, content, like, rcount, rpid]
        else:
            return ["回复", name, sex, level, mid, sign, otherStyleTime, content, like, ' ', rpid]

三、数据存储

数据存储选取了两种方式作为安全:csv和mysql数据库

1、csv:

        用csv模块,while True重复读取队列元素。如果10秒钟还没有数据进来,说明爬完了,或者程序死了。不管是反爬死了还是咋死了,反正它死了

import csv
    
    def csv_writeIn(self, BV):
        file = open("bilibili评论_"+BV+".csv", "w", encoding="utf-8", newline="")
        f = csv.writer(file)
        line1 = ['楼层', '姓名', '性别', '等级', 'uid', '个性签名', '评论时间', '评论内容', '点赞数', '回复数', 'rpid']
        f.writerow(line1)
        file.flush()
        
        while True:
            try:
                line = self.q.get(timeout=10)
            except:
                break
            f.writerow(line)
            file.flush()
            
        file.close()

执行一下,打开csv文件

 我们发现,csv乱码。这是因为,csv默认打开编码方式为ANSI,而我们是用utf-8存储的

这时候我们可以选择用记事本打开,另存为ANSI编码,再用csv打开

   ​

2、mysql
import pymysql as pysql

    def mysql_connect(self, host, user, password, BV):
        # 连接数据库,如果失败退出程序
        try:
            self.conn = pysql.connect(host=host, user=user, password=password)
            self.cursor = self.conn.cursor()
        except:
            print("mysql connect error ... ")
            exit(1)
        
        # 创建库,创建表
        self.cursor.execute('create database if not exists bilibili')
        self.cursor.execute('use bilibili')
        sql = '''
                    create table if not exists {BV} (
                    floor char(6),
                    name char(20),
                    sex char(2),
                    level char(1),
                    uid char(10),
                    sign char(100),
                    time char(23),
                    content char(100),
                    star char(6),
                    reply char(6),
                    rpid char(10))
              '''
        self.cursor.execute(sql.format(BV=BV))        # 用视频BV号创建表

    def mysql_writeIn(self, BV):
        sql = '''insert into BV
                (`floor`, `name`, `sex`, `level`, `uid`, `sign`, `time`, `content`, `star`, `reply`, `rpid`) value
                ("{floor}", "{name}", "{sex}", "{level}", "{uid}", "{sign}", "{t}", "{content}", "{star}", "{reply}", "{rpid}")'''
        sql = sql.replace('BV', BV)
        # 用另一个线程写入表,设置timeout失败退出。
        while True:
            try:
                line = self.q.get(timeout=10)
            except:
                self.conn.close()
                break
            # 由于数据长度是固定长度,有可能出现太长了写不进去的情况。这里根据实际需要,创建表的时候注意一个。实在太长就不要了吧哈哈哈哈
            try:
                self.cursor.execute(sql.format(floor=line[0], name=line[1], sex=line[2], level=line[3], \
                                           uid=line[4], sign=line[5], t=line[6], content=line[7], \
                                           star=line[8], reply=line[9], rpid=line[10]))
            except Exception as e:
                print(e)
                continue
            # 记得提交,不然白存
            self.conn.commit()
四、多线程

        我们需要通过函数调用上面几个部分。多线程这里我们使用threading模块的Thread类构造

         各模块由main函数调用。

from threading import Thread

        def main(self, page, BV):
        self.mysql_connect(host='localhost', user='root', password='SpiderXbest', BV=BV)

        T = []
        T.append(Thread(target=self.content_get, args=(self.replyUrl, page)))
        T.append(Thread(target=self.mysql_writeIn, args=(BV, )))
        # T.append(Thread(target=self.csv_writeIn, args=(BV, )))
        # 二选一,要么csv要么mysql
        
        print("开始爬取...")
        for t in T:
            t.start()
        for t in T:
            t.join()
五、综合。
if __name__ == '__main__':
    cookie = "fingerprint=cdc14f481fb201fec2035d743ff230b; buvid_fp=DE7C7303-E24E-462C-B112-EE78EB55C45B148824infoc; buvid_fp_plain=1BC352F4-4DB9-D82C-44A2-FB17273D240infoc; b_ut=-1; i-wann-go-back=-1; _uuid=43C8466C-79D5-F07A-032C-F6EF1635706854601infoc; buvid3=DE703-E24E-462C-B112-EE78EB55C45B148824infoc; CURRENT_FNVAL=80; blackside_state=1; sid=7wo01l; rpdid=|(u)mmY|~YJ|0J'uYJklJ~ul|; CURRENT_QUALITY=112; PVID=4; bfe_id=cade759d3229a3973a5d4e9161f3bc; innersign=1"
    cookies = {}
    for c in cookie.split(";"):
        b = c.split("=")
        cookies[b[0]] = b[1]

    BV = 'BV14h411n7ok'

    bilibili = Bilibili(BV, 0, cookies, 1)
    bilibili.main(1, BV)

 在传递cookie的时候,最好把它变成字典格式。

 运行一下程序,爬一页试试

 ​

 ok,成功了我们只爬取一页,拨心回复只有最多20条。具体一个视频有多少页呢?我们在浏览器中请求一级评论url看看

 ​

 冰冰姐这个视频有一万多一点评论,到370页的时候就不动了。所以我们可以手动二分法看看具体有多少页,或者写一个判断函数,当请求函数content_get的replies为null的时候,说明结束了。

六、完整代码
# -- coding: utf-8 --
# Author : 竹一
# Time : 2021/9/25 10:37
# version : 1.0
# Software: PyCharm

import requests
import re
import time
import queue
import csv
import pymysql as pysql
from threading import Thread


class Bilibili:
    def __init__(self, BV, mode, cookies, page):
        self.homeUrl = "https://www.bilibili.com/video/"
        self.oid_get(BV)

        self.replyUrl = "https://api.bilibili.com/x/v2/reply/main?jsonp=jsonp&type=1&oid={oid}&mode={mode}&plat=1&next=".format(
            oid=self.oid, mode=mode)
        self.rreplyUrl = "https://api.bilibili.com/x/v2/reply/reply?jsonp=jsonp&type=1&oid={oid}&ps=20&root={root}&pn=".format(
            oid=self.oid, root="{root}")

        self.headers = {
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36"}
        self.cookies = cookies

        self.q = queue.Queue()
        self.count = 1

    # 获取视频 oid
    def oid_get(self, BV):
        response = requests.get(url=self.homeUrl + BV).text
        # 正则表达式 通过视频 bv 号获取 oid
        self.oid = re.findall("\"aid\":([0-9]*),", response)[0]

    # level_1是否为一级评论
    def content_get(self, url, page, level_1=True):
        now = 0
        while now <= page:
            if level_1:
                print("page : <{now}>/<{page}>".format(now=now, page=page))
            response = requests.get(url=url + str(now), cookies=self.cookies, headers=self.headers).json()
            print(url+str(now))
            replies = response['data']['replies']  # 评论数据在data->replies 里面,一共有 20 条
            now += 1
            for reply in replies:
                if level_1:
                    line = self.reply_clean(reply, self.count)
                    self.count += 1
                else:
                    line = self.reply_clean(reply)
                self.q.put(line)
                # 这儿我们可以筛选一下,如果有二级评论,调用函数请求二级评论
                # if level_1 == True and line[-2] != 0:
                #     self.content_get(url=self.rreplyUrl.format(root=str(line[-1])), page=int(line[-2] / 20 + 0.5),
                #                      level_1=False)  # 递归获取二级评论

  

    # 这个函数可以爬一级评论也能爬二级评论
    def reply_clean(self, reply, count=False):
        name = reply['member']['uname']  # 名字
        sex = reply['member']['sex']  # 性别
        if sex == "保密":
            sex = ' '
        mid = reply['member']['mid']  # 帐号的uid
        sign = reply['member']['sign']  # 标签
        rpid = reply['rpid']  # 爬二级评论要用到
        rcount = reply['rcount']  # 回复数
        level = reply['member']['level_info']['current_level']  # 等级
        like = reply['like']  # 点赞数
        content = reply['content']['message'].replace("\n", "")  # 评论内容
        t = reply['ctime']
        timeArray = time.localtime(t)
        otherStyleTime = time.strftime("%Y-%m-%d %H:%M:%S", timeArray)  # 评论时间,时间戳转为标准时间格式
        if count:
            return [count, name, sex, level, mid, sign, otherStyleTime, content, like, rcount, rpid]
        else:
            return ["回复", name, sex, level, mid, sign, otherStyleTime, content, like, ' ', rpid]

    def csv_writeIn(self, BV):
        print("csv文件 数据存储中......")
        file = open("bilibili评论_" + BV + ".csv", "w", encoding="utf-8", newline="")
        f = csv.writer(file)
        line1 = ['楼层', '姓名', '性别', '等级', 'uid', '个性签名', '评论时间', '评论内容', '点赞数', '回复数', 'rpid']
        f.writerow(line1)
        file.flush()

        while True:
            try:
                line = self.q.get(timeout=10)
            except:
                break
            f.writerow(line)
            file.flush()

        file.close()

    def mysql_connect(self, host, user, password, BV):
        try:
            self.conn = pysql.connect(host=host, user=user, password=password)
            self.cursor = self.conn.cursor()
            print("mysql 数据库 连接成功!")
        except:
            print("mysql connect error ... ")
            exit(1)

        self.cursor.execute('create database if not exists bilibili')
        self.cursor.execute('use bilibili')
        sql = '''
                    create table if not exists {BV} (
                    floor char(5),
                    name char(20),
                    sex char(2),
                    level char(1),
                    uid char(10),
                    sign char(100),
                    time char(23),
                    content text,
                    star char(6),
                    reply char(6),
                    rpid char(30))
              '''
        self.cursor.execute(sql.format(BV=BV))

    def mysql_writeIn(self, BV):
        print("mysql 数据存储中 ...")
        sql = '''insert into BV
                (`floor`, `name`, `sex`, `level`, `uid`, `sign`, `time`, `content`, `star`, `reply`, `rpid`) value
                ("{floor}", "{name}", "{sex}", "{level}", "{uid}", "{sign}", "{t}", "{content}", "{star}", "{reply}", "{rpid}")'''
        sql = sql.replace('BV', BV)
        while True:
            try:
                line = self.q.get(timeout=10)
            except:
                self.conn.close()
                break
            try:
                self.cursor.execute(sql.format(floor=line[0], name=line[1], sex=line[2], level=line[3], \
                                               uid=line[4], sign=line[5], t=line[6], content=line[7], \
                                               star=line[8], reply=line[9], rpid=line[10]))
            except Exception as e:
                print(e)
                continue
            self.conn.commit()

    def main(self, page, BV, host, user, password):
        self.mysql_connect(host=host, user=user, password=password, BV=BV)

        T = []
        T.append(Thread(target=self.content_get, args=(self.replyUrl, page)))
        T.append(Thread(target=self.mysql_writeIn, args=(BV,)))
        # T.append(Thread(target=self.csv_writeIn, args=(BV, )))

        print("开始爬取...")
        for t in T:
            t.start()
        for t in T:
            t.join()


if __name__ == '__main__':
    cookie = "_uuid=657210-8EC2-4CC4-9CA7-C1C6B804DF05167640infoc; buvid_fp_plain=F1B301A9-8EC2-4CC4-9CA7-C1C6B804DF05167640infoc; DedeUserID=517144578; DedeUserID__ckMd5=197787e1a943196a; SESSDATA=99fdf911%2C1652767015%2C4bf6d*b1; bili_jct=971a6c010e4f38fc0a92852550a17b; blackside_state=1; rpdid=|(J|~YRu)|JY0J'uYJ~kul|Ru;697ed924ddf787880; bp_video_offset_517144578=612902411653386000; CURRENT_FNVAL=80; CURRENT_BLACKGAP=1; innersign=0; b_lsid=9A129ACD_17E3A5E4588"

    cookies = {}
    for c in cookie.split(";"):
        b = c.split("=")
        cookies[b[0]] = b[1]

    BV = 'BV14h411n7ok'

    host = "localhost"  # 主机名
    user = "root"  # 用户名
    password = "xxxxxx"  # 密码

    bilibili = Bilibili(BV, 0, cookies, 369)

    bilibili.main(369, BV, host, user, password)

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/798783.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-06
下一篇 2022-05-06

发表评论

登录后才能评论

评论列表(0条)

保存