Scrapy学习一:Scrapy基本教程

Scrapy基本教程

这篇教程将分5点阐述Scrapy:

  1. 如何创建Scrapy project
  2. 如何写一个扒站并提取数据的Spider
  3. 使用命令行输出爬获的数据
  4. 如何递归地获取下一页
  5. 使用Spider参数

一、创建project

使用命令行:

scrapy startproject tutorial

就能创建一个project,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

在tutorial/spiders/下新建quotes_spider.py文件(quotes_spider为给定的Spider文件名称):

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)

这个py文件定义了如下 attributes 和 methods:

  • name: 指定Spider标识,这个属性是唯一的;
  • start_requests(): 返回一个iterable的 Requests(list 或 生成器函数),交由Spider开始爬。如果是生成器函数,用yield替代return;
  • parse(): 处理 requests 响应数据的函数,一般作为callback。响应参数是 TextResponse 的一个实例,。TextResponse 用于保存页面内容和其他有用methods。parse() 一般用于拆解响应,提取数据,保存在dict中;同时寻找新的URLs(如下一页),创建新的 request。

运行spider

在project最高层目录(本例中为tutorial\)运行:

 scrapy crawl quotes

查看目录,生成了quotes-1.html 和 quotes-2.html(你可能会好奇,html文档并没有被parse,这部分会在后半段讲)

发生了什么?

Scrapy 调度 Spider 的 start_requests() 方法返回的objects。根据接受到的每一个响应,实例化 Response objects 并调用parse() callback方法。

简便的start_requests()方法

上述py代码使用 start_requests() 函数从URLs生成 scrapy.Request objects。
另外,你也可以定义一个名为start_urls的class属性,赋予一个包含URLs的List。这个list将被默认的start_requests()用来创建initial 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)

注:
1. scrpay.Spider类下默认有start_requests()方法
2. 同样,由于parse()是Scrapy的默认callback函数,将自动地处理每一个requests for URLs。在上述py代码中我们没有使用start_requests()方法,所以没有定义callback=self.parse,但即便如此,parse()也会自行工作,无需在Spider中显式地指明。

三、在Scrapy shell中提取数据

学习如何提取数据最好的方法是使用Scrapy shell的Selector。运行:(注意:在Windows下最好使用双引号包扩URLs;不用引号可能导致一些错误)

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

你会看到类似这样的输出:

[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy.core.engine] DEBUG: Crawled (200) 1/> (referer: None)
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    0x7fa91d888c90>
[s]   item       {}
[s]   request    1/>
[s]   response   <200 http://quotes.toscrape.com/page/1/>
[s]   settings   0x7fa91d888c10>
[s]   spider     '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,你可以试着选用response object的CSS功能:

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

运行 response.css(‘title’) 会返回一个 list-like object,称为SelectorList。SelectorList是一个Selector objects的list,包含XML/HTML元素,允许你进行进一步精细查询或数据提取

要提取title中的text,你可以:

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

注意:
1.. 在CSS查询中添加::text意味着我们仅需要title中的text元素
2.. 如果不指明::text,我们获得的是完整的数据,包括tags,如下所示:

>>> response.css('title').extract()
['</span>Quotes to Scrape<span class="hljs-xmlDocTag">']

3.. 调用.extract()将返回一个list(而不是SelectorList)
4.. 当你只想获取list中的第一个结果,使用:

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

在Python中,我们可能会使用:

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

这是类似的,但.extract_first()定义了错误处理过程,避免了list中没有数据导致IndexError和返回None。在爬虫代码中出现error而使整个爬虫停止是非常致命的,使用.extract_first()获得了错误恢复能力。
当然你也可以自己写错误处理过程。

除了 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 selector,你可能要在浏览器的shell中使用 view(response) 打开响应页面查找有用信息。可以使用浏览器内置的开发者工具或Firefox的Firebug工具。

Selector Gadget 是一个有用的插件,能快速找到CSS selector并提供可视的selected elements,可在大部分浏览器工作。

XPath表达式简介

除了CSS,Scrapy selectors也提供XPath表达式:

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

XPath表达式非常强大,它是Scrapy Selectors的基本构成。并且事实上,先有XPath后有CSS selectors,每个CSS selectors都会在后台被转化为XPath。You can see that if you read closely the text representation of the selector objects in the shell.

While perhaps not as popular as CSS selectors, XPath expressions offer more power because besides navigating the structure, it can also look at the content. Using XPath, you’re able to select things like: select the link that contains the text “Next Page”. This makes XPath very fitting to the task of scraping, and we encourage you to learn XPath even if you already know how to construct CSS selectors, it will make scraping much easier.

但这篇文章不会讲XPath,你可以在 selector,XPath案例,how to think in XPath 读到更多关于XPath的内容。

提取quotes and authors

现在你了解了selection and extraction,我们将完成Spider的提取部分。

每个quote in http://quotes.toscrape.com 由HTML elements表示:

<div class="quote">
    "text">“The world as we have created it is a process of our
    thinking. It cannot be changed without changing our thinking.”
    
        by "author">Albert Einstein
        <a href="/author/Albert-Einstein">(about)a>
    
    <div class="tags">
        Tags:
        <a class="tag" href="/tag/change/page/1/">changea>
        <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughtsa>
        <a class="tag" href="/tag/thinking/page/1/">thinkinga>
        <a class="tag" href="/tag/world/page/1/">worlda>
    div>
div>

打开scrapy shell,我们来看看如何提取需要的数据:(如果出错记得Windows下要用双引号)

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

再通过下面命令获得list of selectors for the quote HTML elements:

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

Each of the selectors returned by the query above allows us to run further queries over their sub-elements. Let’s assign the first selector to a variable, so that we can run our CSS selectors directly on a particular quote:

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

Now, let’s extract title, author and the tags from that quote using the quote object we just created:

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

Given that the tags are a list of strings, we can use the .extract() method to get all of them:

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

已经了解了如何提取每个bit,我们现在开始iterate所有quotes的elements,并将他们放进Python字典结构中print出来:

>>> 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页面保存到了本地。现在是时候把在shell中提取数据的逻辑应用到我们的Spider中了。

一个典型的Scrapy Spider会生成许多包含提取到的数据的字典结构。我们使用Python中的yield关键字(取代return)来回调。来看下面的py代码:

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

改写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默认会扩展而不是覆盖(’a’, not ‘w’)该文件,所以当你运行同样的命令2次并且忘记删除第一个文件,2个JSON结构将导致该JSON文件格式错误。

你也可以输出其他格式,例如JSON Lines:

scrapy crawl quotes -o quotes.jl

JSON Lines格式的一大优点是stream-like,不会发生上述json在2次扩展中导致的错误。你可以向文件中轻易地扩展数据记录;同时,由于各行记录无关,在处理大文件时不需要预读一大块内存。此外还有类似JQ的工具来辅助处理命令行。

在小型项目中,仅仅需要保存抓获的数据,这些就足够了。如果项目变大变复杂,你需要写一个Item Pipeline 来与其他部分进行通信。当项目创建时Item Pipeline文件已经生成了,本例中在tutorial/pipelines.py。小型项目里不需要改动它。

四、下一页链接

注:为了方便理解,在这章我把标题称为下一页链接。事实上原文称作后续链接,下一页链接只是后续链接的一种。只是本章内容完全是在描述下一页链接。

很明显,大部分时间你想要爬取网站所有页面而不是局限于网站的前几页。我们已经知道如何提取一个页面的数据,接下来我们看看如何处理下一页链接。

首先我们需要知道哪些链接是我们要跟踪的。一个网站有下一页链接,但很可能还有友站链接等不需要爬虫进行爬取的链接。所以检查页面,我们可以发现下一页链接的标记:

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

我们在shell中提取它:

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

获得了anchor element(锚元素),但我们需要的是attribute href(属性:href)。为达到这个目的,Scrapy支持一个CSS的扩展 a::attr(href) 来选定attribute的内容:

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

现在改写Spider中的parse()方法来递归获取下一页数据:

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一个新request到下一页,注册自己为callback函数,为下一页提供数据提取并保持这一过程直至遍历所有页面。

综上,Scrapy的下一页链接处理机制为:当你在callback函数中yield一个request,Scrapy将调度这个request,发送这个request,并注册一个callback方法给request,当request完成后,将执行callback。

你可以通过构建自定义的后续规则来实现复杂的爬虫,并根据不同页面提取不同类型的数据

在本例中,建立了一系列递归,下一页又下一页,直到没有下一页。

创建Requests的简便方法

创建request对象的便捷方法就是使用 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:
            #next_page = response.urljoin(next_page)
            #yield scrapy.Request(next_page, callback=self.parse)
            yield response.follow(next_page, callback=self.parse)

注释部分保留了 scrapy.Request() 方法,对比之下,response.follow() 方法支持直接使用相对URL,所以不需要进行 response.urljoin() 。但注意到 response.follow() 方法返回一个request实例,所以还是需要yield。

你也可以传递一个 selector 给 response.follow(),而这个 selector 必须能提取必要的属性:

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

For elements there is a shortcut: response.follow uses their href attribute automatically. So the code can be shortened further:
对于元素,有一个简短的方式:response.follow() 会自动使用href属性。所以只需要传递a:

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

更多示例和模式

这里有另一个示例来说明回调函数和后续链接,这次抓取的是 author 信息:

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   #pagination 标记页码
        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'),
        }

Spider从主页开始,使用 parse() 跟踪所有后续链接,跟踪到作者页后执行 parse_author() 回调,跟踪到下一页后执行 parse() 回调。

本例中,为了使代码更简洁,我们把2个回调函数作为positional arguments传递给 response.follow() ;scrapy.Request() 也可以如此使用。
注:Python中一共包含下列几种arguments:named,default,positional。它们的优先级依次递减。
扩展阅读:positional arguments vs others: https://stackoverflow.com/questions/9450656/positional-argument-v-s-keyword-argument

parse_author() 回调函数定义了一个辅助函数 extract_with_css() 来提取并清洗数据,该函数从CSS请求中读取数据并yield一个包含作者数据的Python字典结构。

本例另一个特点是,即使同一作者被引用了很多次,Scrapy将默认去除重复页面请求,避免类似的冗余操作。这部分配置在DUPEFILTER_CLASS中。

希望现在你对如何使用Scrapy的后续链接和回调函数机制有了更深的了解。

As yet another example spider that leverages the mechanism of following links, check out the CrawlSpider class for a generic spider that implements a small rules engine that you can use to write your crawlers on top of it.

Also, a common pattern is to build an item with data from more than one page, using a trick to pass additional data to the callbacks.

五、使用spider参数

你可以用命令行参数带 -a 来实现更多spider功能。这些参数将传递给 init() 方法,并默认成为spider属性:

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

在本例中,传递了一个a tag=humor,spider将其初始化到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 传递给这个spider,它就只会访问带有humor tag的URL,例如 http://quotes.toscrape.com/tag/humor

下一步学习

Scrapy基础教程到这里就结束了。接下来你可以从命令行使用,spider编写,selector等维度深入学习下去,也可以直接由 示例 入手,以战代练。

原始文档链接:https://docs.scrapy.org/en/latest/intro/tutorial.html

你可能感兴趣的:(python)