从零开始写Python爬虫 --- 2.6 爬虫实践:重构排行榜小说爬虫&Mysql数据库

从零开始写Python爬虫 --- 2.6 爬虫实践:重构排行榜小说爬虫&Mysql数据库

这次我们要在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基本操作的知识:

如果对于数据库一点都不懂,这里有一本比较好的教程,跟着做一遍,大体就都明白了。

mysql 5.7参考手册: MySQL 5.7 参考手册 · GitBook


  • 具体代码:

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 %s values (%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

编辑于 2017-05-21

文章被以下专栏收录