爬虫系列文章:

今天来介绍一下 Python 的一个爬虫框架Scrapy ,类似的还有 Portia Crawley

本项目源码地址:https://github.com/wsuo/pythonHomework/tree/master/demo

一、Scrapy 框架准备

首先是安装爬虫这个框架:

pip install scrapy

官方文档:https://docs.scrapy.org/en/latest/

如果是 Windows 下安装的,还需要安装一下这个:

pip install pypiwin32

如果是 Ubuntu 上安装,需要提前安装这个:

sudo apt-get install python3-dev build-essential python3-pip libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev

二、快速启动项目

以爬取糗事百科网站为例:https://www.qiushibaike.com/

可以分为以下四步:

scrapy startproject demo
cd demo
scrapy genspider qsbk qiushibaike.com
scrapy crawl qsbk

1、创建项目结构

创建一个名为 demo 的项目,该命令仅仅是创建项目结构,你完全可以自己创建,只不过自动生成方便一些:

scrapy startproject demo

使用 PyCharm 打开一个空的文件夹,然后进入终端输入命令即可:

项目结构如图:

  1. items.py:用来存放爬虫爬取下来数据的模型
  2. middlewares.py:用来存放各种中间件的文件。
  3. pipelines.py:用来将items的模型存储到本地磁盘中。
  4. settings.py:本爬虫的一些配置信息(比如请求头、多久发送一次请求、ip***池等)。
  5. scrapy.cfg:项目的配置文件
  6. spiders包:以后所有的爬虫,都是存放到这个里面。

2、创建爬虫

先执行进入到工作目录:

cd demo

然后执行命令创建爬虫:

scrapy genspider qsbk qiushibaike.com

然后就会自动生成一个文件qsbk.py

3、更改设置

首先我们要更改一下设置项,为了能够爬到我们想要的数据,我们这里 顶风作案 一下:

修改 ROBOTSTXT_OBEY = False

然后加上请求头的 User-Agent,伪装为浏览器访问:

DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.116 Safari/537.36',
}

4、爬虫类分析

下面我们来看一下爬虫类:

name = 'qsbk'  # 运行的时候输入这个名字,比如
allowed_domains = ['qiushibaike.com']  # 允许的域名
start_urls = ['http://qiushibaike.com/']  # 开始的 url

我们先来试一下:

class QsbkSpider(scrapy.Spider):
    name = 'qsbk'  # 运行的时候输入这个名字
    allowed_domains = ['qiushibaike.com']  # 允许的域名
    start_urls = ['https://www.qiushibaike.com/text/page/1/']  # 开始的 url

    def parse(self, response):
        print('-'*40)
        print(response)
        print(type(response))
        print('-'*40)

运行一下:scrapy crawl qsbk 就可以看到结果了。

<200 https://www.qiushibaike.com/text/page/1/>
<class 'scrapy.http.response.html.HtmlResponse'>

类型为 HtmlResponse

追踪这个类的父类:

可以看到有 xpath 的方法,说明我们可以使用 xpath 解析响应信息。

编写如下代码:

def parse(self, response):
        content = response.xpath('//div[@class="content"]')
        print('-'*40)
        print(response)
        print(type(content))
        print(type(response))
        print('-'*40)

输出:

追踪类SelectorList,找到selector类:

观察该类的方法。

比较重要的方法就是 extract方法可以将 SelectorList类型的转化为列表类型,并且里面装的是字符串,extract_first方法是获取第一个元素。

5、编写启动脚本

由于每次都要输入命令启动挺麻烦的,所以我们可以使用脚本文件执行命令行的命令。

创建一个start.py文件,随便在哪里创建都可以;

内容如下:

from scrapy import cmdline

cmdline.execute(['scrapy', 'crawl', 'qsbk', '--nolog'])

三、爬虫实战

我们先初步的探索,然后慢慢的优化。

1、初步探索

继续上一步的操作,我们在爬虫类QsbkSpider中继续写逻辑。

先来获取一下作者的名称小试牛刀一下,编写代码:

class QsbkSpider(scrapy.Spider):
    name = 'qsbk'  # 运行的时候输入这个名字
    allowed_domains = ['qiushibaike.com']  # 允许的域名
    start_urls = ['https://www.qiushibaike.com/text/page/1/']  # 开始的 url

    def parse(self, response):
        divs = response.xpath('//div[@class="col1 old-style-col1"]/div')
        for div in divs:
            author = div.xpath('./div[@class="author clearfix"]//h2/text()').get().strip()
            print(author)

查看输出结果:

说明这是可以的,下面我们继续爬取后面的内容,比如爬取段子的内容。

content = ''.join(div.xpath('.//div[@class="content"]/span[1]/text()').getall()).strip()

yield 的作用是将函数作为一个生成器返回,以后遍历的时候就会把数据一个一个的拿过去

def parse(self, response):
        divs = response.xpath('//div[@class="col1 old-style-col1"]/div')
        for div in divs:
            author = div.xpath('.//h2/text()').get().strip()
            content = ''.join(div.xpath('.//div[@class="content"]/span[1]/text()').getall()).strip()
            # yield 的作用是将函数作为一个生成器,以后遍历的时候就会把数据一个一个的拿过去
            yield {'昵称': author, '段子内容': content}

然后我们可以存储到本地,这就是需要到管道中执行了,所以打开DemoPipeline类:

class DemoPipeline(object):
    def process_item(self, item, spider):
        return item
    
    def open_spider(self, spider):
        print('爬虫开始')
    
    def close_spider(self, spider):
        print('爬虫结束')

后面两个方法默认是没有的,但是如果我们有文件操作最好可以放在这里面。

但是在使用之前,我们必须要先到配置文件中打开管道:

后面这个值越小越先执行。

编写代码:

import json

class DemoPipeline(object):
    def process_item(self, item, spider):
        json.dump(item, self.file, ensure_ascii=False, indent=4)
        self.file.write('\n')
        return item

    def open_spider(self, spider):
        self.file = open('dz.json', 'a', encoding='utf-8')
        print('爬虫开始')

    def close_spider(self, spider):
        self.file.close()
        print('爬虫结束')

执行结果:

2、优化数据模型

我们之前使用的是自己造的一个字典在爬虫类和管道之间传输,但是更好的做法是使用数据模型,下面我们来实现一下。

定义一个类DemoItem

class DemoItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    author = scrapy.Field()
    content = scrapy.Field()

然后我们在爬虫类中引入这个模型:

import scrapy
# noinspection PyUnresolvedReferences
from demo.items import DemoItem


class QsbkSpider(scrapy.Spider):
    name = 'qsbk'  # 运行的时候输入这个名字
    allowed_domains = ['qiushibaike.com']  # 允许的域名
    start_urls = ['https://www.qiushibaike.com/text/page/1/']  # 开始的 url

    def parse(self, response):
        divs = response.xpath('//div[@class="col1 old-style-col1"]/div')
        for div in divs:
            author = div.xpath('.//h2/text()').get().strip()
            content = ''.join(div.xpath('.//div[@class="content"]/span[1]/text()').getall()).strip()
            # yield 的作用是将函数作为一个生成器,以后遍历的时候就会把数据一个一个的拿过去
            yield DemoItem(author=author, content=content)

好处就是:解耦,约定数据种类,更规范。

但是在管道中获取的时候会有问题,因为他获取的是一个 DemoItem 类型的参数,我们要把它转化为字典类型。

json.dump(dict(item), self.file, ensure_ascii=False, indent=4)

3、优化数据存储方式

优化存储方式肯定要修改管道中的代码,所以我们先注释掉之前写的代码。

使用 scrapy 自带的 JSON 持久化方式。

from scrapy.exporters import JsonItemExporter


class DemoPipeline(object):
    def __init__(self):
        self.file = open('dz.json', 'wb')
        self.exporter = JsonItemExporter(self.file, ensure_ascii=False, encoding='utf-8')

    def open_spider(self, spider):
        print('爬虫开始了')
        # 使用二进制打开
        self.exporter.start_exporting()

    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item

    def close_spider(self, spider):
        self.exporter.finish_exporting()
        self.file.close()
        print('爬虫结束了')

注意这里必须是文件打开的方式必须是 wb 而且不能指定编码格式。

这样执行的结果就是装在一个列表当中了。

它的执行流程其实就是先将数据塞到一个列表当中,然后调用finish_exporting()写入文件中。

所以他有一个缺陷就是如果 item 比较大的话,他的列表也比较大,一直在内存中就会比较耗内存。

所以我们可以使用JsonLinesItemExporter这个类,他就是不缓存到一个列表当中,直接一行一行的写入文件。

代码如下:

class DemoPipeline(object):
    def __init__(self):
        self.file = open('dz.json', 'wb')
        self.exporter = JsonLinesItemExporter(self.file, ensure_ascii=False, encoding='utf-8')

    def open_spider(self, spider):
        print('爬虫开始了')

    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item

    def close_spider(self, spider):
        self.file.close()
        print('爬虫结束了')

结果如下:
还有很多文件格式的保存方法,比如 xmlcsvpickel 等等,这里就不再一一演示了。

4、爬取多个页面

我们之前爬的都是单页面,那么怎么才能爬取多页面呢?

我们再来分析一下页面结构:

可以看到最后一个 li 标签中就是页数,但是有一点要注意的就是到最后一页的时候要有一个判断。

我们使用 xpath 来获取 href 的值:

在爬虫类中编写代码:

class QsbkSpider(scrapy.Spider):
    name = 'qsbk'  # 运行的时候输入这个名字
    allowed_domains = ['qiushibaike.com']  # 允许的域名
    start_urls = ['https://www.qiushibaike.com/text/page/1/']  # 开始的 url
    base_domain = 'https://www.qiushibaike.com'

    def parse(self, response):
        divs = response.xpath('//div[@class="col1 old-style-col1"]/div')
        for div in divs:
            author = div.xpath('.//h2/text()').get().strip()
            content = ''.join(div.xpath('.//div[@class="content"]/span[1]/text()').getall()).strip()
            # yield 的作用是将函数作为一个生成器,以后遍历的时候就会把数据一个一个的拿过去
            yield DemoItem(author=author, content=content)
        next_url = response.xpath('//ul[@class="pagination"]//li[last()]/a/@href').get()
        if not next_url:
            return
        else:
            yield scrapy.Request(self.base_domain + next_url, callback=self.parse)

我们在设置页面设置下载的延时,这样的话可以控制爬的速度,因为太快的话容易被发现,而且还有可能把别人服务器搞垮了:

执行结果:

当然这样不是最好的解决方案,后续文章会讲解 CrawlSpider 类的使用。


参考链接: