# 4.0.爬虫快速入门

## Scrapy教程

在本教程中，我们假设您的系统上已经安装了Scrapy。如果未安装Scrapy，请参阅[安装指南](https://docs.scrapy.org/en/latest/intro/install.html#intro-install)。

我们将爬取[quotes.toscrape.com](http://quotes.toscrape.com/)网站，该网站列出了著名作家的名言。

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

1. 创建一个新的Scrapy项目
2. 编写spider ，爬取网站并提取数据
3. 使用命令行导出抓取的数据
4. 更改spider 以递归地爬取链接
5. 使用spider 参数

## 创建一个新的Scrapy项目

在开始爬取网站之前，您将必须建立一个新的Scrapy项目。进入你将存储你代码的地方并运行：

```bash
scrapy startproject tutorial
```

这将创建一个名为tutorial的目录，其包含以下内容：

```bash
tutorial/
    scrapy.cfg            # deploy 配置文件

    tutorial/             # 项目的 Python 模块,  you'll import your code from here
        __init__.py

        items.py          # 项目的 items 定义文件

        middlewares.py    # 项目的 middlewares 文件

        pipelines.py      # 项目的 pipelines 文件

        settings.py       # 项目的 settings 文件

        spiders/          # 一个目录，您稍后将在其中放置spider 
            __init__.py
```

## 我们的第一个Spider

Spiders 是您定义的类，Scrapy使用其从网站（或一组网站）中获取信息。它们必须是`Spider`类的子类并定义了初始请求，同时你可以选择性定义，如何跟随页面中的链接，以及如何解析下载的页面内容。

这是我们第一个Spider的代码。将其保存在项目中tutorial / spiders目录下，命名为quotes\_spider.py：

```python
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 = f'quotes-{page}.html'
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log(f'Saved file {filename}')
```

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

* [`name`](https://docs.scrapy.org/en/latest/topics/spiders.html#scrapy.spiders.Spider.name): sprider的标识符。它在一个项目中必须唯一，也就是说，您不能为不同的Spider设置相同的名称。
* [`start_requests()`](https://docs.scrapy.org/en/latest/topics/spiders.html#scrapy.spiders.Spider.start_requests): 必须返回一个可迭代的请求列表（您可以返回一个请求列表或编写一个列表生成器函数），该请求列表用于告诉Spider从哪里开始爬取。随后的请求将从这些初始请求中依次生成。
* [`parse()`](https://docs.scrapy.org/en/latest/topics/spiders.html#scrapy.spiders.Spider.parse): 一个将被调用以处理每个请求响应的方法。`response`参数是TextResponse的一个实例，该实例保存着响应页面内容并具有很多用于处理响应的方法。

  parse()方法通常用于解析响应，提取数据为字典格式，发现将要爬取的新URL并创建新的`Request`对象。

## 如何运行我们的spider

要使我们的spider工作，请转到项目的顶级目录并运行：

```bash
scrapy crawl quotes
```

此命令运行我们刚刚添加的名为quotes的spider，它将发送一些前往quotes.toscrape.com的请求。您将获得类似于以下的输出：

```bash
... (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.html和quotes-2.html，文件中包含与URL对应的内容，正如我们parse方法中所指示的那样。

## start\_requests方法的快捷方式

在之前的小节中，我们通过实现 [`start_requests()`](https://docs.scrapy.org/en/latest/topics/spiders.html#scrapy.spiders.Spider.start_requests) 方法，从URL列表中生成[`scrapy.Request`](https://docs.scrapy.org/en/latest/topics/request-response.html#scrapy.http.Request)对象。除此之外，你也可以只定义一个[`start_urls`](https://docs.scrapy.org/en/latest/topics/spiders.html#scrapy.spiders.Spider.start_urls)类属性，值为一个URL列表。该列表将被`start_requests()`的默认实现使用并为你的spider创建初始请求：

```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):
        page = response.url.split("/")[-2]
        filename = f'quotes-{page}.html'
        with open(filename, 'wb') as f:
            f.write(response.body)
```

## 提取数据

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

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

> 注意：通过命令行运行Scrapy shell时，请记住始终将网址括在引号中，否则包含参数（即＆字符）的网址将不起作用。
>
> 在Windows上，请使用双引号代替：
>
> ```bash
> scrapy shell "http://quotes.toscrape.com/page/1/"
> ```

您将看到类似以下内容：

```bash
[ ... 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选择响应中的元素：

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

运行response.css('title')将返回一个类似列表的SelectorList对象，它是Selector对象的集合，Selector对象包裹XML / HTML元素并允许您运行进一步的查询以细化选择内容或提取数据。

要从上面的title中提取文本，您可以执行以下操作：

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

这里有两点需要注意：一是我们在CSS查询中添加了 `::text` ，这意味着我们只想选择`<title>`元素的text元素。如果不指定`::text` ，则会获得完整的title元素，包括其标签：

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

另一件事是，调用 `.getall()` 的结果是一个列表：选择器有可能返回多个结果，因此我们将它们全部提取出来。当您只需要第一个结果时，在这种情况下，您可以执行以下操作：

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

或者，您可以执行：

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

但是，直接在SelectorList实例上使用`.get()`可以避免IndexError，并且在找不到与所选内容匹配的任何元素时返回None。

在这里我们可以上一课：对于大多数抓取代码，您希望它能够抵抗由于页面上找不到内容而导致的错误，因此即使某些部分未能被抓取，您也至少可以获取一些数据。

除了 [`getall()`](https://docs.scrapy.org/en/latest/topics/selectors.html#scrapy.selector.SelectorList.getall) 和 [`get()`](https://docs.scrapy.org/en/latest/topics/selectors.html#scrapy.selector.SelectorList.get) 方法之外，您还可以通过 [`re()`](https://docs.scrapy.org/en/latest/topics/selectors.html#scrapy.selector.SelectorList.re) 方法使用正则表达式进行提取：

```bash
>>> 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)`从shell中打开Web浏览器。您可以使用浏览器的开发者工具检查HTML并获得一个选择器（请参阅 [Using your browser’s Developer Tools for scraping](https://docs.scrapy.org/en/latest/topics/developer-tools.html#topics-developer-tools)）

Selector Gadget是一个不错的工具，可以快速发现CSS选择器，Selector Gadget可在许多浏览器中使用。

## XPath：简要介绍

除了 [CSS](https://www.w3.org/TR/selectors)，Scrapy选择器还支持使用 [XPath](https://www.w3.org/TR/xpath/all/)表达式：

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

XPath表达式非常强大，并且是Scrapy Selectors的基础。实际上，CSS选择器是在后台将转换为XPath。

尽管XPath表达式可能不如CSS选择器流行，但它提供了更多功能，因为除了navigating the structure之外，它还可以查看内容。使用XPath，您可以选择类似下面的内容：选择包含文本“Next Page”的链接。这使XPath非常适合于抓取任务，即使您已经知道如何构造CSS选择器，我们也鼓励您学习XPath，这将使抓取更加容易。

我们不会在这里介绍XPath，但是您可以阅读[using XPath with Scrapy Selectors here](https://docs.scrapy.org/en/latest/topics/selectors.html#topics-selectors)。要了解有关XPath的更多信息，我们建议你阅读 [this tutorial to learn XPath through examples](http://zvon.org/comp/r/tut-XPath_1.html) 和[this tutorial to learn “how to think in XPath”](http://plasmasturm.org/log/xpath101/).

## 提取名言和作者

现在您对选择和提取有所了解，让我们通过编写代码从网页中提取名言。

[http://quotes.toscrape.com中的每个引号都由如下所示的HTML元素表示：](https://moluo.gitbook.io/notes/bian-cheng-xue-xi/python/scrapy/http:/quotes.toscrape.com中的每个引号都由如下所示的HTML元素表示：)

```markup
<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，看看如何提取所需的数据：

```bash
$ scrapy shell 'http://quotes.toscrape.com'
```

我们获得带有quete HTML元素的selectors 列表，其中包括：

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

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

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

现在，让我们使用刚刚创建的`quote`对象从中提取`text`, `author`和`tags`：

```bash
>>> 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'
```

鉴于`tags`是字符串列表，我们可以使用`.getall()`方法来获取所有标签：

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

在弄清楚如何提取每一个quote之后，我们现在可以遍历所有quotes元素并将它们放到Python字典中：

```bash
>>> 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']}
...
```

## 在我们的spider中提取数据

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

一个Scrapy spider通常会生成许多字典，其中包含从页面提取的数据。为此，我们在回调中使用`yield`Python关键字，如下所示：

```bash
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').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }
```

如果运行此spider，它将输出提取的数据：

```bash
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.”"}
```

## 存储抓取的数据

存储抓取数据的最简单方法是使用[Feed exports](https://docs.scrapy.org/en/latest/topics/feed-exports.html#topics-feed-exports)，命令如下：

```bash
scrapy crawl quotes -O quotes.json
```

这将生成一个`quotes.json`文件，其中包含所有抓取的item, 并以JSON的方式显示。

`-O` 参数将覆盖现有文件； `-o` 参数可以将新内容追加至现有文件。但是，追加到JSON文件会使文件内容成为无效JSON。当使用追加操作时，请考虑使用其他序列化格式，例如 [JSON Lines](http://jsonlines.org/)：

```bash
scrapy crawl quotes -o quotes.jl
```

[JSON Lines](http://jsonlines.org/) 格式很有用，因为它像流一样，您可以轻松地向其添加新记录。而不会当您运行两次时遇到与JSON相同的问题。此外，由于每条记录都是单独的一行，因此您可以处理大文件而不必将所有内容都放入内存中，因此有类似 [JQ](https://stedolan.github.io/jq) 的工具可以在命令行中帮助您完成此操作。

在小型项目中（例如本教程中的项目），这应该足够了。但是，如果要对scraped items执行更复杂的操作，你可以编写 [Item Pipeline](https://docs.scrapy.org/en/latest/topics/item-pipeline.html#topics-item-pipeline)。创建项目时，已在 `tutorial/pipelines.py`中为您设置了Item Pipelines的占位符文件。如果您只想存储scraped items，则无需实现任何item pipelines。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://moluo.gitbook.io/notes/bian-cheng-xue-xi/python/scrapy/4.0.-pa-chong-kuai-su-ru-men.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
