原来写过用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