Scrapy 1.4 文档 03 Scrapy 教程

在本教程中,咱们假设您已经安装了Scrapy。若是没有,请参阅安装指南css

咱们将要抓取 quotes.toscrape.com,一个列出著名做家的名言(quote)的网站。html

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

  1. 建立一个新的 Scrapy 项目
  2. 编写一个爬虫来爬取站点并提取数据
  3. 使用命令行导出抓取的数据
  4. 改写爬虫以递归地跟踪连接
  5. 使用爬虫参数

Scrapy 是用 Python 编写的。若是你没学过 Python,你可能须要了解一下这个语言,以充分利用 Scrapy。程序员

若是您已经熟悉其余语言,并但愿快速学习 Python,咱们建议您阅读 Dive Into Python 3。或者,您能够学习 Python 教程正则表达式

若是您刚开始编程,并但愿从 Python 开始,在线电子书《Learn Python The Hard Way》很是有用。您也能够查看非程序员的 Python 资源列表shell

建立一个项目

在开始抓取以前,您必须建立一个新的 Scrapy 项目。 进入您要存储代码的目录,而后运行:编程

scrapy startproject tutorial

这将建立一个包含如下内容的 tutorial 目录:json

tutorial/
    scrapy.cfg            # 项目配置文件
    tutorial/             # 项目的 Python 模块,放置您的代码的地方
        __init__.py
        items.py          # 项目项(item)定义文件
        pipelines.py      # 项目管道(piplines)文件
        settings.py       # 项目设置文件
        spiders/          # 一个你之后会放置 spider 的目录
            __init__.py

第一个爬虫

Spider 是您定义的类,Scrapy 用它从网站(或一组网站)中抓取信息。 他们必须是 scrapy.Spider 的子类并定义初始请求,和如何获取要继续抓取的页面的连接,以及如何解析下载的页面来提取数据。api

这是咱们第一个爬虫的代码。 将其保存在项目中的 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。 它在项目中必须是惟一的,也就是说,您不能为不一样的 Spider 设置相同的名称。
  • start_requests():必须返回一个 Requests 的迭代(您能够返回一个 requests 列表或者写一个生成器函数),Spider 将从这里开始抓取。 随后的请求将从这些初始请求连续生成。
  • 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.html 和 quotes-2.html,其中包含各个URL的内容,就像咱们的 parse 方法指示同样。

注意

若是您想知道为何咱们尚未解析 HTML,请继续,咱们将尽快介绍。

这个过程当中发生了什么?

Spider 的 start_requests 方法返回 scrapy.Request 对象,Scrapy 对其发起请求 。而后将收到的响应实例化为 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)

Scrapy 将调用 parse() 方法来处理每一个 URL 的请求,即便咱们没有明确告诉 Scrapy 这样作。 这是由于 parse() 是 Scrapy 的默认回调方法,没有明确分配回调方法的请求默认调用此方法。

提取数据

学习如何使用 Scrapy 提取数据的最佳方式是在 Scrapy shell 中尝试一下选择器。 运行:

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

注意

在从命令行运行 Scrapy shell 时必须给 url 加上引号,不然包含参数(例如 &符号)的 url 将不起做用。

在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 对象的列表,容许您进行进一步的查询来细分选择或提取数据。

要从上面的 title 中提取文本,您能够执行如下操做:

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

这里有两件事情要注意:一个是咱们在 CSS 查询中添加了 ::text,这意味着咱们只想要 <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() 返回 None,避免了 IndexError

这里有一个教训:对于大多数爬虫代码,您但愿它具备容错性,若是在页面上找不到指定的元素致使没法获取某些项,至少其它的数据能够被抓取。

除了 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 选择器语句,您能够在浏览器中打开页面并查看源代码。 您也可使用浏览器的开发工具或扩展(如 Firebug)(请参阅有关 Using Firebug for scrapingUsing Firefox for scraping 的部分)。

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。 

虽然也许不像 CSS 选择器那么受欢迎,但 XPath 表达式提供更多的功能,由于除了导航结构以外,它还能够查看内容。 使用 XPath,您能够选择如下内容:包含文本“下一页”的连接。 这使得 XPath 很是适合抓取任务,咱们鼓励您学习 XPath,即便您已经知道如何使用 CSS 选择器,这会使抓取更容易。

咱们不会在这里讲太多关于 XPath 的内容,但您能够阅读 using XPath with Scrapy Selectors 获取更多有关 XPath 的信息。 咱们推荐教程 to learn XPath through examples,和教程 “how to think in XPath”

提取名人和名言

如今你知道了如何选择和提取,让咱们来完成咱们的爬虫,编写代码从网页中提取名言(quote)。

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'

获得 quote 元素的 selector 列表:

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

经过上述查询返回的每一个 selector 容许咱们对其子元素运行进一步的查询。 让咱们将第一个 selector 分配给一个变量,以便咱们能够直接在特定的 quote 上运行咱们的 CSS 选择器:

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

如今,咱们使用刚刚建立的 quote 对象,从该 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']

如今已经弄清楚了如何提取每个信息,接下来遍历全部 quote 元素,并把它们放在一个 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.”"}

存储抓取的数据

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

scrapy crawl quotes -o quotes.json

这将生成一个 quotes.json 文件,其中包含全部抓取到的 JSON 序列化的数据。

因为历史缘由,Scrapy 追加内容到给定的文件,而不是覆盖其内容。 若是您在第二次以前删除该文件两次运行此命令,那么最终会出现一个破坏的 JSON 文件。您还可使用其余格式,如 JSON 行(JSON Lines):

scrapy crawl quotes -o quotes.jl

JSON 行格式颇有用,由于它像流同样,您能够轻松地将新记录附加到文件。 当运行两次时,它不会发生 JSON 那样的问题。 另外,因为每条记录都是单独的行,因此您在处理大文件时无需将全部内容放到内存中,还有 JQ 等工具能够帮助您在命令行中执行此操做。

在小项目(如本教程中的一个)中,这应该是足够的。 可是,若是要使用已抓取的项目执行更复杂的操做,则能够编写项目管道(Item Pipeline)。 在工程的建立过程当中已经为您建立了项目管道的占位符文件 tutorial/pipelines.py 虽然您只须要存储已抓取的项目,不须要任何项目管道。

跟踪连接

或许你但愿获取网站全部页面的 quotes,而不是从 http://quotes.toscrape.com 的前两页抓取。

如今您已经知道如何从页面中提取数据,咱们来看看如何跟踪连接。

首先是提取咱们想要跟踪的页面的连接。 检查咱们的页面,咱们能够看到连接到下一个页面的URL在下面的元素中:

<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').extract_first()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'

这获得了超连接元素,可是咱们须要其属性 href。 为此,Scrapy 支持 CSS 扩展,您能够选择属性内容,以下所示:

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

如今修改咱们的爬虫,改成递归地跟踪下一页的连接,从中提取数据:

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

        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, callback=self.parse)

如今,在提取数据以后,parse() 方法查找到下一页的连接,使用 urljoin() 方法构建一个完整的绝对 URL(由于连接能够是相对的),并生成(yield)一个到下一页的新的请求, 其中包括回调方法(parse)。

您在这里看到的是 Scrapy 的连接跟踪机制:当您在一个回调方法中生成(yield)请求(request)时,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').extract_first(),
                'author': quote.css('span small::text').extract_first(),
                'tags': quote.css('div.tags a.tag::text').extract(),
            }

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

不像 scrapy.Request,response.follow 支持相对 URL - 不须要调用urljoin。请注意,response.follow 只是返回一个 Request 实例,您仍然须要生成请求(yield request)。

您也能够将选择器传递给 response.follow,该选择器应该提取必要的属性:

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

对于<a>元素,有一个快捷方式: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]) 则能够正常工做。

更多的例子和模式

这是另一个爬虫,示例了回调和跟踪连接,此次是为了抓取做者信息:

import scrapy

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

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

    def parse(self, response):
        # 连接到做者页面
        for href in response.css('.author + a::attr(href)'):
            yield response.follow(href, self.parse_author)

        # 连接到下一页
        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).extract_first().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'),
        }

这个爬虫将从主页面开始, 以 parse_author 回调方法跟踪全部到做者页面的连接,以 parse 回调方法跟踪其它页面。

这里咱们将回调方法做为参数直接传递给 response.follow,这样代码更短,也能够传递给 scrapy.Request

parse_author 回调方法里定义了另一个函数来根据 CSS 查询语句(query)来提取数据,而后生成包含做者数据的 Python 字典。

这个爬虫演示的另外一个有趣的事是,即便同一做者有许多名言,咱们也不用担忧屡次访问同一做者的页面。默认状况下,Scrapy 会将重复的请求过滤出来,避免了因为编程错误而致使的重复服务器的问题。能够经过 DUPEFILTER_CLASS 进行相关的设置。

但愿如今您已经了解了 Scrapy 的跟踪连接和回调方法机制。

CrawlSpider 类是一个小规模的通用爬虫引擎,只须要修改其跟踪连接的机制等,就能够在它之上实现你本身的爬虫程序。

另外,一个常见的模式是从多个页面据构建一个包含数据的项(item),有一个将附加数据传递给回调方法的技巧。

使用爬虫参数

在运行爬虫时,能够经过 -a 选项为您的爬虫提供命令行参数:

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

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

在此示例中,经过 self.tag 获取命令行中参数 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:
            yield response.follow(next_page, self.parse)

若是您将 tag = humor 传递给爬虫,您会注意到它只会访问标签为 humor 的 URL,例如 http://quotes.toscrape.com/tag/humor。您能够在这里了解更多关于爬虫参数的信息。

下一步

本教程仅涵盖了 Scrapy 的基础知识,还有不少其余功能未在此说起。 查看初窥 Scrapy 中的“还有什么?”部分能够快速了解有哪些重要的内容。

您能够经过目录了解更多有关命令行工具、爬虫、选择器以及本教程未涵盖的其余内容的信息。下一章是示例项目。

相关文章
相关标签/搜索