译者:OSGeo 中国

在本教程中,我们假定scrapy已经安装在您的系统上。如果还未安装,看 安装指南 .

我们将会爬 quotes.toscrape.com的网站, 这是一家列出著名作家的名言的网站。

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

  1. 创建新的Scrapy项目
  2. 写一篇 spider 对网站进行爬网并提取数据
  3. 使用命令行导出抓取的数据
  4. 将spider改为递归跟踪链接
  5. 使用 Spider 参数

Scrapy是用 Python 写的。如果您对这门语言不熟悉,您可能想从了解这门语言是什么开始,从 Scrapy 语言中得到最大的收获。

如果您已经熟悉其他语言,并且希望快速学习Python,那么 Python Tutorial 是一种很好的资源。

如果您是编程新手,并且想从python开始,那么下面的书可能对您有用:

您也可以看看 this list of Python resources for non-programmers 以及 suggested resources in the learnpython-subreddit .

创建项目

在开始抓取之前,您必须建立一个新的Scrapy项目。首先,进入您想要储存代码的目录,并运行以下代码:

  1. scrapy startproject tutorial

这将创建一个 tutorial 目录包含以下内容:

  1. tutorial/
  2. scrapy.cfg # deploy configuration file
  3. tutorial/ # project's Python module, you'll import your code from here
  4. __init__.py
  5. items.py # project items definition file
  6. middlewares.py # project middlewares file
  7. pipelines.py # project pipelines file
  8. settings.py # project settings file
  9. spiders/ # a directory where you'll later put your spiders
  10. __init__.py

我们的第一只 Spider

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

这是我们第一只 Spider 的代码。将其保存在 tutorial/spiders 目录下的 quotes_spider.py 文件里:

  1. import scrapy
  2. class QuotesSpider(scrapy.Spider):
  3. name = "quotes"
  4. def start_requests(self):
  5. urls = [
  6. 'http://quotes.toscrape.com/page/1/',
  7. 'http://quotes.toscrape.com/page/2/',
  8. ]
  9. for url in urls:
  10. yield scrapy.Request(url=url, callback=self.parse)
  11. def parse(self, response):
  12. page = response.url.split("/")[-2]
  13. filename = 'quotes-%s.html' % page
  14. with open(filename, 'wb') as f:
  15. f.write(response.body)
  16. self.log('Saved file %s' % filename)

如您所见,这是我们的 Spider scrapy.Spider 的子类。其属性和方法的定义如下:

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

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

  • parse() :将调用的方法,用于处理为每个请求下载的响应。响应参数是 TextResponse 的实例,它保存页面内容,并进一步处理它。
    这个 parse() 方法通常解析响应,将抓取的数据提取为dict,并查找新的URL以跟踪和创建新的请求。( Request 从他们那里。

如何运行我们的 Spider

要使 Spider 正常工作,请转到项目的顶级目录并运行:

  1. scrapy crawl quotes

此命令运行名为 quotes 的spider,这将发送一些请求至 quotes.toscrape.com 领域。您将得到类似于以下内容的输出:

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

现在,检查当前目录中的文件。您应该注意有两个的新文件已经被创建: quotes-1.htmlquotes-2.html。如 parse 方法所示,每个文件都含有URL。

注解

如果您想知道为什么我们还没有解析HTML,请稍等,我们很快就会讨论这个问题。

实际上发生了什么事?

Scrapy 安排被 Spider 里 start_requests 方法所返回的对象,也就是 scrapy.Request 。在接收到每个响应时,它实例化 Response 对象并调用与请求关联的回调方法(在本例中,为 parse 方法)将响应作为参数传递。

启动请求方法的快捷方式

而不是执行 start_requests() 生成的方法 scrapy.Request 来自URL的对象,您只需定义 start_urls 具有URL列表的类属性。然后,此列表将由 start_requests() 要为您的 Spider 创建初始请求,请执行以下操作:

  1. import scrapy
  2. class QuotesSpider(scrapy.Spider):
  3. name = "quotes"
  4. start_urls = [
  5. 'http://quotes.toscrape.com/page/1/',
  6. 'http://quotes.toscrape.com/page/2/',
  7. ]
  8. def parse(self, response):
  9. page = response.url.split("/")[-2]
  10. filename = 'quotes-%s.html' % page
  11. with open(filename, 'wb') as f:
  12. f.write(response.body)

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

提取数据

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

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

注解

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

在Windows上,使用双引号:

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

您将看到类似的内容:

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

您可以使用shell试试选择有 CSS 响应对象的元素:

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

运行 response.css('title') 的输出结果是一个列表形式的对象,即为 SelectorList 。这包含一组封装了XML/HTML元素的 Selector 对象,并允许您进行进一步的查询,以细化所选内容或提取数据。

要从以上标题中提取文本,可以执行以下操作:

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

这有两件事需要注意:一是我们添加 ::text 以查询CSS,意味着我们只要直接选择 <title> 元素内的文本元素。如果我们不指定 ::text ,我们将得到包含标签的完整title元素:

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

另外,调用 .getall() 的结果是个列表:一个选择器有可能回返回多个结果,所以我们提取所有结果。假如您只想得到第一个结果的话,您可以这样做:

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

您也可以这样写:

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

不过,当SelectorList 实例找不到任何符合的元素时,直接使用 .get() 可避免 IndexError 和返回 None

课堂小知识:对于大多数的抓取代码,当有找不到东西的时候,您会希望它能处理错误。这样的话即使抓取失败了,您至少可以得到一些些数据。

除了 getall()get() 方法,您也可以以 re() 方法使用 正则表达式提取数据:

  1. >>> response.css('title::text').re(r'Quotes.*')
  2. ['Quotes to Scrape']
  3. >>> response.css('title::text').re(r'Q\w+')
  4. ['Quotes']
  5. >>> response.css('title::text').re(r'(\w+) to (\w+)')
  6. ['Quotes', 'Scrape']

为了找到合适的CSS选择器,您也许会发现在您的浏览器里的shell使用view(response)打开相应页面会很有用 . 您可以使用浏览器内的带有选择器的开发者工具查看HTML(请参见关于 使用浏览器的开发人员工具进行抓取

Selector Gadget 工具可以快速找出CSS元素,其可支持多种浏览器。

xpath:简介

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

  1. >>> response.xpath('//title')
  2. [<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
  3. >>> response.xpath('//title/text()').get()
  4. 'Quotes to Scrape'

XPath表达式是非常强大的,也是抓取选择器的基础。实际上,CSS选择器其实是转换为XPath。Shell中的选择器对象所输出的文字也可以发现这一点。

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

这里我们不介绍太多的XPath,但是您可以阅读更多关于 以Scrapy选择器使用XPath . 要了解有关xpath的更多信息,我们建议 this tutorial to learn XPath through examplesthis tutorial to learn “how to think in XPath” .

提取名言(quote)和作者

既然您对选择和提取有了一些了解,那么让我们通过编写代码从网页中提取名言来完成 Spider 程序。

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

  1. <div class="quote">
  2. <span class="text">“The world as we have created it is a process of our
  3. thinking. It cannot be changed without changing our thinking.”</span>
  4. <span>
  5. by <small class="author">Albert Einstein</small>
  6. <a href="/author/Albert-Einstein">(about)</a>
  7. </span>
  8. <div class="tags">
  9. Tags:
  10. <a class="tag" href="/tag/change/page/1/">change</a>
  11. <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
  12. <a class="tag" href="/tag/thinking/page/1/">thinking</a>
  13. <a class="tag" href="/tag/world/page/1/">world</a>
  14. </div>
  15. </div>

让我们打开Scrapy Shell并了解如何提取所需数据:

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

我们得到了quote的HTML元素的选择器列表:

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

以上的查询返回的每个选择器都允许我们对其子元素运行进一步的查询。让我们将第一个选择器赋给一个变量,这样我们就可以直接在一个特定的quote上运行css选择器:

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

现在,让我们用刚刚创建的对象从使用 quote 的名言中提取 textauthor 以及 tags

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

鉴于标签是字符串列表,我们可以使用 .getall() 方法获取全部内容:

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

了解了如何提取每一位之后,我们现在可以迭代所有quote元素,并将它们放在一个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))
{'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
>>>

在 Spider 中提取数据

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

Scrapy Spider 通常会生成许多字典,其中包含从页面中提取的数据。为此,我们使用 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').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

如果运行这个spider,它将会输出提取的数据在日志中:

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 ,使用以下命令::

scrapy crawl quotes -o quotes.json

会产生一个 quotes.json 包含所有已擦除项的文件,在 JSON .

出于历史原因,scrapy会附加到给定的文件,而不是覆盖其内容。如果在第二次运行此命令两次之前不删除该文件,则最终会得到一个损坏的JSON文件。

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

scrapy crawl quotes -o quotes.jl

这个 JSON Lines 格式很有用,因为它类似于流,您可以很容易地向它附加新记录。当您运行两次时,它不存在相同的JSON问题。另外,由于每个记录都是单独的一行,因此您可以处理大文件,而不必将所有内容都放入内存中,因此有如下工具: JQ 以帮助在命令行中执行此操作。

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

以下链接

比如说,您不需要从http://quotes.toscrape.com的前两页抓取内容,而是需要从网站上所有页面的引用。

既然您知道了如何从页面中提取数据,那么让我们看看如何从页面中跟踪链接。

第一件事是提取到我们要跟踪的页面的链接。检查我们的页面,我们可以看到有一个链接指向下一个带有以下标记的页面:

<ul class="pager">
    <li class="next">
        <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
    </li>
</ul>

我们可以尝试在Shell中提取:

>>> response.css('li.next a').get()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'

这将获取anchor元素,但我们需要该属性 href . 为此,scrapy支持一个css扩展,让您选择属性内容,如下所示:

>>> response.css('li.next a::attr(href)').get()
'/page/2/'

还有一个 attrib 可用属性(请参见 选择元素属性 更多):

>>> response.css('li.next a').attrib['href']
'/page/2'

现在让我们看看我们的spider被修改为递归地跟踪下一页的链接,从中提取数据:

import scrapy

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

    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(),
            }

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

现在,在提取数据之后, parse() 方法查找到下一页的链接,并使用 urljoin() 方法(因为链接可以是相对的),并生成对下一页的新请求,将自身注册为回调,以处理下一页的数据提取,并保持爬行在所有页中进行。

这里您看到的是scrapy的以下链接机制:当您在回调方法中生成一个请求时,scrapy将计划发送该请求,并注册一个回调方法,以便在该请求完成时执行。

使用它,您可以构建复杂的爬虫程序,这些爬虫程序根据您定义的规则跟踪链接,并根据所访问的页面提取不同类型的数据。

在我们的示例中,它创建了一种循环,跟踪到下一页的所有链接,直到找不到一个为止——这对于爬行博客、论坛和其他带有分页的站点很方便。

创建请求的快捷方式

作为创建请求对象的快捷方式,您可以使用 response.follow ::

import scrapy

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

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

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

不像Scrapy.Request, response.follow 直接支持相对URL-无需调用URLJOIN。注意 response.follow 只返回一个请求实例;您仍然需要生成这个请求。

也可以将选择器传递给 response.follow 而不是字符串;此选择器应提取必要的属性:

for href in response.css('li.next a::attr(href)'):
    yield response.follow(href, callback=self.parse)

为了 &lt;a&gt; 元素有一个快捷方式: response.follow 自动使用其href属性。因此代码可以进一步缩短:

for a in response.css('li.next a'):
    yield response.follow(a, callback=self.parse)

注解

response.follow(response.css('li.next a')) 无效,因为 response.css 返回一个类似列表的对象,其中包含所有结果的选择器,而不是单个选择器。一 for 像上面例子中那样循环,或者 response.follow(response.css('li.next a')[0]) 很好。

更多示例和模式

下面是另一个spider,它演示回调和以下链接,这次是为了抓取作者信息:

import scrapy

class AuthorSpider(scrapy.Spider):
    name = 'author'

    start_urls = ['http://quotes.toscrape.com/']

    def parse(self, response):
        # follow links to author pages
        for href in response.css('.author + a::attr(href)'):
            yield response.follow(href, self.parse_author)

        # follow pagination links
        for href in response.css('li.next a::attr(href)'):
            yield response.follow(href, self.parse)

    def parse_author(self, response):
        def extract_with_css(query):
            return response.css(query).get(default='').strip()

        yield {
            'name': extract_with_css('h3.author-title::text'),
            'birthdate': extract_with_css('.author-born-date::text'),
            'bio': extract_with_css('.author-description::text'),
        }

这个 Spider 将从主页开始,它将跟踪所有指向作者页面的链接,调用 parse_author 它们的回调,以及与 parse 像我们以前看到的那样回拨。

这里,我们把回电传递给 response.follow 作为使代码更短的位置参数;它也适用于 scrapy.Request .

这个 parse_author 回调定义了一个助手函数,用于从CSS查询中提取和清理数据,并用作者数据生成python dict。

这个 Spider 展示的另一个有趣的事情是,即使同一作者引用了很多话,我们也不需要担心多次访问同一作者页面。默认情况下,scrappy过滤掉对已经访问过的URL的重复请求,避免了由于编程错误而太多地访问服务器的问题。这可以通过设置进行配置 DUPEFILTER_CLASS .

希望到目前为止,您已经很好地了解了如何使用scrappy跟踪链接和回调的机制。

作为另一个利用以下链接机制的 Spider 示例,请查看 CrawlSpider 类,该类用于实现一个小规则引擎,您可以使用该引擎在上面编写爬虫程序。

另外,一个常见的模式是使用来自多个页面的数据构建一个项目,使用 trick to pass additional data to the callbacks .

使用 Spider 参数

通过使用 -a 运行它们时的选项:

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 = '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').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)

如果您通过 tag=humor 对于这个 Spider ,您会注意到它只访问来自 humor 标记,如 http://quotes.toscrape.com/tag/humor .

您可以 learn more about handling spider arguments here .

下一步

本教程只介绍 Scrapy 的基础知识,但这里没有提到很多其他特性。检查 还有什么? 段在 Scrapy at a glance 第章快速概述最重要的部分。

您可以从该部分继续 基本概念 为了了解更多关于命令行工具、spider、选择器和其他一些本教程没有涉及的内容,比如对抓取的数据建模。如果您喜欢使用示例项目,请检查 实例 部分。