第8页 | 精通Scrapy网络爬虫 | 阅读 ‧ 电子书库

同步阅读进度,多语言翻译,过滤屏幕蓝光,评论分享,更多完整功能,更好读书体验,试试 阅读 ‧ 电子书库

第2章
编写Spider

从本章开始介绍Scrapy爬虫的基础知识部分,我们先从Scrapy爬虫程序中最核心的组件Spider讲起,在学习编写Spider之前,需要一些知识作为铺垫。本章重点讲解以下内容:

● Scrapy框架结构及工作原理。

● Request对象和Response对象。

2.1 Scrapy框架结构及工作原理

图2-1展示了Scrapy框架的组成结构,并从数据流的角度揭示Scrapy的工作原理。

图2-1

首先,简单了解一下Scrapy框架中的各个组件,如表2-1所示。

表2-1

对于用户来说,Spider是最核心的组件,Scrapy爬虫开发是围绕实现Spider展开的。

接下来,看一下在框架中的数据流,有表2-2所示的3种对象。

表2-2

Request和Response是HTTP协议中的术语,即HTTP请求和HTTP响应,Scrapy框架中定义了相应的Request和Response类,这里的Item代表Spider从页面中爬取的一项数据。

最后,我们来说明以上几种对象在框架中的流动过程。

● 当SPIDER要爬取某URL地址的页面时,需使用该URL构造一个Request对象,提交给ENGINE(图2-1中的1)。

● Request对象随后进入SCHEDULER按某种算法进行排队,之后的某个时刻SCHEDULER将其出队,送往DOWNLOADER(图2-1中的2、3、4)。

● DOWNLOADER根据Request对象中的URL地址发送一次HTTP请求到网站服务器,之后用服务器返回的HTTP响应构造出一个Response对象,其中包含页面的HTML文本(图2-1中的5)。

● Response对象最终会被递送给SPIDER的页面解析函数(构造Request对象时指定)进行处理,页面解析函数从页面中提取数据,封装成Item后提交给ENGINE,Item之后被送往ITEM PIPELINES进行处理,最终可能由EXPORTER(图2-1中没有显示)以某种数据格式写入文件(csv,json);另一方面,页面解析函数还从页面中提取链接(URL),构造出新的Request对象提交给ENGINE(图2-1中的6、7、8)。

理解了框架中的数据流,也就理解了Scrapy爬虫的工作原理。如果把框架中的组件比作人体的各个器官,Request和Response对象便是血液,Item则是代谢产物。

2.2 Request和Response对象

通过上述讲解,大家已经了解了Request和Response对象在Scrapy框架中的重要性,下面详细介绍这两个对象。

2.2.1 Request对象

Request对象用来描述一个HTTP请求,下面是其构造器方法的参数列表:

Request(url[, callback, method='GET', headers, body, cookies, meta, encoding='utf-8', priority=0, dont_filter=False, errback])

下面依次介绍这些参数。

● url(必选)

请求页面的url地址,bytes或str类型,如'http://www.python.org/doc'。

● callback

页面解析函数, Callable类型,Request对象请求的页面下载完成后,由该参数指定的页面解析函数被调用。如果未传递该参数,默认调用Spider的parse方法。

● method

HTTP请求的方法,默认为'GET'。

● headers

HTTP请求的头部字典,dict类型,例如{'Accept': 'text/html', 'User-Agent':Mozilla/5.0'}。如果其中某项的值为None,就表示不发送该项HTTP头部,例如{'Cookie': None},禁止发送Cookie。

● body

HTTP请求的正文,bytes或str类型。

● cookies

Cookie信息字典,dict类型,例如{'currency': 'USD', 'country': 'UY'}。

● meta

Request的元数据字典,dict类型,用于给框架中其他组件传递信息,比如中间件Item Pipeline。其他组件可以使用Request对象的meta属性访问该元数据字典(request.meta),也用于给响应处理函数传递信息,详见Response的meta属性。

● encoding

url和body参数的编码默认为'utf-8'。如果传入的url或body参数是str类型,就使用该参数进行编码。

● priority

请求的优先级默认值为0,优先级高的请求优先下载。

● dont_filter

默认情况下(dont_filter=False),对同一个url地址多次提交下载请求,后面的请求会被去重过滤器过滤(避免重复下载)。如果将该参数置为True,可以使请求避免被过滤,强制下载。例如,在多次爬取一个内容随时间而变化的页面时(每次使用相同的url),可以将该参数置为True。

● errback

请求出现异常或者出现HTTP错误时(如404页面不存在)的回调函数。

虽然参数很多,但除了url参数外,其他都带有默认值。在构造Request对象时,通常我们只需传递一个url参数或再加一个callback参数,其他使用默认值即可,代码如下:

>>> import scrapy >>> request = scrapy.Request('http://books.toscrape.com/') >>> request2 = scrapy.Request('http://quotes.toscrape.com/', callback=self.parseItem)

在实际应用中,我们几乎只调用Request的构造器创建对象,但也可以根据需求访问Request对象的属性,常用的有以下几个:

● url

● method

● headers

● body

● meta

这些属性和构造器参数相对应,这里不再重复解释。

2.2.2 Response对象

Response对象用来描述一个HTTP响应,Response只是一个基类,根据响应内容的不同有如下子类:

● TextResponse

● HtmlResponse

● XmlResponse

当一个页面下载完成时,下载器依据HTTP响应头部中的Content-Type信息创建某个Response的子类对象。我们通常爬取的网页,其内容是HTML文本,创建的便是HtmlResponse对象,其中HtmlResponse和XmlResponse是TextResponse的子类。实际上,这3个子类只有细微的差别,这里以HtmlResponse为例进行讲解。

下面介绍HtmlResponse对象的属性及方法。

● url

HTTP响应的url地址,str类型。

● status

HTTP响应的状态码,int类型,例如200,404。

● headers

HTTP响应的头头部,类字典类型,可以调用get或getlist方法对其进行访问,例如:

response.headers.get('Content-Type') response.headers.getlist('Set-Cookie')

● body

HTTP响应正文,bytes类型。

● text

文本形式的HTTP响应正文,str类型,它是由response.body使用response.encoding解码得到的,即

reponse.text = response.body.decode(response.encoding)

● encoding

HTTP响应正文的编码,它的值可能是从HTTP响应头部或正文中解析出来的。

● request

产生该HTTP响应的Request对象。

● meta

即response.request.meta,在构造Request对象时,可将要传递给响应处理函数的信息通过meta参数传入;响应处理函数处理响应时,通过response.meta将信息取出。

● selector

Selector对象用于在Response中提取数据(选择器相关话题在后面章节详细讲解)。

● xpath(query)

使用XPath选择器在Response中提取数据,实际上它是response.selector.xpath方法的快捷方式(选择器相关话题在后面章节详细讲解)。

● css(query)

使用CSS选择器在Response中提取数据,实际上它是response.selector.css方法的快捷方式(选择器相关话题在后面章节详细讲解)。

● urljoin(url)

用于构造绝对url。当传入的url参数是一个相对地址时,根据response.url计算出相应的绝对url。例如,response.url为http://www.example.com/a,url为b/index.html,调用response.urljoin(url)的结果为http://www.example.com/a/b/index.html。

虽然HtmlResponse对象有很多属性,但最常用的是以下的3个方法:

● xpath(query)

● css(query)

● urljoin(url)

前两个方法用于提取数据,后一个方法用于构造绝对url。

2.3 Spider开发流程

有了前面知识的铺垫,现在回到本章的主题“编写Spider”。实现一个Spider子类的过程很像是完成一系列填空题,Scrapy框架提出以下问题让用户在Spider子类中作答:

● 爬虫从哪个或哪些页面开始爬取?

● 对于一个已下载的页面,提取其中的哪些数据?

● 爬取完当前页面后,接下来爬取哪个或哪些页面?

上面问题的答案包含了一个爬虫最重要的逻辑,回答了这些问题,一个爬虫也就开发出来了。

接下来,我们以上一章exmaple项目中的BooksSpider为例,讲解一个Spider的开发流程。为方便阅读,再次给出BooksSpider的代码:

# -*- coding: utf-8 -*- import scrapy class BooksSpider(scrapy.Spider): # 每一个爬虫的唯一标识 name = "books" # 定义爬虫爬取的起始点,起始点可以是多个,我们这里是一个 start_urls = ['http://books.toscrape.com/'] def parse(self, response): # 提取数据 # 每一本书的信息是在<article class="product_pod">中,我们使用 # css()方法找到所有这样的article 元素,并依次迭代 for book in response.css('article.product_pod'): # 书名信息在article > h3 > a 元素的title属性里 # 例如: <a title="A Light in the Attic">A Light in the ...</a> name = book.xpath('./h3/a/@title').extract_first() # 书价信息在 <p class="price_color">的TEXT中。 # 例如: <p class="price_color">£51.77</p> price = book.css('p.price_color::text').extract_first() yield { 'name': name, 'price': price, } # 提取链接 # 下一页的url 在ul.pager > li.next > a 里面 # 例如: <li class="next"><a href="catalogue/page-2.html">next</a></li> next_url = response.css('ul.pager li.next a::attr(href)').extract_first() if next_url: # 如果找到下一页的url,得到绝对路径,构造新的Request 对象 next_url = response.urljoin(next_url) yield scrapy.Request(next_url, callback=self.parse)

实现一个Spider只需要完成下面4个步骤:

步骤 01 继承scrapy.Spider。

步骤 02 为Spider取名。

步骤 03 设定起始爬取点。

步骤 04 实现页面解析函数。

2.3.1 继承scrapy.Spider

Scrapy框架提供了一个Spider基类,我们编写的Spider需要继承它:

import scrapy class BooksSpider(scrapy.Spider): ...

这个Spider基类实现了以下内容:

● 供Scrapy引擎调用的接口,例如用来创建Spider实例的类方法from_crawler。

● 供用户使用的实用工具函数,例如可以调用log方法将调试信息输出到日志。

● 供用户访问的属性,例如可以通过settings属性访问配置文件中的配置。

实际上,在初学Scrapy时,不必关心Spider基类的这些细节,未来有需求时再去查阅文档即可。

2.3.2 为Spider命名

在一个Scrapy项目中可以实现多个Spider,每个Spider需要有一个能够区分彼此的唯一标识,Spider的类属性name便是这个唯一标识。

class BooksSpider(scrapy.Spider): name = "books" ...

执行scrapy crawl命令时就用到了这个标识,告诉Scrapy使用哪个Spider进行爬取。

2.3.3 设定起始爬取点

Spider必然要从某个或某些页面开始爬取,我们称这些页面为起始爬取点,可以通过类属性start_urls来设定起始爬取点:

class BooksSpider(scrapy.Spider): ... start_urls = ['http://books.toscrape.com/'] ...

start_urls通常被实现成一个列表,其中放入所有起始爬取点的url(例子中只有一个起始点)。看到这里,大家可能会想,请求页面下载不是一定要提交Request对象么?而我们仅定义了url列表,是谁暗中构造并提交了相应的Request对象呢?通过阅读Spider基类的源码可以找到答案,相关代码如下:

class Spider(object_ref): ... def start_requests(self): for url in self.start_urls: yield self.make_requests_from_url(url) def make_requests_from_url(self, url): return Request(url, dont_filter=True) def parse(self, response): raise NotImplementedError ...

从代码中可以看出,Spider基类的start_requests方法帮助我们构造并提交了Request对象,对其中的原理做如下解释:

● 实际上,对于起始爬取点的下载请求是由Scrapy引擎调用Spider对象的start_requests方法提交的,由于BooksSpider类没有实现start_requests方法,因此引擎会调用Spider基类的start_requests方法。

● 在start_requests方法中,self.start_urls便是我们定义的起始爬取点列表(通过实例访问类属性),对其进行迭代,用迭代出的每个url作为参数调用make_requests_from_url方法。

● 在make_requests_from_url方法中,我们找到了真正构造Reqeust对象的代码,仅使用url和dont_filter参数构造Request对象。

● 由于构造Request对象时并没有传递callback参数来指定页面解析函数,因此默认将parse方法作为页面解析函数。此时BooksSpider必须实现parse方法,否则就会调用Spider基类的parse方法,从而抛出NotImplementedError异常(可以看作基类定义了一个抽象接口)。

● 起始爬取点可能有多个,start_requests方法需要返回一个可迭代对象(列表、生成器等),其中每一个元素是一个Request对象。这里,start_requests方法被实现成一个生成器函数(生成器对象是可迭代的),每次由yield语句返回一个Request对象。

由于起始爬取点的下载请求是由引擎调用Spider对象的start_requests方法产生的,因此我们也可以在BooksSpider中实现start_requests方法(覆盖基类Spider的start_requests方法),直接构造并提交起始爬取点的Request对象。在某些场景下使用这种方式更加灵活,例如有时想为Request添加特定的HTTP请求头部,或想为Request指定特定的页面解析函数。

以下是通过实现start_requests方法定义起始爬取点的示例代码(改写BooksSpider):

class BooksSpider(scrapy.Spider): # start_urls = ['http://books.toscrape.com/'] # 实现start_requests 方法, 替代start_urls类属性 def start_requests(self): yield scrapy.Request('http://books.toscrape.com/', callback=self.parse_book, headers={'User-Agent': 'Mozilla/5.0'}, dont_filter=True) # 改用parse_book 作为回调函数 def parse_book(response): ...

到此,我们介绍完了为爬虫设定起始爬取点的两种方式:

● 定义start_urls属性。

● 实现start_requests方法。

2.3.4 实现页面解析函数

页面解析函数也就是构造Request对象时通过callback参数指定的回调函数(或默认的parse方法)。页面解析函数是实现Spider中最核心的部分,它需要完成以下两项工作:

● 使用选择器提取页面中的数据,将数据封装后(Item或字典)提交给Scrapy引擎。

● 使用选择器或LinkExtractor提取页面中的链接,用其构造新的Request对象并提交给Scrapy引擎(下载链接页面)。

一个页面中可能包含多项数据以及多个链接,因此页面解析函数被要求返回一个可迭代对象(通常被实现成一个生成器函数),每次迭代返回一项数据(Item或字典)或一个Request对象。

关于如何提取数据、封装数据、提取链接等话题,我们在接下来的章节继续学习。

2.4 本章小结

本章先讲解了Scrapy的框架结构以及工作原理,然后又介绍了Scrapy中与页面下载相关的两个核心对象Request和Response,有了这些知识的铺垫,最后我们讲解了实现一个Spider的开发流程。关于编写Spider时涉及的一些技术细节在后面章节继续讨论。

请支持我们,让我们可以支付服务器费用。
使用微信支付打赏


上一页 · 目录下一页


下载 · 书页 · 阅读 ‧ 电子书库