五千字长文带你入门Scrapy - Scrapy简明教程

本文通过示例简要介绍一下使用Scrapy抓取网站内容的基本方法和流程。

继续阅读之前请确保已安装了scrapy。

基本安装方法为:pip install scrapy

我们已经在之前的文章中初步介绍了scrapy,本文是前文的进一步拓展。

本文主要包含如下几部分:

1,创建一个scrapy项目

2,编写一个爬虫(或蜘蛛spider,本文中含义相同)类用于爬取网站页面并提取数据

3,使用命令行导出爬到的数据

4,递归地爬取子页面

5,了解并使用spider支持的参数

我们测试的网站为quotes.toscrape.com,这是一个收录名人警句的站点。Let's go!

创建爬虫项目

Scrapy将爬虫代码各模块及配置组织为一个项目。Scrapy安装成功后,你可以在shell中使用如下命令创建一个新的项目:

scrapy startproject tutorial

这将会创建一个tutorial目录,该目录的文件结构如下所示:

编写蜘蛛类

Spiders是Scrapy中需要定义的实现爬取功能的类。

每个spider继承自Spider基类。

spider主要定义了一些起始url,并负责解析web页面元素,从中提前所需数据。

也可以产生新的url访问请求。

下边这段代码就是我们所定义的spider,将其保存为quotes_spider.py,放在项目的tutorial/spiders/目录下。

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) 

在我们的代码中,QuotesSpider继承自scrapy.Spider,并定义了一些属性和方法:

name:用于在项目中唯一标识一个spider。项目中可以包含多个spider,其name必须唯一。

start_requests():用于产生初始的url,爬虫从这些页面开始爬行。

        这个函数需要返回一个包含Request对象的iterable,可以是一个列表(list)或者一个生成器(generator)。我们的例子中返回的是一个生成器。

parse():是一个回调函数,用于解析访问url得到的页面.

    参数response包含了页面的详细内容,并提供了诸多从页面中提取数据的方法。

    我们通常在parse中将提取的数据封装为dict,查找新的url,并为这些url产生新的Request,以继续爬取。


运行蜘蛛

Spider定义好了之后,我们可以在项目的顶层目录,即最顶层的tutorial,执行如下命令来运行这个spider:

scrapy crawl quotes

这个命令会在项目的spiders目录中查找并运行name为quotes的Spider.

它会向quotes.toscrape.com这个网站发起HTTP请求,并获取如下响应:

...

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)

...

这些输出告诉我们,爬虫已成功访问了一些url,并将其内容保存为html文件。

这正是我们在parse()中定义的功能。

底层执行逻辑

Scrapy统一调度由spider的start_requests()方法产生的Request。

每当Request请求完成之后,Scrapy便创建一个与之相应的Response,并将这个Response作为参数传递给Request关联的回调函数(callback),由回调函数来解析这个web响应页面,从中提取数据,或发起新的http请求。

这个流程由Scrapy内部实现,我们只需要在spider中定义好需要访问的url,以及如何处理页面响应就行了。

start_requests的简写

除了使用start_requests()产生请求对象Request之外,我们还可以使用一个简单的方法来生成Request。

那就是在spider中定义一个start_urls列表,将开始时需要访问的url放置其中。如下所示:

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)

实际上,spider仍然会去调用默认的start_requests()方法,在这个方法里读取start_urls,并生成Request。

这个简版的请求初始化方法也没有显式地将回调函数parse和Request对象关联。

很容易想到,scrapy内部为我们做了关联:parse是scrapy默认的Request回调函数。

数据提取

我们得到页面响应后,最重要的工作就是如何从中提取数据。

这里先介绍一下Scrapy shell这个工具.

它是scrapy内置的一个调试器,可以方便地拉取一个页面,测试数据提取方法是否可行。

scrapy shell的执行方法为:

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

直接在后面加上要调试页面的url就行了,注意需要用引号包括url。

回车后会得到如下输出:

我们接下来就可以在shell中测试如何提取页面元素了。

可以使用Response.css()方法来选取页面元素:

>>> response.css('title')

[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]

css()返回结果是一个selector列表,每个selector都是对页面元素是封装,它提供了一些用于获取元素数据的方法。

我们可以通过如下方法获取html title的内容:

>>> response.css('title::text').getall()

['Quotes to Scrape']

这里,我们在css查询中向title添加了::text,其含义是只获取<title>标签中的文本,而不是整个<title>标签:

>>> response.css('title').getall()

['<title>Quotes to Scrape</title>']

不加::text就是上边这个效果。

另外,getall()返回的是一个列表,这是由于通过css选取的元素可能是多个。

如果只想获取第一个,可以用get():

>>> response.css('title::text').get()

'Quotes to Scrape'

还可以通过下标引用css返回的某个selector:

>>> response.css('title::text')[0].get()

'Quotes to Scrape'

如果css选择器没有匹配到页面元素,get()会返回None。

除了get()和getall(),我们还可以使用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选择器。

常用的方法是借助于浏览器的开发者工具进行分析。在chrome中可以通过F12打开开发者工具。

XPath简介

除了css,Scrapy还支持使用XPath来选取页面元素

>>> response.xpath('//title')

[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]

>>> response.xpath('//title/text()').get()

'Quotes to Scrape'

XPath表达式功能强大,它是Scrapy中选择器实现的基础,css在scrapy底层也会转换为XPath。

相较于css选择器,XPath不仅能解析页面结构,还可以读取元素内容。

可以通过XPath方便地获取到页面上“下一页”这样的url,很适于爬虫这种场景。

我们会在后续的Scrapy选取器相关内容进一步了解其用法,当然网上也有很多这方面的资料可供查阅。

提取警句和作者

通过上边的介绍,我们已经初步了解了如何选取页面元素,如何提取数据。

接下来继续完善这个spider,我们将从测试站点页面获取更多信息。

打开http://quotes.toscrape.com/,在开发者工具中查看单条警句的源码如下所示:

<divclass="quote">

    <spanclass="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<smallclass="author">Albert Einstein</small><ahref="/author/Albert-Einstein">(about)</a></span>

<divclass="tags">Tags:

    <aclass="tag"href="/tag/change/page/1/">change</a>

    <aclass="tag"href="/tag/deep-thoughts/page/1/">deep-thoughts</a>    

    <aclass="tag"href="/tag/thinking/page/1/">thinking</a>

    <aclass="tag"href="/tag/world/page/1/">world</a>

    </div>

</div>

现在我们打开scrapy shell来测试一下如何提取其中的元素。

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

shell获取到页面内容后,我们通过css选取器可以得到页面中的警句列表:

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

...]

由于页面中有很多警句,这个结果是一个包含很多selector对象的列表。

我们可以通过索引获取第一个selector,然后调用其中的方法得到元素内容。

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

通过quote对象就可以提取其中的文字、作者和标签等内容,这同样是使用css选择器来实现的。

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

页面上每个警句都打了若干标签,我们可以通过getall()来获取这些标签字符串:

>>> tags=quote.css("div.tags a.tag::text").getall()

>>> tags['change', 'deep-thoughts', 'thinking', 'world']

既然我们已经获取了第一个quote的内容,我们同样可以通过循环来获取当前页面所有quote的内容:

>>> 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代码中提取数据

在了解了如何使用scrapy shell提取页面元素后,我们重新回到之前编写的spider代码。

到目前为止,我们的spider仅仅将页面响应Response.body一股脑保存到了HTML文件中。我们需要对它进行完善,以保存有意义的数据。

Scrapy Spider通常会在解析页面之后返回一些包含数据的dict,这些dict可用于后续的处理流程。

我们可以通过在回调函数中使用yield来返回这些dict。

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-1918:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200http://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-1918:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200http://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.”"}

存储数据

Scrapy支持将数据存储到各种存储系统中,最简单的方法是将其保存文件文件。可通过如下命令实现:

scrapy crawl quotes -o quotes.json

这会以JSON格式保存提取的数据,并且是以append的方式写入文件。

如果同时执行多次这个命令,写入到相同文件的数据会相互覆盖,造成数据破坏!

Scrapy提供了JSON Lines的写入方式,可以避免上述覆盖的情况。

scrapy crawl quotes -o quotes.jl

这种格式的文件是按行来保存JSON对象的。

除了JSON,Scrapy还支持csv、xml等存储格式。

如果存储逻辑比较复杂,还可以通过scrapy提供的Item流水线(pipeline)来拆解存储过程,将每个存储步骤封装为一个pipeline,由scrapy引擎来调度执行。这方面的内容会在后续文章中一起学习。

追踪链接

目前我们实现的spider只从两个页面获取数据,如果想要自动获取整个网站的数据,我们还需要提取页面上的其他链接,产生新的爬取请求。

我们了解一下跟踪页面链接的方法。

首先要在页面中找到要进一步爬取的链接。

在测试网站页面上,可以看到列表右下有一个“Next”链接,其HTML源码为:

<ulclass="pager">

    <liclass="next">

        <ahref="/page/2/">Next<spanaria-hidden="true">&rarr;</span></a>

    </li>

</ul>

使用scrapy shell测试一下如何提取这个链接:

>>> response.css('li.next a').get()

'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'

我们使用css('li.next a')得到了这个链接的selector,并通过get()得到了整个链接元素。

显然这数据有点冗余,我们需要的是链接的href属性值。

这个值可以通过scrapy提供的css扩展语法获得:

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

'/page/2/'

也可以通过访问selector的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)

现在我们的初始url为第一页。

parse()函数提取完第一页上所有的警句之后,继续查找页面上的“Next”链接。

如果找到,就产生一个新的请求,并关联自己为这个Request的回调函数。

这样就可以递归地访问整个网站,直到最后一页。

这就是Scrapy跟踪页面链接的机制:

用户负责解析这些链接,通过yield产生新的请求Request,并给Request关联一个处理函数callback。Scrapy负责调度这些Request,自动发送请求,并通过callback处理响应消息。

创建Requests的快捷方法

除了直接创建一个scrapy.Request对象,我们还可以使用response.follow来简化生成Request的方法。

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)

follow可以直接通过相对路径生成url,不需要再调用urljoin()。这和页面上的href写法一致,很方便。

follow还支持直接传入url对应的selector,而不需调用get()提取url字符串。

for href in response.css('ul.pager a::attr(href)'):

    yield response.follow(href,callback=self.parse)

对<a>标签,还可以进一步简化:

for a in response.css('ul.pager a'):

    yield response.follow(a,callback=self.parse)

这是因为follow会自动使用<a>的href属性。

我们还可以使用follow_all从可迭代对象中批量创建Request:

#aonchors包含多个<a>选择器

anchors=response.css('ul.pager a')

yield from response.follow_all(anchors,callback=self.parse)

follow_all也支持简化写法:

yield from response.follow_all(css='ul.pager a',callback=self.parse)

更多示例

我们再看一个spider,其作用是获取所有警句的作者信息。

import scrapy

class AuthorSpider(scrapy.Spider):

    name='author'

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

    def parse(self,response):

        author_page_links=response.css('.author + a')

            yield from response.follow_all(author_page_links,self.parse_author)

        pagination_links=response.css('li.next a')

        yield from response.follow_all(pagination_links,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从测试站点主页开始爬取。

提取这个页面上所有的author链接,并产生新的Request;提取页面上的Next链接,产生对应的Request。

通过parse_author提取作者信息。在parse_author中我们定义了一个helper函数供提取数据使用。

值得注意的是,某个作者可能有多条警句,而每条警句单独包含了这个作者的标签。我们可能会提取多个相同作者的url。但实际上,Scrapy并不会对相同的url发起多次请求,它会自动进行去重处理,这在一定程度上会减轻爬虫对网站的压力。

使用spider参数

我们可以通过scrapy命令行的-a选项来向spider传递一些参数。比如:

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

这里,-a之后,tag为参数名,humor为参数值。

这些参数会传递给spider的__init__方法,并成为spider的属性。

我们可以在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').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),我们就会在初始化url中追加“tag/humor”,这样就只会爬取标签为humor的页面:http://quotes.toscrape.com/tag/humor。

结语

本文“详尽的”介绍了scrapy的基础知识,Scrapy还有跟多特性无法在一篇文章中全部介绍。

我们后续会继续学习Scrapy的方方面面,并在实践中不断理解和掌握。

【欢迎关注RealPython,访问realpython.cn一起学Python】

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354