Skip to main content

Scrapy教程

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

我们将要删除 quotes.toscrape.com,这是一个列出着名作者的报价的网站。

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

  1. 创建一个新的Scrapy项目

  2. 编写 蜘蛛 以爬网站点和提取数据

  3. 使用命令行导出已删除的数据

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

  5. 使用蜘蛛参数

Scrapy是在 Python 中写的。如果你是新来的语言,你可能想开始通过了解语言是什么样子,以充分利用Scrapy。

如果您已经熟悉其他语言,并且想要快速学习Python,我们建议您通过 Dive Into Python 3 阅读。或者,您可以按照 Python Tutorial

如果你刚接触编程,想从Python开始,你可能会发现有用的在线书 Learn Python The Hard Way。你也可以看看 this list of Python resources for non-programmers

创建项目

在开始刮擦之前,您必须设置一个新的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

        pipelines.py      # project pipelines file

        settings.py       # project settings file

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

我们的第一个蜘蛛

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

这是我们第一个蜘蛛的代码。将其保存在项目中 tutorial/spiders 目录下名为 quotes_spider.py 的文件中:

import scrapy


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

    def start_requests(self):
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://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 = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file %s' % filename)

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

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

  • start_requests():必须返回一个可迭代的请求(你可以返回一个请求列表或写一个生成器函数),蜘蛛将开始抓取。后续请求将从这些初始请求连续生成。

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

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

如何运行你的蜘蛛

要让我们的蜘蛛工作,去项目的顶级目录并运行:

scrapy crawl quotes

这个命令运行我们刚才添加的名为 quotes 的蜘蛛,它将发送一些对 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 http://quotes.toscrape.com/robots.txt> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://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的 start_requests 方法返回的 scrapy.Request 对象。在接收到每个响应时,它实例化 Response 对象并调用与请求(在这种情况下,parse 方法)相关联的回调方法,将响应作为参数传递。

start_requests方法的快捷方式

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

import scrapy


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

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)

将调用 parse() 方法来处理对这些URL的每个请求,即使我们没有明确告诉Scrapy这样做。发生这种情况是因为 parse() 是Scrapy的默认回调方法,该方法针对没有明确分配回调的请求调用。

提取数据

学习如何使用Scrapy提取数据的最好方法是使用shell Scrapy壳 尝试选择器。跑:

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

注解

记住,当从命令行运行Scrapy shell时,总是用引号引起url,否则包含参数的urls(即 & 字符)将不起作用。

在Windows上,请使用双引号:

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

你会看到类似的东西:

[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://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 http://quotes.toscrape.com/page/1/>
[s]   response   <200 http://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,您可以尝试使用带有响应对象的 CSS 选择元素:

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

运行 response.css('title') 的结果是一个名为 SelectorList 的列表式对象,它表示一个包含XML/HTML元素的 Selector 对象列表,并允许您运行进一步的查询以精细选择或提取数据。

要从上面的标题中提取文本,你可以做:

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

这里有两个要注意的事情:一个是我们添加了 ::text 到CSS查询,意味着我们只想直接在 <title> 元素中选择文本元素。如果我们不指定 ::text,我们将获得完整的title元素,包括其标签:

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

另一件事是调用 .extract() 的结果是一个列表,因为我们正在处理一个 SelectorList 的实例。当你知道你只想要第一个结果,在这种情况下,你可以做:

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

作为替代,你可以写:

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

然而,使用 .extract_first() 避免了 IndexError,并且在找不到与选择匹配的任何元素时返回 None

这里有一个教训:对于大多数剪贴代码,您希望它对于错误是有弹性的,因为在页面上找不到任何东西,所以即使一些部分没有被抓取,您至少可以获得 一些 数据。

除了 extract()extract_first() 方法,您还可以使用 re() 方法提取使用 regular expressions:

>>> 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选择器使用,你可能会发现有用的打开响应页面从您的Web浏览器中的shell使用 view(response)。您可以使用浏览器开发人员工具或扩展(如Firebug)(请参见有关 使用Firebug进行刮使用Firefox刮 的部分)。

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

XPath:简短介绍

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

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

XPath表达式非常强大,是Scrapy选择器的基础。事实上,CSS选择器转换为XPath under-the-hood。你可以看到,如果你仔细阅读在shell中的选择器对象的文本表示。

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

我们不会在这里介绍XPath的很多,但你可以阅读更多关于 在这里使用XPath与Scrapy选择器。要了解有关XPath的更多信息,我们建议使用 本教程通过示例学习XPath本教程将学习“如何在XPath中思考”

提取引号和作者

现在你知道了一点关于选择和提取,让我们通过编写代码从网页中提取引号来完成我们的蜘蛛。

http://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 'http://quotes.toscrape.com'

我们得到一个列表的HTML元素的选择器:

>>> response.css("div.quote")

通过上面的查询返回的每个选择器允许我们对它们的子元素执行进一步的查询。让我们将第一个选择器分配给一个变量,以便我们可以直接对特定报价运行我们的CSS选择器:

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

现在,让我们使用我们刚刚创建的 quote 对象从该报价中提取 titleauthortags:

>>> title = quote.css("span.text::text").extract_first()
>>> title
'“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").extract_first()
>>> author
'Albert Einstein'

鉴于标签是字符串列表,我们可以使用 .extract() 方法来获取它们:

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

在找出了如何提取每个位之后,我们现在可以遍历所有的引号元素,并将它们放在一起成为一个Python字典:

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

在我们的蜘蛛中提取数据

让我们回到我们的蜘蛛。直到现在,它不会提取任何特别的数据,只是将整个HTML页面保存到本地文件。让我们将上面的提取逻辑集成到我们的蜘蛛中。

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

import scrapy


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

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

如果你运行这个蜘蛛,它将输出提取的数据与日志:

2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://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 http://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.”"}

存储已删除的数据

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

scrapy crawl quotes -o quotes.json

这将生成一个 quotes.json 文件,其中包含所有被刮除的项目,在 JSON 中序列化。

出于历史原因,Scrapy会附加到给定文件,而不是覆盖其内容。如果你运行这个命令两次,没有在第二次之前删除文件,你会得到一个破碎的JSON文件。

您也可以使用其他格式,如 JSON Lines:

scrapy crawl quotes -o quotes.jl

JSON Lines 格式是有用的,因为它是流式的,你可以轻松地附加新记录到它。它不具有相同的JSON问题,当你运行两次。此外,由于每条记录都是单独的行,因此您可以处理大文件,而无需将所有内容都放在内存中,有像 JQ 这样的工具可以帮助在命令行执行。

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

使用蜘蛛参数

您可以在运行它们时通过使用 -a 选项为您的蜘蛛提供命令行参数:

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

这些参数传递给Spider的 __init__ 方法,默认情况下成为蜘蛛属性。

在此示例中,为 tag 参数提供的值将通过 self.tag 提供。您可以使用它来使您的蜘蛛仅获取带有特定标记的引号,并根据参数构建URL:

import scrapy


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

    def start_requests(self):
        url = 'http://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').extract_first(),
                'author': quote.css('small.author::text').extract_first(),
            }

        next_page = response.css('li.next a::attr(href)').extract_first()
        if next_page is not None:
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, self.parse)

如果您将 tag=humor 参数传递给此蜘蛛,您会注意到它只会访问 humor 标记(例如 http://quotes.toscrape.com/tag/humor)的网址。

你可以 在这里了解更多关于处理spider参数的信息

下一步

本教程仅介绍Scrapy的基础知识,但这里没有提到很多其他功能。检查 Scrapy一览 章节中的 还有什么? 部分,以便快速了解最重要的部分。

您可以继续从 基本概念 部分了解更多关于命令行工具,蜘蛛,选择器和教程没有涵盖的其他事情,如建模刮削的数据。如果您喜欢使用示例项目,请检查 例子 部分。