Scrapy 教程

在本教程中,我们假设 Scrapy 已经安装在您的系统上。如果不是这种情况,请参阅安装指南

我们将抓取quotes.toscrape.com,这是一个列出著名作家语录的网站。

本教程将引导您完成以下任务

  1. 创建一个新的 Scrapy 项目

  2. 编写一个Spider 来爬取网站并提取数据

  3. 使用命令行导出抓取的数据

  4. 更改 Spider 以递归方式跟随链接

  5. 使用 Spider 参数

Scrapy 使用Python编写。您对 Python 了解得越多,您就能从 Scrapy 中获得更多收益。

如果您已经熟悉其他语言并希望快速学习 Python,则Python 教程是一个很好的资源。

如果您是编程新手并希望从 Python 开始,以下书籍可能对您有所帮助

您还可以查看此 Python 非程序员资源列表,以及learnpython-subreddit 中推荐的资源

创建项目

在开始抓取之前,您需要设置一个新的 Scrapy 项目。输入您想要存储代码的目录并运行

scrapy startproject tutorial

这将在 tutorial 目录中创建以下内容

tutorial/
    scrapy.cfg            # deploy configuration file

    tutorial/             # project's Python module, you'll import your code from here
        __init__.py

        items.py          # project items definition file

        middlewares.py    # project middlewares file

        pipelines.py      # project pipelines file

        settings.py       # project settings file

        spiders/          # a directory where you'll later put your spiders
            __init__.py

我们的第一个 Spider

Spider 是您定义的类,Scrapy 使用它们从网站(或一组网站)中抓取信息。它们必须是 Spider 的子类,并定义要发出的初始请求,以及可选地,如何在页面中跟随链接以及解析下载的页面内容以提取数据。

这是我们第一个 Spider 的代码。将其保存在名为 quotes_spider.py 的文件中,该文件位于项目中的 tutorial/spiders 目录下

from pathlib import Path

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        urls = [
            "https://quotes.toscrape.com/page/1/",
            "https://quotes.toscrape.com/page/2/",
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = f"quotes-{page}.html"
        Path(filename).write_bytes(response.body)
        self.log(f"Saved file {filename}")

如您所见,我们的 Spider 是 scrapy.Spider 的子类,并定义了一些属性和方法

  • name:标识 Spider。它在项目中必须是唯一的,也就是说,您不能为不同的 Spider 设置相同的名称。

  • start_requests():必须返回一个 Request 可迭代对象(您可以返回一个请求列表或编写一个生成器函数),Spider 将从此处开始爬取。后续请求将从这些初始请求中依次生成。

  • parse():一个将在为每个发出的请求下载响应时调用的方法。response 参数是 TextResponse 的实例,它保存页面内容并具有更多有用的方法来处理它。

    parse() 方法通常解析响应,将抓取的数据提取为字典,并查找要跟随的新 URL,并从中创建新的请求(Request)。

如何运行我们的 Spider

要让我们的 Spider 运行起来,请转到项目的顶级目录并运行

scrapy crawl quotes

此命令运行我们刚刚添加的名为 quotes 的 Spider,它将向 quotes.toscrape.com 域发送一些请求。您将获得类似于以下内容的输出

... (omitted for brevity)
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Spider opened
2016-12-16 21:24:05 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-12-16 21:24:05 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (404) <GET https://quotes.toscrape.com/robots.txt> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://quotes.toscrape.com/page/1/> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://quotes.toscrape.com/page/2/> (referer: None)
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-1.html
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-2.html
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Closing spider (finished)
...

现在,检查当前目录中的文件。您应该注意到已创建了两个新文件:quotes-1.htmlquotes-2.html,其中包含相应 URL 的内容,因为我们的 parse 方法指示。

注意

如果您想知道为什么我们还没有解析 HTML,请稍等,我们很快就会介绍。

幕后发生了什么?

Scrapy 调度 Spider 的 scrapy.Request 对象返回的 start_requests 方法。在为每个请求收到响应后,它会实例化 Response 对象并调用与请求关联的回调方法(在本例中为 parse 方法),并将响应作为参数传递。

start_requests 方法的快捷方式

而不是实现一个 start_requests() 方法,该方法从 URL 生成 scrapy.Request 对象,您可以只定义一个 start_urls 类属性,其中包含 URL 列表。然后,此列表将由 start_requests() 的默认实现用于为您的 Spider 创建初始请求。

from pathlib import Path

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        "https://quotes.toscrape.com/page/1/",
        "https://quotes.toscrape.com/page/2/",
    ]

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = f"quotes-{page}.html"
        Path(filename).write_bytes(response.body)

parse() 方法将被调用以处理这些 URL 的每个请求,即使我们没有明确告诉 Scrapy 这样做。这是因为 parse() 是 Scrapy 的默认回调方法,它用于没有显式分配回调的请求。

提取数据

学习如何使用 Scrapy 提取数据的最佳方法是使用Scrapy shell尝试选择器。运行

scrapy shell 'https://quotes.toscrape.com/page/1/'

注意

请记住,在从命令行运行 Scrapy shell 时始终将 URL 括在引号中,否则包含参数(即 & 字符)的 URL 将无法工作。

在 Windows 上,请改用双引号

scrapy shell "https://quotes.toscrape.com/page/1/"

您将看到类似以下内容

[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://quotes.toscrape.com/page/1/> (referer: None)
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    <scrapy.crawler.Crawler object at 0x7fa91d888c90>
[s]   item       {}
[s]   request    <GET https://quotes.toscrape.com/page/1/>
[s]   response   <200 https://quotes.toscrape.com/page/1/>
[s]   settings   <scrapy.settings.Settings object at 0x7fa91d888c10>
[s]   spider     <DefaultSpider 'default' at 0x7fa91c8af990>
[s] Useful shortcuts:
[s]   shelp()           Shell help (print this help)
[s]   fetch(req_or_url) Fetch request (or URL) and update local objects
[s]   view(response)    View response in a browser

使用 shell,您可以尝试使用 response 对象的 CSS 选择元素

>>> response.css("title")
[<Selector query='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]

运行 response.css('title') 的结果是一个称为 SelectorList 的类似列表的对象,它表示一个 Selector 对象列表,这些对象围绕 XML/HTML 元素,并允许您运行进一步的查询以细化选择或提取数据。

要提取上面标题的文本,您可以执行以下操作

>>> response.css("title::text").getall()
['Quotes to Scrape']

这里需要注意两点:一是我们在 CSS 查询中添加了 ::text,表示我们只想选择 <title> 元素内部的文本元素。如果我们不指定 ::text,我们将获得完整的标题元素,包括其标签

>>> response.css("title").getall()
['<title>Quotes to Scrape</title>']

另一件事是调用 .getall() 的结果是一个列表:选择器可能会返回多个结果,因此我们提取所有结果。当您知道您只需要第一个结果时,如本例所示,您可以执行以下操作

>>> response.css("title::text").get()
'Quotes to Scrape'

或者,您可以编写

>>> response.css("title::text")[0].get()
'Quotes to Scrape'

SelectorList 实例上访问索引将在没有结果时引发 IndexError 异常

>>> response.css("noelement")[0].get()
Traceback (most recent call last):
...
IndexError: list index out of range

您可能希望直接在SelectorList 实例上使用.get(),如果没有任何结果,它将返回None

>>> response.css("noelement").get()

这里有一个教训:对于大多数抓取代码,您希望它能够抵御因页面上找不到内容而导致的错误,这样即使某些部分抓取失败,您至少也可以获取一些数据。

除了getall()get() 方法之外,您还可以使用re() 方法使用正则表达式进行提取。

>>> response.css("title::text").re(r"Quotes.*")
['Quotes to Scrape']
>>> response.css("title::text").re(r"Q\w+")
['Quotes']
>>> response.css("title::text").re(r"(\w+) to (\w+)")
['Quotes', 'Scrape']

为了找到要使用的正确 CSS 选择器,您可能会发现使用view(response)在 Web 浏览器中打开 shell 中的响应页面很有用。您可以使用浏览器的开发者工具检查 HTML 并想出一个选择器(参见使用浏览器的开发者工具进行抓取)。

Selector Gadget 也是一个很好的工具,可以快速找到视觉选择元素的 CSS 选择器,它可以在许多浏览器中使用。

XPath:简要介绍

除了CSS 之外,Scrapy 选择器还支持使用XPath 表达式。

>>> response.xpath("//title")
[<Selector query='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath("//title/text()").get()
'Quotes to Scrape'

XPath 表达式非常强大,是 Scrapy 选择器的基础。事实上,CSS 选择器在后台会转换为 XPath。如果您仔细阅读 shell 中选择器对象的文本表示形式,就可以看到这一点。

虽然可能不如 CSS 选择器流行,但 XPath 表达式提供了更强大的功能,因为它除了导航结构之外,还可以查看内容。使用 XPath,您可以选择诸如:包含文本“下一页”的链接之类的东西。这使得 XPath 非常适合抓取任务,我们鼓励您学习 XPath,即使您已经知道如何构建 CSS 选择器,它也会使抓取变得更容易。

我们在这里不会过多介绍 XPath,但您可以在此处阅读有关使用 Scrapy 选择器与 XPath 的更多信息。要了解有关 XPath 的更多信息,我们推荐此教程通过示例学习 XPath,以及此教程学习“如何用 XPath 思考”

提取引言和作者

现在您已经了解了一些关于选择和提取的信息,让我们通过编写提取网页引言的代码来完成我们的蜘蛛。

https://quotes.toscrape.com 中,每个引言都由如下所示的 HTML 元素表示

<div class="quote">
    <span class="text">“The world as we have created it is a process of our
    thinking. It cannot be changed without changing our thinking.”</span>
    <span>
        by <small class="author">Albert Einstein</small>
        <a href="/author/Albert-Einstein">(about)</a>
    </span>
    <div class="tags">
        Tags:
        <a class="tag" href="/tag/change/page/1/">change</a>
        <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
        <a class="tag" href="/tag/thinking/page/1/">thinking</a>
        <a class="tag" href="/tag/world/page/1/">world</a>
    </div>
</div>

让我们打开 scrapy shell 并玩一下,找出如何提取我们想要的数据。

scrapy shell 'https://quotes.toscrape.com'

我们使用以下命令获取引言 HTML 元素的选择器列表:

>>> response.css("div.quote")
[<Selector query="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
<Selector query="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
...]

上面查询返回的每个选择器都允许我们对其子元素运行进一步的查询。让我们将第一个选择器分配给一个变量,以便我们可以直接在特定的引言上运行我们的 CSS 选择器。

>>> quote = response.css("div.quote")[0]

现在,让我们使用我们刚刚创建的quote 对象从该引言中提取textauthortags

>>> text = quote.css("span.text::text").get()
>>> text
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author = quote.css("small.author::text").get()
>>> author
'Albert Einstein'

鉴于标签是字符串列表,我们可以使用.getall() 方法获取所有标签。

>>> tags = quote.css("div.tags a.tag::text").getall()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']

在弄清楚如何提取每个部分之后,我们现在可以遍历所有引言元素并将它们组合到 Python 字典中。

>>> for quote in response.css("div.quote"):
...     text = quote.css("span.text::text").get()
...     author = quote.css("small.author::text").get()
...     tags = quote.css("div.tags a.tag::text").getall()
...     print(dict(text=text, author=author, tags=tags))
...
{'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', 'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}
{'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'tags': ['abilities', 'choices']}
...

在我们的蜘蛛中提取数据

让我们回到我们的蜘蛛。到目前为止,它还没有提取任何特定数据,只是将整个 HTML 页面保存到本地文件。让我们将上面的提取逻辑集成到我们的蜘蛛中。

Scrapy 蜘蛛通常会生成许多包含从页面提取的数据的字典。为此,我们使用回调中的yield Python 关键字,如下所示。

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        "https://quotes.toscrape.com/page/1/",
        "https://quotes.toscrape.com/page/2/",
    ]

    def parse(self, response):
        for quote in response.css("div.quote"):
            yield {
                "text": quote.css("span.text::text").get(),
                "author": quote.css("small.author::text").get(),
                "tags": quote.css("div.tags a.tag::text").getall(),
            }

要运行此蜘蛛,请通过输入以下命令退出 scrapy shell:

quit()

然后,运行:

scrapy crawl quotes

现在,它应该使用日志输出提取的数据。

2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com/page/1/>
{'tags': ['life', 'love'], 'author': 'André Gide', 'text': '“It is better to be hated for what you are than to be loved for what you are not.”'}
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com/page/1/>
{'tags': ['edison', 'failure', 'inspirational', 'paraphrased'], 'author': 'Thomas A. Edison', 'text': "“I have not failed. I've just found 10,000 ways that won't work.”"}

存储抓取的数据

存储抓取数据的最简单方法是使用Feed 导出,使用以下命令:

scrapy crawl quotes -O quotes.json

这将生成一个quotes.json 文件,其中包含所有抓取的项目,并以JSON 格式序列化。

-O 命令行开关会覆盖任何现有文件;请改用-o 将新内容追加到任何现有文件。但是,将内容追加到 JSON 文件会使文件内容成为无效的 JSON。在追加到文件时,请考虑使用不同的序列化格式,例如JSON Lines

scrapy crawl quotes -o quotes.jsonl

JSON Lines 格式很有用,因为它类似于流,因此您可以轻松地向其中追加新记录。当您运行两次时,它不会像 JSON 那样出现相同的问题。此外,由于每个记录都是一行,因此您可以处理大型文件,而无需将所有内容都放入内存中,有一些工具(如JQ)可以在命令行中帮助执行此操作。

在小型项目(例如本教程中的项目)中,这应该就足够了。但是,如果您想对抓取的项目执行更复杂的操作,则可以编写一个项目管道。在创建项目时,已为您设置了一个项目管道的占位符文件,位于tutorial/pipelines.py 中。但是,如果您只想存储抓取的项目,则无需实现任何项目管道。

使用 Spider 参数

您可以使用 -a 选项在运行 Spider 时向其提供命令行参数。

scrapy crawl quotes -O quotes-humor.json -a tag=humor

这些参数默认情况下会传递到 Spider 的 __init__ 方法,并成为 Spider 属性。

在此示例中,为 tag 参数提供的值可以通过 self.tag 访问。您可以使用它来使您的 Spider 仅抓取具有特定标签的语录,并根据参数构建 URL。

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        url = "https://quotes.toscrape.com/"
        tag = getattr(self, "tag", None)
        if tag is not None:
            url = url + "tag/" + tag
        yield scrapy.Request(url, self.parse)

    def parse(self, response):
        for quote in response.css("div.quote"):
            yield {
                "text": quote.css("span.text::text").get(),
                "author": quote.css("small.author::text").get(),
            }

        next_page = response.css("li.next a::attr(href)").get()
        if next_page is not None:
            yield response.follow(next_page, self.parse)

如果您向此 Spider 传递 tag=humor 参数,您会注意到它只会访问 humor 标签下的 URL,例如 https://quotes.toscrape.com/tag/humor

您可以在这里了解有关处理 Spider 参数的更多信息

后续步骤

本教程仅涵盖了 Scrapy 的基础知识,但还有许多其他功能在此处未提及。请查看 其他内容? 部分,该部分位于 Scrapy 概览 章节中,可以快速了解最重要的功能。

您可以从 基本概念 部分继续学习,以了解更多关于命令行工具、Spider、选择器和其他教程未涵盖的内容,例如对抓取的数据进行建模。如果您更喜欢使用示例项目进行操作,请查看 示例 部分。