Scrapy教程

在本教程中,我们假设Scrapy已经安装在你的系统上。 如若不然,请参考 安装指南

我们打算爬取quotes.toscrape.com,一个列出名人名言的网站。

本篇教程中将带您完成下列任务:

  1. 创建一个Scrapy项目
  2. 编写一只spider爬取网站并提取数据
  3. 使用命令行导出爬取的数据
  4. 修改spider来递归地跟踪链接
  5. 使用spider的参数

Scrapy是用Python编写的。如果你是这个语言的新学者,你可能想通过获取该语言是什么样子来开始,以从Scrapy学到最多。

如果你已经熟悉其他语言,并且想要快速学习Python,我们推荐阅读Dive Into Python 3另外,你可以依照Python 教程

如果你是编程的新人,并要以Python 开始,你会发现有用的在线图书Learn Python The Hard Way你还可以看看这个给非程序员的Python资源列表

创建一个项目

在开始爬取之前,你必须创建一个新的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

我们的第一个Spider

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

这是我们第一个Spider的代码。将它保存在你的项目中的tutorial/spiders目录下的名为dmoz_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。在一个项目中,它必须是唯一的,也就是,你不能给不同的Spider设置相同的名称。

  • start_requests():必须返回Request的一个可迭代对象(你可以返回一个请求的列表或者编写一个生成器函数),Spider将从它们开始爬取。后续请求将从这些初始的请求连续生成。

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

    parse()方法通常解析response,提取爬取的数据为字典,找出跟踪的新URL并从它们创建新的请求(Request)。

如何运行我们的Spider

为了让我们的Spider工作,回到项目的顶层目录并运行︰

scrapy crawl quotes

此命令运行我们刚添加的名为quotes的Spider,它将发送一些请求给quotes.toscrape.com域。你将得到类似下面的输出︰

... (omitted for brevity)
2016-09-20 14:48:00 [scrapy] INFO: Spider opened
2016-09-20 14:48:00 [scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-09-20 14:48:00 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-09-20 14:48:00 [scrapy] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2016-09-20 14:48:00 [scrapy] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2016-09-20 14:48:01 [quotes] DEBUG: Saved file quotes-1.html
2016-09-20 14:48:01 [scrapy] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None)
2016-09-20 14:48:01 [quotes] DEBUG: Saved file quotes-2.html
2016-09-20 14:48:01 [scrapy] INFO: Closing spider (finished)
...

现在,检查当前目录中的文件。你应该注意到已创建两个新文件︰quotes-1.htmlquotes-2.html,内容分别对应我们parse方法指示的URL。

Note

如果你想知道为什么我们尚未解析HTML,坚持住,我们很快就将涵盖的。

底层刚刚发生了什么?

Scrapy调度这个Spider的start_requests方法返回的scrapy.Request对象。在接收到每个响应后,它实例化Response对象,并调用和将该Response作为参数传递给与请求关联的回调方法(在这种情况下是parse 方法)。

start_requests方法的快捷方式

你可以用一个URL列表只定义一个start_urls类属性,而不用实现一个从URL生成scrapyscrapy.Request对象的start_requests()方法。然后,start_requests()的默认实现将使用此列表来创建你的Spider的初始请求︰

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提取数据的最佳方法就是使用Scrapy shell来尝试Selector。运行︰

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

Note

记住,当从命令行运 Scrapy shell时,要始终将URL放入引号中,否则包含参数的URL(即&字符)将不能工作。

你将看到类似这样︰

[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy] 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的类似列表的对象,它表示一个Selector对象的列表,这些对象封装XML/HTML元素并允许你进一步查询细粒度的选择或提取数据。

从上面的标题中提取文本,你可以︰

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

这里有两件事要注意︰ 一是我们已经添加 ::text到 CSS 查询,意味着我们只想要选择直接在<title>元素内的文本元素。如果我们不指定::text,我们会得到完整的标题元素,包括其标记︰

>>> 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()方法提取,它利用正则表达式

>>> 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选择器来使用,你可以在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。如果你仔细查看shell中选择器对象的文本表示形式,你可以看到这点。

虽然也许不像CSS选择器受欢迎,XPath表达式提供更强大的能力,因为除了浏览结构,它也可以查看内容。使用XPath,你能够选择这样的东西︰选择包含文本"Next Page"的链接这使得XPath非常适于爬取任务,我们鼓励你学会XPath,即使你已经知道如何构建CSS选择器,它将使得爬取容易得多。

这里我们不会大量讲述XPath ,但你可以阅读更多有关Scrapy选择器使用XPath若要了解更多关于XPath,我们推荐这本教程来通过实例学习XPath这本教程来学习"如何在XPath中思考"

提取名言和作者

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

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
>>>

在我们的Spider中提取数据

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

Scrapy的Spider通常会生成许多包含从页面提取的数据的字典。要做到这一点,我们在回调函数中使用Python关键字yield,正如你可以看到的下面内容︰

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('span small::text').extract_first(),
                'tags': quote.css('div.tags a.tag::text').extract(),
            }

如果你运行这个Spider,它将输出提取的数据︰

2016-09-19 18:57:19 [scrapy] 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] 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 Pipelinetutorial/pipelines.py中,在项目创建的时候已经为Item Pipelines建立一个占位文件。然而你不需要实现任何item pipelines,如果你只想存储爬取的项目。

使用Spider的参数

当运行你的Spider时,你可以通过使用-a选项来提供命令行参数给它们︰

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

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

在此示例中,提供给tag参数的值,可通过self.tag访问。基于参数构建URL,你可以使用它来让你的Spider只获取带有一个特定标记的名言:

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('span small a::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参数给这个Spider,你会注意到它将只访问来自humor标记的URL,如http://quotes.toscrape.com/tag/humor

你可以 在这里了解有关处理Spider参数的更多内容

下一步

本教程涵盖了基本的Scrapy,但还有很多其他功能这里没有提到。查看What else?一节,位于Scrapy at a glance一章,以获得最重要信息的快速概览。

你可以继续基本概念部分以更多地了解命令行工具、Spider、 选择器和其它本教程没有涵盖的内容,如建模爬取的数据。如果你喜欢玩示例项目,请查看示例部分。