Scrapy简明教程

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

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

基本安装方法为:pip install scrapypython

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

本文主要包含以下几部分:chrome

1,建立一个scrapy项目shell

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

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

4,递归地爬取子页面app

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

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

  • 建立爬虫项目

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

scrapy startproject tutorial

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

    scrapy.cfg # 部署配置文件 tutorial/ # 项目的Python模块, 咱们从这个模块中导入所需代码 __init__.py items.py # items定义文件 middlewares.py # middlewares(中间件)定义文件 pipelines.py # pipelines(流水线)定义文件 settings.py # 项目配置文件 spiders/ # 存放spider的目录 __init__.py
  • 编写蜘蛛类

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。

回车后会获得以下输出:

[ ... 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中测试如何提取页面元素了。

可使用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/,在开发者工具中查看单条警句的源码以下所示:

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

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-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.”"}
  • 存储数据

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源码为:

<ul class="pager"> <li class="next"> <a href="/page/2/">Next <span aria-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 -o quotes-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的方方面面,并在实践中不断理解和掌握。

相关文章
相关标签/搜索