这次我们要在scrapy框架下重构我们上次写的排行榜小说爬虫(https://zhuanlan.zhihu.com/p/26756909) 并将爬取的结果存储到mysql数据库中。另外,这是爬虫专栏第二部分:Scrapy框架 的最后一篇文章啦~
目标分析:
我们的目标十分明确:
由于上次自己写的bs4小说爬虫效率堪忧,
我又不肯自己写多线程(其实是不会!逃)
所以我们来利用Scrapy强大的并发功能吧!
但是,用到并发其实会有个坑,下文会着重说明。
那么我们只要:
找到小说每一章的链接地址,
将每章小说的标题、正文部分存入数据库。
数据筛选:
由于是代码的重构,其实在上次的文章中我们就已经把整个爬虫如何运作的逻辑完成了,这次只需要用Scrapy框架的方法重写一遍就行。另外,也会抛弃bs4库,投降Xpath的怀抱。
好来,我们来看具体怎么写吧。
遇到的麻烦:
说起来,一开始我是不肯用数据库的,
我觉得直接把小说爬下来写入文本不就结了吗?
然而,理想很丰满 现实很骨干!
写入的文本是这样的:
发现了没有? 小说的顺序是不固定的,序章之后居然就是65章了。
我去查了一下原因:scrapy异步处理Request请求,Scrapy发送请求之后,不会等待这个请求的响应,他会同时发送其他请求或者做别的事情。
整数因为这个特性,Scrapy才能有这么快的速度,他在cpu等待IO操作的时间,发起了一个新的线程。
如何解决?
遇到关于顺序这个蛋疼的问题,我想了很多办法:
比如设置request的priority(优先级),让每次request按照优先级排队发出,然而这样并不能改变写进文本的顺序。
在思考一番之后,我决定通过将数据写入mysql数据库,来解决排序的问题:
当然,还是由于Scrapy的并发系统,就算是写入数据库,也不能按顺序入库,
结果是这样的:
我拍脑袋一想,为什么要按顺序入库呢,
我在查询数据的时候,给他按章节名来排序不就结了?
我真机智,快为我点个赞!
然而,如果按照章节名排序,出来的结果是:
是的 mysql是瑞典人开发的,
默认是utf8编码,
不支持中文排序也是很正常的!
这里我们是不是陷入了trouble呢?
当然不是,我们给每章小说都定义一个id字段,
最后通过id来给章节排序,不就完了吗?
这里我选择:
将每一章的章节名中的数字,
转换为阿拉伯数字,
再传入id字段~
看代码吧:
注意,需要将这个模块 放在和spider同级目录,方便一会我们写spider的时候导入
'''实现了中文向阿拉伯数字转换用于从小说章节名提取id来排序'''
chs_arabic_map = {'零': 0, '一': 1, '二': 2, '三': 3, '四': 4,
'五': 5, '六': 6, '七': 7, '八': 8, '九': 9,
'十': 10, '百': 100, '千': 10 ** 3, '万': 10 ** 4,
'〇': 0, '壹': 1, '贰': 2, '叁': 3, '肆': 4,
'伍': 5, '陆': 6, '柒': 7, '捌': 8, '玖': 9,
'拾': 10, '佰': 100, '仟': 10 ** 3, '萬': 10 ** 4,
'亿': 10 ** 8, '億': 10 ** 8, '幺': 1,
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5,
'7': 7, '8': 8, '9': 9}
num_list = ['1','2','4','5','6','7','8','9','0','一','二','三','四','五','六','七','八','九','十','零','千','百',]
def get_tit_num(title):
result =''
for char in title:
if char in num_list:
result+=char
return result
def Cn2An(chinese_digits):
result = 0
tmp = 0
hnd_mln = 0
for count in range(len(chinese_digits)):
curr_char = chinese_digits[count]
curr_digit = chs_arabic_map[curr_char]
# meet 「亿」 or 「億」
if curr_digit == 10 ** 8:
result = result + tmp
result = result * curr_digit
# get result before 「亿」 and store it into hnd_mln
# reset `result`
hnd_mln = hnd_mln * 10 ** 8 + result
result = 0
tmp = 0
# meet 「万」 or 「萬」
elif curr_digit == 10 ** 4:
result = result + tmp
result = result * curr_digit
tmp = 0
# meet 「十」, 「百」, 「千」 or their traditional version
elif curr_digit >= 10:
tmp = 1 if tmp == 0 else tmp
result = result + curr_digit * tmp
tmp = 0
# meet single digit
elif curr_digit is not None:
tmp = tmp * 10 + curr_digit
else:
return result
result = result + tmp
result = result + hnd_mln
return result
# test
print (Cn2An(get_tit_num('第一千三百九十一章 你妹妹被我咬了!')))
解决了用于排序的id的问题,我们就可以开始写代码了
项目的创建:
# 创建项目
scrapy startproject biquge
# 进入文件夹
cd biquge
# 生成爬虫文件
scrapy genspider xsphspider
# 看一下目录树:
.
├── biquge
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-36.pyc
│ │ ├── items.cpython-36.pyc
│ │ ├── pipelines.cpython-36.pyc
│ │ └── settings.cpython-36.pyc
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-36.pyc
│ │ ├── sjzh.cpython-36.pyc
│ │ └── xsphspider.cpython-36.pyc
│ ├── sjzh.py
│ └── xsphspider.py
└── scrapy.cfg
编写Items:
还是和原来一样,我们先定义好没一个爬取的item(小说章节)有哪些字段是我们需要的:
import scrapy
class BiqugeItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 小说名字
bookname = scrapy.Field()
#章节名
title = scrapy.Field()
#正文
body = scrapy.Field()
#排序用id
order_id = scrapy.Field()
编写Spider:
由于我们的spider爬取顺序是这样的:
首先: 爬取排行榜页面,找到每一本小说的页面
接着: 爬取小说页面, 找到小说每一章的链接
最后: 爬取每一章节页面,找到文章标题和正文内容
我们再来复习一下 spider是怎么运作的:
首先: 从start_urls里发起请求,返回response
接着: 自动调用 parse函数
中间: 一系列我们自己添加的功能
最后: 返回item,给PIPELINE处理
为了实现我们定好的spider逻辑,我们得调用Scrapy内置的requests函数,
来介绍一下Scrapy.request函数:
class Request(url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, dont_filter=False, errback=None)
# 这里其实和我们一直用的request模块也差不多,最主要需要注意的参数:
# callback 这个参数的意思是回调函数,就是会自动运行的函数,并将request获得的response自动传进去。
来看一下具体的代码:
比起之前的爬虫,稍微长一点,仔细看能看懂的,
都有详细的注释。
# -*- coding: utf-8 -*-
import scrapy
from biquge.items import BiqugeItem
# 导入我们自己写的函数
from .sjzh import Cn2An,get_tit_num
class XsphspiderSpider(scrapy.Spider):
name = "xsphspider"
allowed_domains = ["qu.la"]
start_urls = ['http://www.qu.la/paihangbang/']
novel_list = []
def parse(self, response):
# 找到各类小说排行榜名单
books = response.xpath('.//div[@class="index_toplist mright mbottom"]')
# 找到每一类小说排行榜的每一本小说的下载链接
for book in books:
links = book.xpath('.//div[2]/div[2]/ul/li')
for link in links:
url = 'http://www.qu.la' + \
link.xpath('.//a/@href').extract()[0]
self.novel_list.append(url)
# 简单的去重
self.novel_list = list(set(self.novel_list))
for novel in self.novel_list:
yield scrapy.Request(novel, callback=self.get_page_url)
def get_page_url(self, response):
'''找到章节链接'''
page_urls = response.xpath('.//dd/a/@href').extract()
for url in page_urls:
yield scrapy.Request('http://www.qu.la' + url,callback=self.get_text)
def get_text(self, response):
'''找到每一章小说的标题和正文并自动生成id字段,用于表的排序'''
item = BiqugeItem()
# 小说名
item['bookname'] = response.xpath(
'.//div[@class="con_top"]/a[2]/text()').extract()[0]
# 章节名 ,将title单独找出来,为了提取章节中的数字
title = response.xpath('.//h1/text()').extract()[0]
item['title'] = title
# 找到用于排序的id值
item['order_id'] = Cn2An(get_tit_num(title))
# 正文部分需要特殊处理
body = response.xpath('.//div[@id="content"]/text()').extract()
# 将抓到的body转换成字符串,接着去掉\t之类的排版符号,
text = ''.join(body).strip().replace('\u3000', '')
item['body'] = text
return item
编写PIPELINE:
mysql数据库:
由于这里我们需要将数据写入Mysql数据库,这里需要自己有一点mysql基本操作的知识:
如果对于数据库一点都不懂,这里有一本比较好的教程,跟着做一遍,大体就都明白了。
具体代码:
import pymysql
class BiqugePipeline(object):
def process_item(self, item, spider):
'''将爬到的小数写入数据库'''
# 首先从items里取出数据
name = item['bookname']
order_id = item['order_id']
body = item['body']
title = item['title']
# 与本地数据库建立联系
# 和本地的scrapyDB数据库建立连接
connection = pymysql.connect(
host='localhost', # 连接的是本地数据库
user='root', # 自己的mysql用户名
passwd='********', # 自己的密码
db='bqgxiaoshuo', # 数据库的名字
charset='utf8mb4', # 默认的编码方式:
cursorclass=pymysql.cursors.DictCursor)
try:
with connection.cursor() as cursor:
# 数据库表的sql
sql1 = 'Create Table If Not Exists%s(id int,zjm varchar(20),body text)' % name
# 单章小说的写入
sql = 'Insert into%svalues (%d,\'%s\',\'%s\')' % (
name, order_id, title, body)
cursor.execute(sql1)
cursor.execute(sql)
# 提交本次插入的记录
connection.commit()
finally:
# 关闭连接
connection.close()
return item
配置settings:
将我们写的PIPELINE加入settings:
ITEM_PIPELINES = {
'biquge.pipelines.BiqugePipeline': 300,
}
中断后如何恢复任务?
由于这次我们需要爬得数据量非常的大,
就算有强大的多线程也不是一时半会就能爬完的,
所以这里我们得知道如果爬虫爬到一半断了,我们如何从断的地方接着工作,
而不是从头开始
Job 路径
要启用持久化支持,你只需要通过 JOBDIR 设置 job directory 选项。这个路径将会存储 所有的请求数据来保持一个单独任务的状态(例如:一次spider爬取(a spider run))。必须要注意的是,这个目录不允许被不同的spider 共享,甚至是同一个spider的不同jobs/runs也不行。也就是说,这个目录就是存储一个 单独 job的状态信息。
如何使用?
要启用一个爬虫的持久化,运行以下命令:
scrapy crawl somespider -s JOBDIR=crawls/somespider-1
然后,你就能在任何时候安全地停止爬虫(按Ctrl-C或者发送一个信号)。
恢复这个爬虫也是同样的命令:
scrapy crawl somespider -s JOBDIR=crawls/somespider-1
结果展示:
由于没有爬太长时间,我就关闭掉了,就爬了一点点:
# 登录mysql数据库
mysql -uroot -p
# 选中小说数据库
use bqgxiaoshuo;
# 查看爬到的小说L
show tables;
看一下小说章节的排序:
select zjm from "小说名" order by id;
可以看到已经基本完成排序的工作了。
当然,这个爬虫只是初步实现了基本功能,
实际上还有很多bug和需要优化的地方,
这些大家可以自己在源代码的基础上添加功能啦,
如果有人能基于这个真的做出一个小说阅读app那就更好了!
到这里,我们的Scrapy爬虫的学习记录就要告一段落了,
从下一篇文章开始,我将会介绍模拟浏览器爬虫~
每天的学习记录都会 同步更新到:
微信公众号: findyourownway
知乎专栏:从零开始写Python爬虫 - 知乎专栏
blog : www.ehcoblog.ml
Github: Ehco1996/Python-crawler