自学Python第十六天-Scrapy框架创建爬虫

自学Python第十六天-Scrapy框架创建爬虫


Scrapy 框架是 python 开发的一个快速,高层次的屏幕抓取和 web 抓取框架,用于抓取 web 站点并从页面中提取结构化的数据。它采用了 Twisted异步网络框架,可以大大的增加下载速度。

Scrapy的用途广泛,可以用于数据挖掘、监测和自动化测试。Scrapy 吸引人的地方在于它是一个框架,任何人都可以根据需求方便的修改。并且它也提供了多种类型爬虫的基类,如 BaseSpider、sitemap 爬虫等。

Scrapy 官方文档
Scrapy 2.5中文文档

运行原理

普通的爬虫工作流程大概是:
在这里插入图片描述
上面的流程可以改写成以下结构:
在这里插入图片描述
Scrapy框架的执行流程:
在这里插入图片描述
流程描述

  1. 爬虫中起始的url构造成request对象–>爬虫中间件–>引擎–>调度器
  2. 调度器把request–>引擎–>下载中间件–>下载器
  3. 下载器发送请求,获取response响应---->下载中间件---->引擎—>爬虫中间件—>爬虫
  4. 爬虫提取url地址,组装成request对象---->爬虫中间件—>引擎—>调度器,重复第二个步骤
  5. 爬虫提取数据—>引擎—>管道处理和保存数据

注意点

  • 图中绿色线条表示数据的传递
  • 注意图中中间件的位置,决定了其作用
  • 注意其中引擎的位置,所有的模块之前相互独立,只和引擎进行交互

上图中的1 - 12序号的解释说明:

  1. ScrapySpider子类中提取start_urls,然后构造为request请求对象
  2. request请求对象传递给爬虫中间件
  3. request请求对象传递给Scrapy引擎(就是核心代码)
  4. request请求对象传递给调度器(它负责对多个request调度,好比交通管理员负责交通的指挥员)
  5. request请求对象传递给Scrapy引擎
  6. Scrapy引擎将request请求对象传递给下载中间件(可以更换代理IP,更换Cookies,更换User-Agent,自动重试。等)
  7. request请求对象传给到下载器(它通过异步的发送HTTP(S)请求),得到响应封装为response对象
  8. response对象传递给下载中间件
  9. 下载中间件将response对象传递给Scrapy引擎
  10. Scrapy引擎将response对象传递给爬虫中间件(这里可以处理异常等情况)
  11. 爬虫对象中的parse函数被调用(在这里可以对得到的response对象进行处理,例如用status得到响应状态码,xpath可以进行提取数据等)
  12. 将提取到的数据传递给Scrapy引擎,它将数据再传递给管道(在管道中我们可以将数据存储到csvMongoDB等)

Scrapy 的主要对象和模块

Scrapy内置的三个对象

  • request请求对象:由urlmethodpost_dataheaders等构成
  • response响应对象:由urlbodystatusheaders等构成
  • item数据对象:本质是个字典

Scrapy 的主要组件

Scrapy 主要由以下几个组件组成:

  • Scrapy Engine (Scrapy 引擎)
    用来处理整个系统的数据流处理,触发事务(框架核心)
  • Scheduler (调度器)
    调度器存放的是需要爬取的页面链接的列表。具体的说是用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。可以想象成一个URL(抓取网页的网址或者说是链接)的优先队列,由它来决定下一个要抓取的网址是什么,同时去除重复的网址
  • Downloader (下载器)
    用于下载网页内容,并将网页内容返回给爬虫(Scrapy 下载器是建立在 twisted 这个高效的异步模型上的)
  • Spiders (爬虫)
    用于从特定的网页中提取自己需要的信息(解析下载器下载的页面数据),即实体(Item)
  • Item (实体)
    爬取数据的信息实体,即实际的信息数据
  • Pipeline (项目管道)
    负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
  • Downloader Middlewares (下载器中间件)
    位于 Scrapy 引擎和下载器之间的框架,主要是处理 Scrapy 引擎与下载器之间的请求及响应
  • Spider Middlewares (爬虫中间件)
    介于引擎和爬虫之间的框架,主要工作是处理爬虫的响应输入和请求输出
  • Scheduler Middlewares (调度中间件)
    介于引擎和调度之间的框架,处理从引擎发送到调度的请求和响应

Scrapy内置模块图解

在这里插入图片描述
注意:爬虫中间件和下载中间件只是运行逻辑的位置不同,作用是重复的:如替换User-Agent等。

安装

Scrapy 是一个框架,由多个模块组成 。

注:网上有说非顺序安装会报错。但是直接安装 scrapy 可以顺带安装需要的支持库。
不过需要注意的是:

  • scrapy 需要的 Twisted 版本限制在 22.10.0版本,而默认安装 scrapytwisted 版本可能会过高,会报错AttributeError:'AsyncioSelectorReactor' object has no attribute '_handleSignals'
  • parse 版本需要限制在1.7.0,否则会报警告:UserWarning: Selector got both text and root, root is being ignored. super().__init__(text=text, type=st, root=root, **kwargs)
  • cryptography版本需要使用36.0.2,否则会报错:twisted.web._newclient.ResponseNeverReceived: [<twisted.python.failure.Failure OpenSSL.SSL.Error: [(‘SSL routines’, ‘’, ‘unsafe legacy renegotiation disabled’)]>

scrapy

Scrapy 主引擎模块,使用 pip install scrapy 进行安装

使用 scrapy 项目

一个 scrapy 爬虫的流程为:

  1. 新建项目(Project): 新建一个新的爬虫项目
  2. 明确目标(Items): 明确想要抓取的目标
  3. 制作爬虫(Spider): 制作爬虫开始爬取网页
  4. 存储内容(Pipeline): 设计管道存储爬取内容

创建项目

当安装完各需要的库后,就可以创建项目了。在 cmd 中,进入需要进入的目录,然后执行命令 scrapy startproject 项目名称

当看到如下提示时,说明项目创建成功,且在目录下建立了一个项目名称命名的文件夹:

scrapy startproject 项目名称

New Scrapy project '项目名称', using template directory 'C:\Program Files\Python310\lib\site-packages\scrapy\templates\project', created in:
    项目目录\项目名称

You can start your first spider with:
    cd 项目名称
    scrapy genspider example example.com

创建好了后,目录结构为:

  • scrapy.cfg : 项目的配置文件
  • 项目名称/ : 项目的 python 模块,将会从这里引用代码
  • 项目名称/items.py :项目的 items 文件,用来存放抓取内容容器的文件
  • 项目名称/pipelines.py :负责处理爬虫从网页中抽取的实体,持久化实体、验证实体有效性、清除不需要的信息
  • 项目名称/settings.py :项目的设置文件
  • 项目名称/spiders/ :存储爬虫的目录

创建爬虫

创建一个继承 scrapy.Spider 的子类,且定义以下三个属性一个方法:

  • name: 用于区别 Spider ,该名字必须是唯一的
  • allowed_domains: 确定爬取的域名列表。有时候从页面中获取需要爬取的 url 可能会获取到其他网站的域名,如果不在列表中则忽略爬取。如不需要使用可以注释掉。
  • start_urls: 包含了 Spider 在启动时进行爬取的 url 列表,初始 url 是其中之一。后续的 url 则从初始 url 获取到的数据中提取
  • parse(): Spider 的一个方法,每个初始 url 完成下载后生成的 Response 对象将会作为唯一的参数传递给该函数进行处理。该方法负责解析返回的数据(response data)、提取数据(生成item)以及生成需要进一步处理的 url 的 Request 对象

根据提示,进入项目工作目录,使用 scrapy genspider 爬虫名称 爬取页面url 来创建爬虫。

cd doubanTest
scrapy genspider top250 movie.douban.com/top250

Created spider 'top250' using template 'basic' in module:
  doubanTest.spiders.top250

这样就创建了 top250.py 这个爬虫文件。修改一下需要的数据,完成 parse() 方法,就可以了。

response 对象的常用属性和方法

scrapy.Spider 类的 parse() 方法是 scrapy 爬虫的一个回调方法,此方法接受一个 response 对象参数,该对象是 scrapy.http.HtmlResponse 类。response 对象里的内容就是请求获取的响应数据。该对象有一些常用的属性和方法用来访问这些数据:

属性说明
response.url响应的 url 地址
response.headers响应头信息,格式为字典格式。但是字典中使用的不是字符串,而是字节格式
response.status响应状态码
response.body响应体,字节类型
response.text文本内容
response.request请求对象
response.request.url请求地址
response.request.headers请求头

response 对象的解析方法和属性也适用于 selector 对象。

方法或属性说明
css()使用 css 选择器解析 response 并返回一个 selector 节点对象
xpath()使用 xpath 解析 response 并返回一个 selector 节点对象
re()使用正则解析并返回一个字符串
extract()getall()获取 selector 对象的的数据列表
extract_first()get()获取 selector 对象的第一个匹配数据值
attrib获取selector对象的属性字典

解析提取实体

parse() 方法中,可以使用 css 选择器解析,也可以使用 xpath 解析。另外scrapy 使用了一种基于 xpath 和 css 表达式机制的解析: scrapy selectors (selectors 选择器)。parse() 方法中传入的参数 response 就是一个 Selector 对象,也可以使用 Selector(response.text)->Selector 将 HTML 文本创建成 selector 对象。

selector 对象可以使用 extract() 方法和 extrcat_first() 方法来获取需要的数据,区别在于第一种方法会获取一个列表,而第二种方法会获取第一个匹配结果。也可以使用 getall() 方法和 get() 方法,这两种方法是之前两种的简写。
另外可以使用 attrib 对象获取属性字典。例如 selector.attrib['href'] 来获取元素的 href 的值。也可以使用 xpath 的 @ 获取属性对象,并使用get() 方法获取其数据。例如selector.xpath('//a/@href').get()

css 选择器解析

可以使用 .css() 方法返回一系列的 selectors ,每一个selector 表示一个 css 参数表达式选择的节点。

使用 Selector.css(css选择器文本).get() 来获取需要的内容,例如:

response.css('span::text').get()	# 获取 span 标签内的文本内容

xpath 解析

可以使用 .xpath() 方法返回一系列的 selectors ,每一个selector 表示一个 xpath 参数表达式选择的节点。

使用 Selector.xpath(xpath文本).get() 来获取需要的内容,例如:

response.xpath('//span/text()').get()		# 获取 span 标签内的文本内容
response.xpath('//div/span[contains(text(),"文本内容")]').extract()		# 根据文本内容定位标签

re 解析

可以使用 .re() 方法返回一个 unicode 字符串,内容为正则表达式抓取的内容

解析完成

parse() 方法解析完成后,数据会返回给引擎,由引擎判断数据类型。如果是 BaseItemDictNone,则交给管道进行处理;如果是request,则交给调度器,进入下载队列。

将解析的实体数据存入容器

管道会将 parse() 方法作为迭代器调用,并将相应的数据交给容器进行处理,例如持久化保存。

数据容器已经设置过,在 items.py 里,所以需要先在爬虫文件中导入 items.py 中的类(类名称根据项目名称不同而改变),例如:

from doubanTest.items import DoubantestItem

然后在 parse() 方法中,创建容器实例并将解析的数据存储到实例中相应的字段中。例如:

    def parse(self, response):
        lis = response.css('.grid_view').css('li')  # 获取 class='grid_view' 的标签下的 li 标签列表
        for li in lis:  # 在每个 li 标签里
            name = li.xpath('.//span[@class="title"]/text()').get()  # 查找 class='title' 的 span 标签,取文本内容
            stars = li.css('span.rating_num::text').get()           # 查找 class='rating_num 的 span 标签,取文本内容
            critical = li.xpath('.//div[@class="star"]/span')[-1].xpath('./text()').get()[:-3]
            quote = li.css('p.quote>span::text').get()

            # 创建 item 容器,将解析到的数据存放到容器中
            item = DoubantestItem(name=name,stars=stars,critical=critical,quote=quote)
            # 将 item 容器传送到 pipeline
            yield item
            # 或直接返回字典
            # yield {'name': name,'stars': stars,'critical': critical,'quote': quote}

也可以使用 yield 直接返回字典,字典的 key 需要和 item 的名称对应。需要注意的是解析方法中的yield能够传递的对象只能是:BaseItemRequestdictNone

启动爬虫的准备

启动爬虫前,要进行一些准备:

  • 取消 robots 协议
    在设置文件 setting.py 中,找到 ROBOTSTXT_OBEY,设置为 False
  • 修改请求头,防止反扒。
    在设置文件 settings.py 中,找到被注释的 DEFAULT_REQUEST_HEADERS ,取消注释并修改内容添加UA即可。(UA也可以在USER_AGENT 字段中配置)
  • 设置导出数据的格式
    在设置文件 settings.py 中,添加 FEED_FORMAT ,其值可以设为 json、json lines、csv 和 xml
  • 设置导出文件路径
    在设置文件 settings.py 中,添加 FEED_URI ,其值为导出文件路径,支持 FTP 等协议,也可以保存为本地文件,例如 file:///d:/tmp/export.csv ,需要注意的是绝对路径
  • 设置导出字段及顺序
    在设置文件 settings.py 中,添加 FEED_EXPORT_FIELDS ,其值为导出字段的文本列表,例如 [‘name’, ‘stars’, ‘critical’, ‘quote’]
  • 设置等待延迟(因为scrapy是异步的,防止请求太快被封)
    在设置文件 settings.py 中,找到注释的 DOWNLOAD_DELAY,取消注释。参数值会乘以 0.5-1.5 之间的数

启动爬虫

在控制台使用 scrapy crawl 爬虫名称 来启动爬虫。启动后会看到很多 scrapy 的输出日志。可以使用 scrapy crawl 爬虫名称 --nolog 来屏蔽默认的输出日志。但是需慎用,因为一旦屏蔽,代码报错信息也无法显示。

另外可以使用 scrapy crawl 爬虫名称 -o 文件名 -t 输出格式 保存信息到文件中(需要在 settings.py 中正确配置),支持 json,xml,csv格式

爬取流程

一个最简单的流程就是通过开始链接列表初始化 url 并交给调度器,然后调度器封装 request 交给下载器,下载器下载的数据 response 交给爬虫,爬虫解析并处理数据,将数据交给管道或将需要的链接交给调度器继续下载并爬取,管道进行数据处理并保存。

使用解析出的 url (下一页)

parse() 方法中获取的url,例如需要下载的文件、图片,或继续访问的链接(例如翻页),可以打包为 request 对象,并指定处理回调。引擎会将此对象交给调度器,在合适时候交给下载器进行下载,并使用回调函数进行处理。另外 scrapy 也可以使用 response.urljoin() 方法,将下一页的链接地址拼接进请求的 start_url(从start_urls中迭代) 中。

解析“下一页”链接并添加到调度器队列

在爬虫的 parse() 方法中,添加解析和递归代码(因为是递归调用函数,所以添加的代码应该在正常解析数据之后):

        # 解析下一页链接
        nextLink = self.start_urls[0] + response.css('span.next>a').attrib['href']  # css 方式解析
        # nextLink = self.start_urls[0] + response.xpath('//span[@class="next"]/a/@href').get()    # xpath 方式解析
        # 使用 urljoin() 方法
        # nextLink = response.urljoin(response.css('span.next>a').attrib['href'])
        if nextLink:  # 如果存在下一页链接
            # 将下一页链接添加到 scheduler 队列
            # callback 是解析数据的函数
            yield scrapy.Request(nextLink, callback=self.parse)

解析图片地址并下载图片

def parse_img(self, response, image_name):		# 用于处理获取的 img 数据, image_name 在构造 request 对象时进行传递
	# 可以直接将 response.body 交给管道进行保存
	yield {'image_name': image_name + '.jpg', 'image_content': response.body}
	

def parse(self, response):
	# 解析图片地址
	img_link = response.xpath('//img/@src').get()
	img_name = response.xpath('//img/@alt').get()
	if img_link:
		yield scrapy.Request(img_link, callback=self.parse_img, cb_kwargs={'image_name':img_name})

明确实体目标(设置容器)

scrapy 的items.py可以用作数据校验。查看 items.py ,可以看到类定义的说明里写了“在这里为实体定义字段”,并举了例子。所以可以根据例子进行字段定义:

    name = scrapy.Field()       # 电影名称
    stars = scrapy.Field()      # 评分
    critical = scrapy.Field()   # 评分人数
    quote = scrapy.Field()      # 经典影评

使用cmdline直接执行scrapy爬虫

默认 scrapy 是使用控制台命令行来执行爬虫的,可能在调试中会感觉不方便。这时可以使用 cmdline 来直接执行 scrapy 爬虫。

import scrapy
from scrapy import cmdline

class BaiduSpider(scrapy.Spider):
    # 爬虫名称
    name = "baidu"
        # 允许爬取的域名
    allowed_domains = ["baidu.com"]
    # 爬取地址
    start_urls = ["https://baidu.com"]
	
    # 数据解析方法,之后需要我们自己编写逻辑
    def parse(self, response):
        pass


if __name__ == '__main__':
	cmdline.execute('scrapy crawl baidu'.split())

此种执行方法和直接输入指令效果是一样的(有可能日志信息的颜色不太一样)。

信号

扩展

scrapy 可以自定义扩展(Extend),类似于插件。可以在项目根目录下创建相应的扩展脚本,例如 extend.py,并写入合适的逻辑。通常扩展会使用信号和爬虫进行绑定。例如使用扩展每15秒钟获取一个代理ip

# extend.py
import time
import threading

import requests
from scrapy import signals

# 提取代理IP的api
api_url = 'https://dps.kdlapi.com/api/getdps/?secret_id=o1fjh1re9o28876h7c08&signature=xxxxx&num=10&pt=1&format=json&sep=1'
foo = True

# 代理类,通过此类对象获取代理
class Proxy:

    def __init__(self, ):
        self._proxy_list = requests.get(api_url).json().get('data').get('proxy_list')

    @property
    def proxy_list(self):
        return self._proxy_list

    @proxy_list.setter
    def proxy_list(self, list):
        self._proxy_list = list


pro = Proxy()
print(pro.proxy_list)

# 扩展类
class MyExtend:

    def __init__(self, crawler):
        self.crawler = crawler
        # 将自定义方法绑定到scrapy信号上,使程序与spider引擎同步启动与关闭
        # scrapy信号文档: https://www.osgeo.cn/scrapy/topics/signals.html
        # scrapy自定义拓展文档: https://www.osgeo.cn/scrapy/topics/extensions.html
        crawler.signals.connect(self.start, signals.engine_started)
        crawler.signals.connect(self.close, signals.spider_closed)

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def start(self):
        t = threading.Thread(target=self.extract_proxy)
        t.start()

    def extract_proxy(self):
        while foo:
            pro.proxy_list = requests.get(api_url).json().get('data').get('proxy_list')
            #设置每15秒提取一次ip
            time.sleep(15)

    def close(self):
        global foo
        foo = False

然后在 settings.py 中的 EXTENSIONS 中添加 项目名称.extend.MyExtend,即可以使用该扩展了。如果需要添加代理,可以引入此文件的 pro 对象。

CrawlSpider 类

查看爬虫文件可以看到,新建的爬虫文件继承的是 scrapy.Spider 类,但是也可以继承 CrawlSpider 类。当使用 Crawl Spider 类时需引入 from scrayp.spiders import CrawlSpider, Rule 两个类和 from scrapy.linkextractors import LinkExtractor 这个类。

使用 CrawlSpider 比较主要的区别是爬取页面时按照规则(rules)获取链接继续爬取下一页,而 Spider 类需要手动写 yield scrapy.Request(url, callback=self.parse) 。除此之外,一些流程和方法也有改变。

创建 CrawlSpider 爬虫模板

scrapy genspider -t crawl 爬虫名称 域名

Rules 对象

Rules 指定了继续爬取的链接,例如下一页等。

rules =(
	Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True, process_links=None),
)

需要注意的是:

  • LinkExtractro 对象用于定义需要提取的链接
  • follow=True 的意思是是否跟进,即是否继续提取链接
  • process_links 是 LinkExtractor 获取到链接的处理函数,主要用于过滤链接,例如更改参数等
  • callback 的处理函数名称需要使用引号括起来

LinkExtractor 对象

LinkExtractor 对象主要用于获取链接,其主要参数有:

  • allow=() :匹配括号中的正则表达式,如果为空则匹配全部
  • deny=() :和 allow 相反
  • allow_domains=() :会被提取链接的域名
  • deny_domains=() :不被提取链接的域名
  • restrict_xpaths=() :被提取链接的 xpath 表达式(只选到节点,不选属性)

处理函数

处理函数的写法基本没变,需要注意的是,在 CrawlSpider 中不能使用 parse 这个方法名称。

爬取流程

Spider 类不同的是, CrawlSpider 的流程是从 start_urls 开始,初始化 url 然后交给调度器,调度器将 request 交给下载器,下载器下载到的 response 交给 rules 解析,获取到的链接交给调度器封装成 request 发送给下载器下载,然后新下载的 response 交给 rules 提取下一步链接的同时进行解析数据提取,然后交给管道。即第一次访问的页面是不会被提取数据的。

通常处理这个的方式是从这个页面的上一级页面开始爬取,然后在 rules 中添加一条规则,指向这个页面,注意此规则的 follow 也要设置为 True。

管道的处理和使用

在 scrapy 的流程中,爬虫将页面数据 response 解析后发送给管道。管道会以调用迭代器的方式调用解析方法。这里需要注意的是,之前版本发送给管道的数据必须是 item 对象,而比较新的版本中可以发送字典格式。

启用管道

管道文件 pipellines.py 里可以写很多处理类,如果使用的话必须在 setting.py 文件中开启。

在 setting.py 文件中,有 ITEM_PIPELINES 的字典,key 即为需要开启的管道名称,value 是处理优先级。这里需要注意的是 key 的规则为 “项目名称.管道文件名.管道处理类名” 。

管道类的组成

管道类主要由三个方法组成:

  • open_spider(self, spider) : 爬虫开启时执行此方法一次
  • close_spider(self, spider) : 爬虫关闭时执行此方法一次
  • process_item(self, item, spider) : 处理 item 时执行

爬虫和管道的对应关系

当爬虫启动时,每个管道都会被调用且调用时机是相同的,只是根据优先级的不同执行顺序不同而已。因此每个管道可以处理多个爬虫的数据,也可以每个爬虫由多个管道来处理数据。如何确定管道处理哪个爬虫的数据,则需要在管道的类中判断爬虫名称(spider.name)。

由此可见,每个 item 过来,会经过所有的管道。但是如果 item 是无效数据,不想继续进入其他的管道怎么办?可以设置一个优先级比较高(值小)的管道进行判断,如果要扔掉,则使用 scrapy.exceptions.DropItem 类来处理

from scrapy.exceptions import DropItem

class DropPipeline(object):
	def process_item(self, item, spider):
		if item['type'] == '恐怖':
			raise DropItem		# 手动抛出异常
		return item

这样就直接扔掉,其他管道也不会进行处理了。

管道处理不同的解析方法传来的数据

因为每个解析方法都会将数据传给管道,所以管道处理时,需要分辨这些数据是哪个解析方法传来的,然后做相应处理

class SavePipeline(object):
	def process_item(self, item, spider):
		if spider.name == '下载图片' and item['type'] == 'img':
			with open(item.get('img_name'), 'wb') as f:
				f.write(item.get('img_content')
		elif spider.name == '下载图片' and item['type'] == 'info':
			# 保存数据到数据库
			pass

各种数据格式的处理

字典数据

管道获取的 item 是字典时,可以直接将字典内的数据抓出进行处理或保存,也可以将字典转换为字符串进行处理或保存。

可以使用 json 库的 jsum.dumps() 方法将字典转换为字符串,转换时需要注意编码的问题,添加参数 ensure_ascii=False 。

item = json.dumps(item,ensure_ascii=False)

item 对象

item 对象可以直接转换成字典对象,然后再进行处理

item = dict(item)

图片

scrapy 提供了 ImagesPipeline 专门处理图片的数据,且需按照以下流程进行:

  1. 在 item.py 中,添加 image_urlsimages 两个容器 ()
  2. 开启图片管道: 'scrapy.contrib.pipeline.images.ImagesPipeline': 1
  3. 在 setting.py 中设置存储图片的文件夹 :
  4. 在爬虫中,抓取一个项目,将其中图片的 url 放入 image_urls 组内
  5. 当项目进入 ImagesPipeline ,image_urls 组内的 urls 将被 scrapy 的调度器和下载器安排下载。其优先级更高,会在其他页面被抓取前进行处理。项目会在这个特定的管道阶段保持锁定(locked)的状态,直到完成图片下载(或由于某些原因未完成下载)
  6. 当图片下载完成,另一个组(image)将会被更新到结构中。这个组将包含一个字典列表,其中包括下载图片的信息,比如下载路径、源抓取地址和图片校验码。images 列表的图片顺序将和 images_urls 保持一致。如果某个图片下载失败,将会记录下错误信息,不会出现在 images 组中。

实际使用中,只需要设置好 setting.py ,然后在爬虫中将图片的地址列表以字典形式返回,并不需要写 pipelines.py 文件,即可爬取图片。例:

import scrapy

class ZolSpider(scrapy.Spider):
    name = 'zol'
    allowed_domains = ['zol.com.cn']
    start_urls = ['https://desk.zol.com.cn/bizhi/9945_119391_2.html']

    def parse(self, response):
        image_url = response.xpath('//img[@id="bigImg"]/@src').extract()
        yield {
            'image_urls': image_url
        }

深入使用 ImagePipeline 处理图片

简单的爬取图片有一些限制,比如图片名称是 Hash 值 等。如果想要更多功能,则要重写 ImagesPipeline 。

在 pipelines.py 中重写 ImagePipeline:

from scrapy.pipelines.images import ImagesPipeline
import scrapy

class ImagePipeline(ImagesPipeline):
    def get_media_requests(self, item, info):
        for image_url in item['image_urls']:
            yield scrapy.Request(image_url, meta={'image_name': item['image_name']})  # 将文件名传给 request

    def file_path(self, request, response=None, info=None, *, item=None):
        filename = request.meta['image_name'].strip().replace('\r\n\t\t', '').replace('/', '_') + '.jpg'	# 设置文件名
        return filename

需要注意的是,在 setting.py 中将默认处理管道删除,并添加此管道。

Scrapy 的中间件

scrapy 有两种中间件:爬虫中间件和下载中间件。两者的功能是一样的,都是预处理requestresponse对象。在Scrapy默认的情况下,两种中间件都在middlewares.py一个文件中。

爬虫中间件使用方法和下载中间件相同,且功能重复,区别只在处理时机不同(但是在引擎和调度器自动工作的情况下,对用户来说两者处理的时机基本是一致的),所以常使用下载中间件。

具体的处理时机和使用方法在后面。

Scrapy 进阶,重写一些方法

爬虫的 start_request() 方法会返回一个 Request 对象,将此对象传给下载中间件process_request() 方法进行处理,那么我们可以通过重写这些方法进行很多复杂的操作。

重写 start_request

发送带参数的请求

如果对请求有另外的需求,比如需要传递 form 参数等,需要重写 start_request,可以将 url、method、headers、cookies、meta、callback 等参数传给 Request。如果需要传 form ,则可以将 formdata 和上述参数传给 FormRequest 。

# 重写传递 form 数据,字典格式
class sxtSpider(scrapy.Spider):
	name = 'sxt'
	allowed_domains = ['sxt.cn']
	# 重写 request 请求
	def start_requests(self):
		url = 'http://www.sxt.cn/index/login/login.html'	
		form_data = {		# 设置 formdata
			'user': '13513535555',
			'password': '123456'
		}
		# 带 form 数据的发送给 FormRequest ,否则可以发给 Request
		yield scrapy.FormRequest(url, formdata=form_data, callback=self.parse)
		
	def parse(self,response):
		# 已经登录过,可以直接访问其他的页面,并更改解析方法
		yield scrapy.Request('http://www.sxt.cn/index/user.html', callback=self.parse_info)
		
	def parse_info(self,response):
		# 进一步解析数据
		pass
# 重写传递 cookies ,需要注意的是 cookies 只支持字典和列表
class LoginSpider(scrapy.Spider):
	name = 'login'
	def start_requests(self):
		url = 'http://www.sxt.cn/index/user.html'
		cookie_str = 'Um_distinctid=163d8c88a6740c-01c2fe892f8d8c-737245c-100200-163d8c88a582a2; 53gid2=104556692380; 53revisit=13513535555; acw_tc=AqqAAAecrfectaA0aocesdvt'
		# 将 cookie_str 转成字典
		for cookie in cookie_str.split(';'):
			key,value = cookie.split('=', 1)
			cookies[key.strip()] = value.strip()
		yield scrapy.Request(url, cookies=cookies, callback=self.parse)
		
	def parse(self,response):
		# 进一步解析数据
		pass
# 传递自定义参数 meta
class LoginSpider(scrpay.Spider):
	name = 'login'
	start_urls = ['https://passport.ganji.com/login.php']
	def parse(self, response):
		hash_code = re.findall(r'"__hash__":"(.+)"', response.text)[0]		# 获取页面中的 hash 值
		img_url = response.xpath('img[@class="login-img-checkcode"]/@data-url').extract_first()		# 页面中验证码的 url
		yield scrapy.Request(img_url, callback=self.parse_info, meta={'hash_code': hash_code})		# 传递自定义参数 meta	

	def parse_info(self, response):
		hash_code = response.request.meta['hash_code']		# 传递的参数是传到 request 中,所以从 response 的 request 中获取
		with open('yzm.jpg', 'wb') as f:		# 保存验证码图片
			f.write(response.body)

		code = input('请输入验证码:')		# 人工查看验证码
		form_data={
			"username": "17777777777",
			"password": "123456abcd",
			"setcookie": "0",
			"checkCode": code,
			"next": "/",
			"source": "passport",
			"__hash__": hash_code
		}
		login_url = 'https://passport.ganji.com/login.php'
		yield scrapy.FormRequest(login_url, callback=self.after_login, formdata=form_data)
	
	def after_login(self.response):
		# 继续解析
		pass

实时爬取更新信息

既然 start_requests 是 scrapy 的请求入口,那么如果循环固定对某个页面进行访问爬取,则可以获得实时更新的数据。需要注意的是, scrapy 有默认去重的功能,此时需要将这个功能关闭。

def start_requests(self):
	while True:
		yield scrapy.Request(url, callback=self.parse, dont_filter=True)		

重写 process_request

修改 request 的头部信息

可以在下载中间件中设置动态 UA 和代理

使用下载中间件时,同样要在 setting.py 里开启 DOWNLOADER_MIDDLEWARES。如果中间件设置没起作用,可能是优先级太低(数字太大),将优先级提高(数字调小)再试即可。

UA 在中间件的函数 process_request 里设置。

    def process_request(self, request, spider):
        from fake_useragent import UserAgent
        
        request.headers.setdefault(b'User-Agent', UserAgent().random)		# 使用随机 UA
        # 也可以这样写:
        # request.headers['User-Agent'] = UserAgent().random
        return None

也可以在这里设置代理

        # request.meta['proxy'] = 'http://ip:port'			# 匿名代理
        # request.meta['proxy'] = 'http://username:password@ip:port'	# 独享代理

截断 request 请求,并返回数据

正常的流程是 process_request 处理请求信息,然后交给下载器进行下载。如果我们有特殊要求,跳过下载器直接返回响应数据给爬虫,可以在此函数中进行处理。例如使用 selenium 获取已经编译后的页面代码,交给爬虫进行处理。

需要注意的是 process_request 原功能只处理 request 对象,并不返回数据。所以需要使用此方法返回数据,要引入 from scrapy.http import HtmlResponse 类。

from scrapy.http import HtmlResponse

class SeleniumMiddleware(object):		# 下载中间件

    def process_request(self, request, spider):
    	# 从传入的 request 对象中获得 url
        url = request.url
        # selenium 浏览器打开页面,注意这个浏览器对象是 spider 的一个成员
        spider.chrome.get(url)
        # 获取页面编译后的源代码
        html = spider.chrome.page_source
        # 跳过下载器,直接返回 response 对象至爬虫
        return HtmlResponse(url=url, body=html, request=request, encoding='utf-8')

因为涉及到打开 selenium 浏览器,以及爬虫结束后需要自动关闭,所以要在爬虫中重写 spider 的创建过程,添加打开浏览器和关闭浏览器的事件。

import scrapy
from scrapy import signals
from selenium import webdriver
from selenium.webdriver.chrome.service import Service

class GuaziSpider(scrapy.Spider):
    name = 'guazi'
    allowed_domains = ['guazi.com']
    start_urls = ['https://www.guazi.com/buy']

    def parse(self, response):		# 解析部分,先不写
        # print(response.text)
        pass
	
	# 根据爬虫设置创建 spider 对象,并且此对象创建时创建并打开 selenium 浏览器
    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):  
        spider = super(GuaziSpider, cls).from_crawler(crawler, *args, **kwargs)  # 创建 spider 对象
        # 设置一个 spider_closed 的信号,并指定此信号的处理方法
        crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)  
        path = Service('D:\\chromedriver.exe')  # 获取浏览器驱动的路径
        spider.chrome = webdriver.Chrome(service=path)  # 创建 selenium 浏览器实例
        return spider
	
	# spider 结束时会发送 spider_closed 信号,监听到此信号的处理方法
    def spider_closed(self, spider):
        spider.chrome.quit()  # 关闭浏览器

重写 process_response

下载中间件的 process_response 方法接收3个参数,requestresponsespider,该方法在下载器下载完成后触发。

重新请求

可以通过判断 response.status 状态的方式,将 request 对象发给调度器的方式进行重新请求。需要注意的是,scrapy 自带去重过滤,会将已经请求过的 request 对象过滤掉,所以需要设置 requerst.dont_filter

def process_response(self, request, response, spider):
	if response.status != 200:
		request.dont_filter = True	# 关闭过滤
		return request

scrapy 常用的内置方法

scrapy 可以按照预设的流程执行 Spider、SpiderMiddleware、DownloaderMiddleware、ItemPipeline 的相应方法,所以我们可以根据需要重写或继承这些方法。这里列举一些常用的方法。

Spider 的内置方法

Spider 类主要有**parse(self, response, **kwargs)**、**start_request(self)** 内置方法

parse(self, response, **kwargs)

  1. 当引擎获取 Response 对象时,交给Spider解析时被调用
  2. 返回 Request 对象:把 request 对象通过引擎交给调度器
  3. 返回 BaseItem 对象,或字典对象:将数据交给管道进行处理
  4. 此方法是一个生成器,使用 yield 发送结果

**start_request(self)**

  1. 当爬虫启动时,发送请求给引擎。需注意的是,如果使用此方法,则会忽略 start_urls 列表中的地址
  2. 可以不设置 start_urls,而是在此方法中直接封装 Request 对象,发给引擎
  3. 此方法也是一个生成器,所以使用 yield 返回结果

start_request 方法通常使用在:

  1. 如果start_urls列表中的地址需要登录后才能访问,则需要重写start_requests方法并手动添加cookie
  2. 需要自己构建翻页地址的情况下可以重写start_requests方法
  3. 如果在start_urls中的URL需要用POST提交的话,则需要在start_requests方法中修改
  4. 默认情况下start_urls中的URL在被生成Request对象时,都是设置为不过滤,即dont_filter=True,所以如果想使用暂停、恢复爬取功能的话,就需要重写此方法了。

可以在start_request方法中发送 post 请求,或表单:

	yield scrapy.FormRequest(url=url, formdata=data, callback=self.parse, dont_filter=False)
	# yield scrapy.Request(url=url, body=json.dumps(data), method='POST', callback=self.parse)
	# yield scrapy.JsonRequest(url=url, data=json_data)

SpiderMiddleware 的内置方法

SpiderMiddleware 的用处有些和 DownloadMiddleware 重合了,大部分时候可能会在 DownloadMiddleware 中进行相应的处理。

SpiderMiddleware 主要有 process_start_requests(self, start_requests, spider)process_spider_input(self, response, spider)process_spider_output(self, response, result, spider)

process_start_requests(self, start_requests, spider)

  • request 对象通过爬虫中间件时,该方法被调用
  • 该方法必须且仅返回 request 对象,交给引擎
  • 因为爬虫是以迭代器方式发送数据,所以此方法也使用 yield

process_spider_input(self, response, spider)

  • response 通过中间件交给爬虫时,该方法被调用
  • 返回值为None 或引发错误

process_spider_output(self, response, result, spider)

  • 当爬虫处理完 response 对象并返回结果时被调用
  • 必须返回 request 对象或 item 对象(也可以是字典对象)
  • 因为爬虫是以迭代器方式发送数据,所以此方法也使用 yield

DownloadMiddleware 的内置方法

DownloadMiddleware 主要有两个内置方法:process_request(self, request, spider)process_response(self, request, response, spider)

  • process_request(self, request, spider)

    1. 当每个request通过下载中间件时,该方法被调用
    2. 返回None值:没有return也是返回None,该request对象传递给下载器,或通过引擎传递给其他权重低的process_request方法
    3. 返回Response对象:不再请求,把response返回给引擎
    4. 返回Request对象:把request对象通过引擎交给调度器,此时将不通过其他权重低的process_request方法
  • process_response(self, request, response, spider)

    1. 当下载器完成http请求,传递响应给引擎的时候调用
    2. 返回Resposne对象:通过引擎交给爬虫处理或交给权重更低的其他下载中间件的process_response方法
    3. 返回Request对象:通过引擎交给调度器继续请求,此时将不通过其他权重低的process_request方法

注意:需要在settings.py文件中开启中间件,权重越小越优先执行。

ItemPipeline 的内置方法

管道类主要有三个内置方法:process_item(self, item, spider)open_spider(self, spider)close_spider(self, spider)

process_item(self, item, spider)

  • 管道类中必须要有的方法
  • 实现对item数据的处理
  • 一般情况下都会return item,如果没有return,那么就相当于将None传递给权重低的process_item,也可以 raise DropItem

open_spider(self, spider)

  • 在爬虫开启的时候仅执行一次
  • 可以在该方法中链接数据库、打开文件等等

close_spider(self, spider)

  • 在爬虫关闭的时候仅执行一次
  • 可以在该方法中关闭数据库连接、关闭文件对象等

增量爬虫

有些情况下,我们希望能暂停爬虫,之后在恢复运行,尤其是抓取大型站点的时候可以完成暂停与恢复。此时就用到了Scrapy的爬虫暂停与爬虫恢复。

启用可暂停爬虫的命令

想要实现暂停,Scrapy代码不用修改,只需要在启动时修改运行命令即可:

# scrapy crawl 爬虫名称 -s JOBDIR=缓存scrapy信息的路径

scrapy crawl MySpider -s JOBDIR=crawls/my_spider-1

暂停爬虫

暂停爬虫就是终止 python 程序,一般使用快捷键 ctrl + c。需要注意的是,如果程序在执行一些耗时操作,例如下载视频,并不会立刻暂停。此时稍等一下,而不要再按 ctrl + c 强制结束程序。

恢复爬虫执行

恢复爬虫时运行与启用可暂停爬虫相同的命令。

分布式爬虫

分布式爬虫是网络爬虫的一种,它将任务分散在多台计算机上,这些计算机协同工作以更高效地收集网络数据。与传统的单机爬虫相比,分布式爬虫由多个节点组成,每个节点都可以执行爬虫任务,而且这些节点之间相互协作,共享资源和信息。

  • 高效性能:分布式爬虫可以充分利用多台计算机的计算资源和网络带宽,同时执行多个爬虫任务,从而大大提高数据抓取的效率。它能够快速地处理大规模的数据,并在较短的时间内完成爬取任务。
  • 高拓展性:分布式爬虫系统可以根据需求进行横向扩展,通过增加更多的爬虫节点来处理更大规模的数据抓取任务。这使得系统能够适应不断增长的数据量和更高的并发需求。
  • 高可靠性:分布式爬虫系统具有容错和冗余的特性。当某个节点出现故障或者网络问题时,其他节点可以继续执行任务,从而保证数据抓取的连续性和可靠性。
  • 数据一致性:分布式爬虫可以通过合理的任务调度和数据同步机制,确保多个节点爬取的数据保持一致性。这对于需要对多个数据源进行聚合和分析应用非常重要。
  • 大规模数据处理:分布式爬虫系统可以方便地应对大规模数据的处理和存储需求。通过将数据分布在多个节点上,系统可以更高效地处理和存储大量数据。

scrapy 实现分布式爬虫需要用到 scrapy-redis 包

# 使用 scrapy-redis之前最好将scrapy版本保持在2.6.3,2.11.0版本有兼容问题
pip install scrapy=2.6.3
pip install scrapy-redis

添加 scrapy-redis 配置

想要让scrapy实现增量爬取(即暂停、恢复)功能,就需要在scrapy项目中的settings.py文件中进行配置

""" scrapy-redis配置 """
# 调度器类
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 指纹去重类
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 可以替换成布隆过滤器(支持亿万级别数据去重)
# 下载 - pip install scrapy-redis-bloomfilter
# from scrapy_redis_bloomfilter.dupefilter import RFPDupeFilter
# DUPEFILTER_CLASS = 'scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter'

# 是否在关闭时候保留原来的调度器和去重记录,True=保留,False=清空
SCHEDULER_PERSIST = True

# Redis服务器地址
REDIS_URL = "redis://127.0.0.1:6379/0"  # Redis默认有16库,/1的意思是使用序号为2的库,默认是0号库(这个可以任意)
# 使用密码: "redis:@user:password//127.0.0.1:6379/0"

SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'  # 使用有序集合来存储
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'  # 先进先出
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'  # 先进后出、后进先出

# 配置redis管道
# from scrapy_redis.pipelines import RedisPipeline
ITEM_PIPELINES = {
    "douban.pipelines.DoubanPipeline": 300,
    'scrapy_redis.pipelines.RedisPipeline': 301
}

# 重爬: 一般不配置,在分布式中使用重爬机制会导致数据混乱,默认是False
# SCHEDULER_FLUSH_ON_START = True

实现分布式爬虫

实现分布式爬虫需要以下步骤:

  • 普通爬虫继承的类是 scrapy.Spider,分布式爬虫需要将继承的类改为 scrapy_redis.spiders.RedisSpider
  • 添加类属性 redis_key,表示保存需要访问的 urls 的键名,即从哪里获取 url 请求。
  • 删除类属性 start_urls,因为从 redis 中获取请求,所以不自行设置
  • 修改 settings.py 中的 ITEM_PIPELINES,改为(按需要添加) 'scrapy_redis.pipelines.RedisPipeline': 300
  • 因为爬虫本身并没有请求地址,所以创建方法存储请求地址到 redis 中
class Top250Spider(RedisSpider):
	...
	redis_key = 'top250:start_urls'
# settings.py
ITEM_PIPELINES = {
	'scrapy_redis.pipelines.RedisPipeline': 300,
}
# insert_start_urls.py
# 添加请求地址到 redis 中
import redis
with redis.Redis(host='192.168.1.55', port=6379, db=0) as redis_client:
	redis_client.lpush('top250:start_urls', 'https://movie.douban.com/top250?start=0&filter=')

需要注意的是,如果有下载图片或文件之类的,是无法直接存储到 redis 中的,需要编码为 base64 才能进行存储。不过一般不会在 redis 中存储文件,而是使用一个优先级高点的管道来保存文件,然后 raise DropItem,不交给 redis 管道。

到这里分布式配置就结束了,所有的分布式爬虫都会从redis服务器中获取请求,并且保存数据到 redis 服务器中。

部署 scrapyd

如果有大量的 scrapy 项目需要启动,可以使用 scrapyd 进行远程部署调度。scrapydscrapy 的一个组件,需要另外安装(安装在服务端,非scrapy客户端)。

pip install scrapyd

安装完成后可以使用命令来查看是否可以正常使用

scrapyd

配置 scrapyd

安装完成后,需要配置一下 scrapyd,让其支持远程访问。随便在某一目录中创建配置文件 scrapyd.conf,并输入以下内容

[scrapyd]
# 监听的IP地址,默认为127.0.0.1(只有改成0.0.0.0才能在别的电脑上能够访问scrapyd运行之后的服务器)
bind_address = 0.0.0.0
# 监听的端口,默认为6800
http_port   = 6800
# 是否打开debug模式,默认为off
debug = off

然后可以在配置文件所在目录运行 scrapyd,查看是否安装成功。

本地 scrapy 项目上传服务端

配置好并运行了 scrapyd 后,可以将本地的 scrapy 项目上传到服务端。首先配置 scrapy 项目,在项目下的 scrapy.cfg 文件里,设置好 url 项,改位服务端的地址。可以在 deploy 后添加当前节点(客户端)的名称。

[settings]
default = douban.settings

[deploy:node-1]
url = http://192.168.55.5:6800/
project = douban

配置好后可以将项目上传,需要使用到 scrapyd-client。安装完成包后,可以使用 scrapyd-deploy -l 来验证是否可以使用。需要注意的是,此命令必须在 scrapy.cfg 文件所在目录下使用。使用后可以看见节点名称和服务端地址。

使用以下命令发布 scrapy 项目到 scrapyd 服务器:

scrapy-deploy <target> -p <project> --version <version>

参数 <target> 是配置文件中 deploy 后面节点的名称;参数 <project> 可以随便写,一般是项目名称;参数<version>是自定义的版本信息,默认使用时间戳。所以实际项目中使用以下指令上传项目:

scrapy-deploy node-1 -p douban

上传成功将返回一个字典,里面有 status:ok

项目将打包并上传到服务端配置文件的目录下。

web端控制 scrapyd

使用浏览器打开服务端地址,能看到 scrapyd 的信息。如果上传了项目,则可以在 Available projects 中看到。

Jobs 里能监测项目的状态。

执行上传的项目

在客户端里,可以使用 curl 命令来执行已经上传的项目:

curl http://192.168.55.5:6800/schedule.json -d project=douban -d spider=top250

需要注意的有:

  • 服务端的地址和端口
  • project 是需要执行的项目名称,在上传的项目时候设置
  • spider 是执行的爬虫名称,scrapy 爬虫类的 name

执行完成后,可以在返回值中看到 status: ok,表示运行成功。此时可以在 web 端的 jobs 项里看到运行信息。

停止执行

使用 curl 命令来停止执行项目

curl http://192.168.55.5:6800/cancel.json -d project=douban -d job=234798u2318979184u9812344912379851638941

其中 job 的值是任务的 id 值,可以在 web 端的 jobs 项下查看。

使用 scrapydweb 部署

scrapydwebscrapyd 的可视化组件。

pip install scrapydweb

secrpydweb 是基于 scrapyd 的,所以运行之前需要先运行 scrapyd

scrapyd
scrapydweb

注:第一次执行可能会报错,报错后再次执行就可以了。执行成功之后会提示一个地址,可以使用浏览器通过该地址来访问 web 管理页面。

可以在 web 页面中,左侧的 Deploy Project 项目中上传本地项目代码的 zip 压缩文件。

可以在 Run Spider 项目中来选择工程和爬虫来运行。

使用 Gerapy 部署

Gerapy 是国产的爬虫管理软件。可以通过 gerapy 配置 scrapyd,然后直接通过图形化界面开启爬虫。需注意的是 Gerapy 是安装在客户端而不是服务端的。

pip install gerapy=0.9.12

安装完成后,需要初始化。在目标目录中输入

gerapy init

然后进行配置

cd gerapy
gerapy migrate	# 同步 sqlite 数据库
gerapy createsuperuser	# 创建超级管理员
gerapy runserver		# 启动服务,访问地址 127.0.0.1:8000

在浏览器中使用访问地址和超级管理员的账户进行登录,然后在主机管理中创建服务器主机(需要启动 scrapyd)。

项目管理中可以上传爬虫项目的 zip 文件,并进行部署。在主机管理中的调度项里可以执行已经上传的爬虫项目。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值