Spider 的使用

在 Scrapy 中,网站的链接配置,抓取逻辑、解析逻辑其实都是在 Spider 中配置的。在前一节的实例中,我们发现抓取逻辑也是在 Spider 中完成的。本节我们就来专门了解一下 Spider 的基本用法。

Spider 运行流程

在实现 Scrapy 爬虫项目时,最核心的类便是 Spider 类了,它定义了如何爬取某个网站的流程和解析方式。简单来讲,Spider 就是要做如下两件事:

  • 定义爬取网站的动作;

  • 分析爬取下来的网页。

对于 Spider 类来说,整个爬取循环如下所述。

  1. 以初始的 URL 初始化 Request 并设置回调方法。当该 Request 成功请求并返回时,将生成 Response 并将其作为参数传给该回调方法。

  2. 在回调方法内分析返回的网页内容。返回结果可以有两种形式,一种是将解析到的有效结果返回字典或 Item 对象,下一步可直接保存或者经过处理后保存;另一种是解析的下一个(如下一页)链接,可以利用此链接构造 Request 并设置新的回调方法,返回 Request。

  3. 如果返回的是字典或 Item 对象,可通过 Feed Exports 等形式存入文件,如果设置了 Pipeline,可以经由 Pipeline 处理(如过滤、修正等)并保存。

  4. 如果返回的是 Reqeust,那么 Request 执行成功得到 Response 之后会再次传递给 Request 中定义的回调方法,可以再次使用选择器来分析新得到的网页内容,并根据分析的数据生成 Item。

循环进行以上几步,便完成了站点的爬取。

Spider 类分析

在上一节的例子中,我们定义的 Spider 继承自 scrapy.spiders.Spider,即 scrapySpider 类,二者指代的是同一个类,这个类是最简单最基本的 Spider 类,其他的 Spider 必须继承这个类。

这个类里提供了 start_requests 方法的默认实现,读取并请求 start_urls 属性,然后根据返回的结果调用 parse 方法解析结果。另外它还有一些基础属性,下面对其进行讲解。

  • name:爬虫名称,是定义 Spider 名字的字符串。Spider 的名字定义了 Scrapy 如何定位并初始化 Spider,所以它必须是唯一的。不过我们可以生成多个相同的 Spider 实例,这没有任何限制。name 是 Spider 最重要的属性,而且是必须的。如果该 Spider 爬取单个网站,一个常见的做法是以该网站的域名名称来命名 Spider。例如 Spider 爬取 mywebsite.com,该 Spider 通常会被命名为 mywebsite。

  • allowed_domains:允许爬取的域名,是一个可选的配置,不在此范围的链接不会被跟进爬取。

  • start_urls:起始 URL 列表,当我们没有实现 start_requests 方法时,默认会从这个列表开始抓取。

  • custom_settings:一个字典,是专属于本 Spider 的配置,此设置会覆盖项目全局的设置,而且此设置必须在初始化前被更新,所以它必须定义成类变量。

  • crawler:此属性是由 from_crawler 方法设置的,代表的是本 Spider 类对应的 Crawler 对象,Crawler 对象中包含了很多项目组件,利用它我们可以获取项目的一些配置信息,常见的就是获取项目的设置信息,即 Settings。

  • settings:一个 Settings 对象,利用它我们可以直接获取项目的全局设置变量。

除了一些基础属性,Spider 还有一些常用的方法,在此介绍如下。

  • start_requests:此方法用于生成初始请求,它必须返回一个可选代对象,此方法会默认使用 start_urls 里面的 URL 来构造 Request,而且 Request 是 GET 请求方式。如果我们想在启动时以 POST 方式访问某个站点:可以直接重写这个方法,发送 POST 请求时我们使用 FormRequest 即可。

  • parse:当 Response 没有指定回调方法时,该方法会默认被调用,它负责处理 Response,并从中提取想要的数据和下一步的请求,然后返回。该方法需要返回一个包含 Request 或 Item 的可迭代对象。

  • closed:当 Spider 关闭时,该方法会被调用,这里一般会定义释放资源的一些操作或其他收尾操作。

实例演示

接下来我们以一个实例来演示一下 Spider 的一些基本用法。首先我们创建一个 Scrapy 项目,名字叫作 scrapyspiderdemo,创建项目的命令如下:

scrapy startproject scrapyspiderdemo

运行完毕后,当前运行目录便出现了一个 scrapyspiderdemo 文件夹,即对应的 Scrapy 项目就创建成功了。

接着我们进入 demo 文件夹,来针对 www.httpbin.org 这个网站创建一个 Spider,命令如下:

scrapy genspider httpbin www.httpbin.org

这时候我们可以看到项目目录下生成了一个 HttpbinSpider,内容如下:

import scrapy

class HttpbinSpider(scrapy.Spider):
    name = 'httpbin'
    allowed_domains = ['www.httpbin.org']
    start_urls = ['https://www.httpbin.org/']

    def parse(self, response):
        pass

这时候我们可以在 parse 方法中打印输出一些 response 对象的基础信息,同时修改 start_urls 为 https://www.httpbin.org/get ,这个链接可以返回 GET 请求的一些详情信息,最终我们可以将 Spider 修改如下:

import scrapy

class HttpbinSpider(scrapy.Spider):
    name = 'httpbin'
    allowed_domains = ['www.httpbin.org']
    start_urls = ['https://www.httpbin.org/']

    def parse(self, response):
        print('url', response.url)
        print('request', response.request)
        print('status', response.status)
        print('headers', response.headers)
        print('text', response.text)
        print('meta', response.meta)

这里我们打印了 response 的多个属性

  • url:请求的页面 URL,即 Request URL。

  • request:response 对应的 request 对象。

  • status:状态码,即 Response Status Code。

  • headers:响应头,即 Response Headers。

  • text:响应体,即 Response Body。

  • meta:一些附加信息,这些参数往往会附在 meta 属性里。

运行该 Spider,命令如下:

scrapy crawl httpbin

运行结果如下:

2024-01-30 19:15:22 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://httpbin.org/get> (referer: None)
url https://httpbin.org/get
request <GET https://httpbin.org/get>
status 200
headers {b'Content-Length': [b'564'], b'Date': [b'Tue, 30 Jan 2024 11:15:21 GMT'], b'Content-Type': [b'application/json'], b'Server': [b'gunicorn/19.9.0'], b'Access-Control-Allow-Origin': [b'*'], b'Access-Control-Allow-Credentials': [b'true']}
text {
  "args": {},
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en",
    "Cookie": "name=germey; age=26",
    "Host": "httpbin.org",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
    "X-Amzn-Trace-Id": "Root=1-65b8da49-582154551ba2c815108f46af"
  },
  "origin": "27.38.238.130",
  "url": "https://httpbin.org/get?offset=0"
}

以上省略了部分结果,只摘取了关键的 parse 方法的输出内容。

可以看到,这里分别打印输出了 url、request、status、headers、text、meta 信息。我们可以观察一下,text 的内容中包含了我们请求所使用的 User-Agent、请求 IP 等信息,另外 meta 中包含了几个默认设置的参数。

注意,这里并没有显式地声明初始请求,是因为 Spider 默认为我们实现了一个 start_requests 方法,代码如下:

def start_requests(self):
    for url in self.start_urls:
        yield Request(url, dont_filter=True)

可以看到,逻辑就是读取 start_urls 然后生成 Request,这里并没有为 Request 指定 callback,默认就是 parse 方法。它是一个生成器,返回的所有 Request 都会作为初始 Request 加入调度队列。

因此,如果我们想要自定义初始请求,就可以在 Spider 中重写 start_requests 方法,比如我们想自定义请求页面链接和回调方法,可以把 start_requests 方法修改为下面这样:

import scrapy
from scrapy import Request


class HttpbinSpider(scrapy.Spider):
    name = 'httpbin2'
    allowed_domains = ['httpbin.org']
    start_url = 'https://httpbin.org/get'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
    }
    cookies = {'name': 'germey', 'age': '26'}

    def start_requests(self):
        for offset in range(5):
            url = self.start_url + f'?offset={offset}'
            yield Request(url, headers=self.headers,
                          cookies=self.cookies,
                          callback=self.parse_response,
                          meta={'offset': offset})

    def parse_response(self, response):
        print('url', response.url)
        print('request', response.request)
        print('status', response.status)
        print('headers', response.headers)
        print('text', response.text)
        print('meta', response.meta)

这里我们自定义了如下内容。

  • url:我们不再依赖 start_urls 生成 url,而是声明了一个 start_url,然后利用循环给 URL 加上了 Query 参数,如 offset=0,拼接到 https://www.httpbin.org/get 后面,这样请求的链接就变成了 https://www.httpbin.org/get?offset=0

  • headers:这里我们还声明了 headers 变量,为它添加了 User-Agent 属性并将其传递给 Request 的 headers 参数进行赋值。

  • cookies:另外我们还声明了 Cookie,以一个字典的形式声明,然后传给 Request 的 cookies 参数。

  • callback:在 HttpbinSpider 中,我们声明了一个 parse_response 方法,同时我们也将 Request 的 callback 参数设置为 parse_response,这样当该 Request 请求成功时就会回调 parse_response 方法进行处理。

  • meta:meta 可以用来传递额外参数,这里我们将 offset 的值也赋值给 Request,通过 response.meta 就能获取这个内容了,这样就实现了 Request 到 Response 的额外信息传递。

重新运行看看效果,输出内容如下:

url https://httpbin.org/get?offset=1
request <GET https://httpbin.org/get?offset=1>
status 200
headers {b'Content-Length': [b'564'], b'Date': [b'Tue, 30 Jan 2024 11:15:22 GMT'], b'Content-Type': [b'application/json'], b'Server': [b'gunicorn/19.9.0'], b'Access-Control-Allow-Origin': [b'*'], b'Access-Control-Allow-Credentials': [b'true']}
text {
  "args": {
    "offset": "1"
  },
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en",
    "Cookie": "name=germey; age=26",
    "Host": "httpbin.org",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
    "X-Amzn-Trace-Id": "Root=1-65b8da4a-5c3504de597cfd5a7fd5ea4b"
  },
  "origin": "27.38.238.130",
  "url": "https://httpbin.org/get?offset=1"
}

meta {'offset': 1, 'download_timeout': 180.0, 'download_slot': 'httpbin.org', 'download_latency': 0.9361684322357178}

这时候我们看到相应的设置就成功了。

  • url:url 上多了我们添加的 Query 参数。

  • text:结果的 headers 可以看到 Cookie 和 User-Agent,说明 Request 的 Cookie 和 User-Agent 都设置成功了。

  • meta:meta 中看到了 offset 这个参数,说明通过 meta 可以成功传递额外的参数。

通过上面的案例,我们就大致知道了 Spider 的基本流程和配置,可以发现其实现还是很灵活的。

当然除了发起 GET 请求,我们还可以发起 POST 请求。POST 请求主要分为两种,一种是以 Form Data 的形式提交表单,一种是发送 JSON 数据,二者分别可以使用 FormRequest 和 JsonRequest 来实现。例如我们可以分别发起两种 POST 请求,对比一下结果:

import scrapy
from scrapy.http import JsonRequest, FormRequest


class HttpbinSpider(scrapy.Spider):
    name = 'httpbin'
    allowed_domains = ['httpbin.org']
    start_url = 'https://httpbin.org/post'
    data = {'name': 'germey', 'age': '26'}

    def start_requests(self):
        yield FormRequest(self.start_url,
                          callback=self.parse_response,
                          formdata=self.data)
        yield JsonRequest(self.start_url,
                          callback=self.parse_response,
                          data=self.data)

    def parse_response(self, response):
        print('text', response.text)

这里我们利用 start_requests 方法生成了一个 FormRequest 和 JsonRequest,请求的页面链接修改为了 https://www.httpbin.org/post ,它可以把 POST 请求的详情返回,另外 data 保持不变。

运行结果如下:

2024-01-30 19:22:56 [scrapy.core.engine] DEBUG: Crawled (200) <POST https://httpbin.org/post> (referer: None)
text {
  "args": {},
  "data": "",
  "files": {},
  "form": {
    "age": "26",
    "name": "germey"
  },
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en",
    "Content-Length": "18",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "httpbin.org",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.0 (KHTML, like Gecko) Chrome/28.0.857.0 Safari/536.0",
    "X-Amzn-Trace-Id": "Root=1-65b8dc10-0b5b9afd33b4b51b71974d71"
  },
  "json": null,
  "origin": "27.38.238.130",
  "url": "https://httpbin.org/post"
}

2024-01-30 19:22:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST https://httpbin.org/post> (referer: None)
text {
  "args": {},
  "data": "{\"age\": \"26\", \"name\": \"germey\"}",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "application/json, text/javascript, */*; q=0.01",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en",
    "Content-Length": "31",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "User-Agent": "Mozilla/5.0 (Windows NT 5.1; ur-IN; rv:1.9.2.20) Gecko/3174-05-19 00:19:44 Firefox/3.6.4",
    "X-Amzn-Trace-Id": "Root=1-65b8dc10-3b65de4b0f02dc7823685415"
  },
  "json": {
    "age": "26",
    "name": "germey"
  },
  "origin": "27.38.238.130",
  "url": "https://httpbin.org/post"
}

这里我们可以看到两种请求的效果。

第一个 FormRequest,我们可以观察到页面返回结果的 form 字段就是我们请求时添加的 data 内容,这说明实际上是发送了 Content-Type 为 application/x-www-form-urlencoded 的 POST 请求,这种对应的就是表单提交。

第二个 JsonRequest,我们可以观察到页面返回结果的 json 字段就是我们所请求时添加的 data 内容,这说明实际上是发送了 Content-Type 为 application/json 的 POST 请求,这种对应的就是发送 JSON 数据。

这两种 POST 请求的发送方式我们需要区分清楚,并根据服务器的实际需要进行选择。

Request 和 Response

在上面的 Spider 例子中,大部分流程实际是在构造 Request 对象和解析 Response 对象,因此对于它们的用法和参数我们需要详细了解一下。

Request

在 Scrapy 中,Request 对象实际上指的就是 scrapy.http.Request 的一个实例,它包含了 HTTP 请求的基本信息,用这个 Request 类我们可以构造 Request 对象发送 HTTP 请求,它会被 Engine 交给 Downloader 进行处理执行,返回一个 Response 对象。

这个 Request 类怎么使用呢?那自然要了解一下它的构造参数都有什么,梳理如下。

  • url:Request 的页面链接,即 Request URL。

  • callback:Request 的回调方法,通常这个方法需要定义在 Spider 类里面,并且需要对应一个 response 参数,代表 Request 执行请求后得到的 Response 对象。如果这个 callback 参数不指定,默认会使用 Spider 类里面的 parse 方法。

  • method:Request 的方法,默认是 GET,还可以设置为 POST、PUT、DELETE 等。

  • meta:Request 请求携带的额外参数,利用 meta,我们可以指定任意处理参数,特定的参数经由 Scrapy 各个组件的处理,可以得到不同的效果。另外,meta 还可以用来向回调方法传递信息。

  • body:Request 的内容,即 Request Body,往往 Request Body 对应的是 POST 请求,我们可以使用 FormRequest 或 JsonRequest 更方便地实现 POST 请求。

  • headers:Request Headers,是字典形式。

  • cookies:Request 携带的 Cookie,可以是字典或列表形式。

  • encoding:Request 的编码,默认是 utf-8。

  • prority:Request 优先级,默认是 0,这个优先级是给 Scheduler 做 Request 调度使用的,数值越大,就越被优先调度并执行。

  • dont_filter:Request 不去重,Scrapy 默认会根据 Request 的信息进行去重,使得在爬取过程中不会出现重复请求,设置为 True 代表这个 Request 会被忽略去重操作,默认是 False。

  • errback:错误处理方法:如果在请求处理过程中出现了错误,这个方法就会被调用。

  • flags:请求的标志,可以用于记录类似的处理。

  • cb_kwargs:回调方法的额外参数,可以作为字典传递。

以上便是 Request 的构造参数,利用这些参数,我们可以灵活地实现 Request 的构造。

值得注意的是,meta 参数是一个十分有用而且易扩展的参数,它可以以字典的形式传递,包含的信息不受限制,所以很多 Scrapy 的插件会基于 meta 参数做一些特殊处理。在默认情况下,Scrapy 就预留了一些特殊的 key 作为特殊处理。

比如 request.meta['proxy']可以用来设置请求时使用的代理,request.meta['max_retry_times'] 可以设置用来设置请求的最大重试次数等。

另外如上文所介绍的,Scrapy 还专门为 POST 请求提供了两个类一FormRequest 和 JsonRequest,它们都是 Request 类的子类,我们可以利用 FormRequest 的 formdata 参数传递表单内容,利用 JsonRequest 的 json 参数传递 JSON 内容,其他的参数和 Request 基本是一致的。二者的详细介绍可以参考官方文档:

Response

Request 由 Downloader 执行之后,得到的就是 Response 结果了,它代表的是 HTTP 请求得到的响应结果,同样地我们可以梳理一下其可用的属性和方法,以便我们做解析处理使用。

  • url:Request URL。

  • status:Response 状态码,如果请求成功就是 200。

  • headers:Response Headers,是一个字典,字段是一一对应的。

  • body:Response Body,这个通常就是访问页面之后得到的源代码结果了,比如里面包含的是 HTML 或者 JSON 字符串,但注意其结果是 bytes 类型。

  • request:Response 对应的 Request 对象。

  • certificate:是 twisted.internet.ssl.Certificate 类型的对象,通常代表一个 SSL 证书对象。

  • ip_address:是一个 ipaddress.IPv4Address 或 ipaddress.IPv6Address 类型的对象,代表服务器的 IP 地址。

  • urljoin:是对 URL 的一个处理方法,可以传人当前页面的相对 URL,该方法处理后返回的就是绝对 URL。

  • follow/follow_all:是一个根据 URL 来生成后续 Request 的方法,和直接构造 Request 不同的是,该方法接收的 url 可以是相对 URL,不必一定是绝对 URL。

另外 Response 还有几个常用的子类,如 TextResponse 和 HtmlResponse,HtmlResponse 又是 TextResponse 的子类,实际上回调方法接收的 response 参数就是一个 HtmlResponse 对象,它还有几个常用的方法或属性。

  • text:同 body 属性,但结果是 str 类型。

  • encoding:Response 的编码,默认是 utf-8。

  • selector:根据 Response 的内容构造而成的 Selector 对象,Selector 在上一节我们已经了解过,利用它我们可以进一步调用 xpath、css 等方法进行结果的提取。

  • xpath:传入 XPath 进行内容提取,等同于调用 selector 的 xpath 方法。

  • css:传入 css 选择器进行内容提取,等同于调用 selector 的 css 方法。

  • json:是 Scrapy 2.2 新增的方法,利用该方法可以直接将 text 属性转为 JSON 对象。

以上便是对 Response 的基本介绍,关于 Response 更详细的解释可以参考官方文档: https://docs.scrapy.org/en/latest/topics/request-responsehtml#response-subclasses

总结

本节中我们介绍了 Spider 的基本使用方法以及 Request,Response 对象的基本数据结构,通过了解本节内容,我们便可以灵活地完成爬取逻辑的定制了。