B站全站视频数据爬虫(scrapy)更新中...

原来写过用requests爬取一个区的爬虫,这段时间研究了下scrapy,写了个爬取全站视频的爬虫,踩了一堆scrapy的坑,正好记录一下,有空慢慢更新吧。
在B站完结动画分区数据爬取那章里介绍了B站的api,其中有一个参数叫做rid,指分区编号。在那篇文章中还介绍了b站小伙伴的b站爬虫项目,里面的wiki页面有记录分区及对应的编号,这次我们就根据这些编号爬取B站全部的视频数据。

前期准备

首先,请确保安装python3.6及以上版本,python库scrapy及pymysql。
然后创建scrapy项目及video爬虫(不会的话请自行学习scrapy基础)
创建好后,使用pycharm打开该项目。(当然,你也可以使用其他任何IDE,文本编辑器等)

需要爬取的信息

https://github.com/uupers/BiliSpider/wiki/

打开B站小伙伴提供的api介绍,选择二级分区视频分页数据。
我们要提取的数据有av号,标题,硬币,播放等。
这些数据可以分为两类:静态数据和动态数据。
比如视频所属分区就是静态的,上传之后就不会再改变了。而播放收藏这些是会随着时间增长的数据则是动态数据。这样分的原因是静态数据会占用大量的空间(因为有很多文本信息),且不会改变,无需重复爬取,而动态数据则会随时间变化,需要重复爬取,(个人需求,主要记录历史数据可以绘制随时间变化的图标)本身又都是数字,占用空间也不大,可以单独提出来。

分析完数据后,就可以写item类了,具体内容如下:

class videoItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    aid = Field()
    # tid = Field()
    # videos = Field()
    # pubdate = Field()
    # mid = Field()
    # copyright = Field()
    # duration = Field()
    # title = Field()
    # pic = Field()
    view = Field()
    danmaku = Field()
    reply = Field()
    favorite = Field()
    coin = Field()
    share = Field()
    like = Field()
    dislike = Field()

注释了的是静态数据,没注释的是动态数据。
接下来就是spider部分。

class VideoSpider(scrapy.Spider):
    name = 'video1'
    allowed_domains = ['api.bilibili.com']
    base_url = 'http://api.bilibili.com/x/web-interface/newlist?'

    def start_requests(self):
        ps = 50
        rids = []
        # 从分区列表文件读取分区号,构造初始连接
        with open('bilibili/分区列表.txt', 'r', encoding='utf8') as f:
            zone = f.readline()
            while zone:
                if zone.find('c1-') != -1:
                    rids.append(int(zone.split(':')[0].split('-')[1]))
                zone = f.readline()

        for rid in rids:
            start_url = self.base_url + urlencode({'rid': rid, 'ps': ps, 'pn': 1})
            yield scrapy.Request(url=start_url, callback=self.parse_json, dont_filter=True)

    def parse_json(self, response):
        try:
            data = json.loads(response.body)
        except Exception:
            # 如果json文件解析失败,重新爬取该页面
            return scrapy.Request(url=response.url, callback=self.parse_json)
        code = int(data['code'])
        if code != 0:
            # code不为0则表示结果异常
            return scrapy.Request(url=response.url, callback=self.parse_json)
        page = data['data']['page']
        archives = data['data']['archives']
        for i in range(len(archives)):
            item = videoItem()
            item['aid'] = archives[i]['aid']
            # 视频静态属性
            # item['tid'] = archives[i]['tid']
            # item['pic'] = archives[i]['pic']
            # item['videos'] = archives[i]['videos']
            # item['copyright'] = archives[i]['copyright']
            # item['pubdate'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(archives[i]['pubdate']))
            # item['duration'] = archives[i]['duration']
            # item['title'] = archives[i]['title']
            # item['mid'] = archives[i]['owner']['mid']
            # 视频动态属性
            item['view'] = archives[i]['stat']['view']
            item['danmaku'] = archives[i]['stat']['danmaku']
            item['reply'] = archives[i]['stat']['reply']
            item['favorite'] = archives[i]['stat']['favorite']
            item['coin'] = archives[i]['stat']['coin']
            item['share'] = archives[i]['stat']['share']
            item['like'] = archives[i]['stat']['like']
            item['dislike'] = archives[i]['stat']['dislike']
            yield item

        # 获取下一页
        count = page['count']   # 当前分区视频总数
        pn = page['num']    # 当前页面
        rid = archives[0]['tid']
        if pn <= count // 50:
            pn += 1
            params = {'rid': rid, 'ps': 50, 'pn': pn}
            next_page = self.base_url + urlencode(params)
            yield scrapy.Request(url=next_page, callback=self.parse_json, dont_filter=True)

首先,通过start_requests函数读取分区列表中的分区号,构造出分区的初始链接。
然后在parse-json函数中解析出每个视频的数据。然后在返回的json文件中有count属性,该值为当前分区的总视频数,我们除以每页50,即得需要爬取的总页数。json文件中还有一个num属性,该属性是当前的页码,加一即得下一页的url。
然后是pipeline:

class MysqlPipeline(object):
    """使用adbapi连接池实现异步多线程mysql插入。
    连接池是创建多条connection,而不是批量插入。"""

    def __init__(self, host, database, user, password, port, dynamic_table):
        self.host = host
        self.database = database
        self.user = user
        self.password = password
        self.port = port
        self.dynamic_table = dynamic_table

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            host=crawler.settings.get('MYSQL_HOST'),
            database=crawler.settings.get('MYSQL_DATABASE'),
            user=crawler.settings.get('MYSQL_USER'),
            password=crawler.settings.get('MYSQL_PASSWORD'),
            port=crawler.settings.get('MYSQL_PORT'),
            dynamic_table=crawler.settings.get('DYNAMIC_TABLE_NAME'),
        )

    def open_spider(self, spider):
        self.dbpool = adbapi.ConnectionPool("pymysql", host=self.host, user=self.user, password=self.password,
                                            database=self.database, charset='utf8', port=self.port)

    def close_spider(self, spider):
        self.dbpool.close()

    def process_item(self, item, spider):
        # self.dbpool.runInteraction(self._insert_table_static, item)
        self.dbpool.runInteraction(self._insert_table_dynamic, item)
        return item

    def _insert_table_static(self, tx, item):
        values = ','.join(['%s'] * 9)
        sql = 'insert into %s values (%s)' % ('video_static', values)
        # 该元组元素顺序与数据库表键顺序对应
        tx.execute(sql, (item['aid'], item['tid'], item['videos'], item['pubdate'],
                         item['mid'], item['copyright'], item['duration'], item['title'], item['pic']))

    def _insert_table_dynamic(self, tx, item):
        values = ','.join(['%s'] * 9)
        sql = 'insert into %s values (%s)' % (self.dynamic_table, values)
        # 该元组元素顺序与数据库表键顺序对应
        try:
            tx.execute(sql, (item['aid'], item['view'], item['danmaku'], item['reply'],
                             item['favorite'], item['coin'], item['share'], item['like'], item['dislike']))
        except IntegrityError:
            pass

这里是构建adbapi连接池,因为scrapy本身是异步的,有时会用到多条mysql连接,这是直接从adbapi中获取即可。

先贴个github地址:https://github.com/HOUTASU/bili_spider

    原文作者:Houtasu
    原文地址: https://www.jianshu.com/p/19ca71c42654
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞