3、编写提取数据的爬虫Spider

  实现一个Spider子类就像完成一系列填空题,先介绍如何填空,再详细讨论每个细节。

爬虫逻辑

  编写spider之前,先考虑以下问题:①给爬虫起一个名字 ②爬虫从哪个/哪些页面开始爬取?③对于一个已下载的页面,提取其中的哪些数据?④爬取完当前页面后,接下来爬取哪个/哪些页面?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import scrapy
from bookspider.items import BookItem
class BooksSpider(scrapy.Spider): # (1)继承scrapy.Spider
name = "books" #(2)为Spider取名,即每个爬虫的唯一标识
start_urls = ['http://books.toscrape.com/'] #(3)设定爬虫起始点,可以多个

def parse(self,response): #(4)实现页面解析函数,提取数据
for sel in response.css('article.product_pod'): #每本书的信息,是书本信息的大框架
book = BookItem()
book['name'] = sel.xpath('./h3/a/@title').extract_first() #提取书名信息,(5)xpath语法
book['price'] = sel.css('p.price_color::text').extract_first() #提取书价信息,(6)css语法
yield book

#(7)提取新的待爬取url
next_url = response.css('ul.pager li.next a::attr(href)').extract_first() #得到相对路径
if next_url:
next_url = response.urljoin(next_url) #得到绝对路径
yield scrapy.Request(next_url,callback=self.parse) #(8)构造下载请求

(1)Spider基类提供的功能

  供engine调用的接口,比如用来创建Spider实例的类方法from_crawler
  供用户使用的工具函数,比如log方法可以将调试信息输出到日志
  供用户访问的属性,比如通过settings属性可以访问配置文件中的配置

(2)为Spider命名

  一个Scrapy项目可以实现多个Spider,所以应唯一标识每个Spider
  执行scrapy crawl时就用到这个标识,告诉Scrapy用哪个Spider爬虫

(3)设定起始爬虫url

  start_urls通常被实现成一个列表,放入所有起始爬取点的url。设定start_urls之后,Spider基类会自动构造并提交对应的Request对象,以下是Spider基类的源码:

1
2
3
4
5
6
7
8
9
10
class Spider(object_ref):
……
def start_requests(self): #构造并提交Request对象
for url in self.start_urls: #遍历起始爬取点(通过实例访问类属性)
yield self.make_requests_from_url(url)
def make_requests_from_url(self,url): #真正构造Request对象
return Request(url,dont_filter=True)
def parse(self,response)
raise NotImplementedError
……

  【补充1】BooksSpider类没有实现start_requests方法,所以引擎会调用Spider基类的start_requests方法产生下载请求;我们也可以在BooksSpider类中自定义start_requests方法,覆盖基类的方法

1
2
3
4
5
6
7
8
def start_requests(self):
yield scrapy.Request('http://books.toscrape.com/',
callback=self.parse_book,
headers={'User-Agent':'...'},
dont_filter=True)
def parse_book(response):
改用parse_book作为回调函数
……

  【补充2】Spider基类构造Request对象时,没有向callback传递页面解析函数,它的默认解析函数是parse方法。所以BooksSpider类必须实现parse方法,否则会调用Spider基类的parse方法,抛出NotImplementedError异常。
  【补充3】起始爬取点可能有多个,经过start_requests方法实现后,会得到一个可迭代对象(每个元素都是Request对象),每次由yield语句返回一个Request对象。

(4)网页解析

  网页解析就是从页面中提取数据。有两个python模块能处理这一问题 ①BeautifulSoup(API简洁易用,速度慢) ②lxml(由c语言编写的,速度快,API复杂)。而scrapy的Selector类综合了上述两者的优点(它基于lxml库构建,并简化了API接口),它在scrpay.selector模块→from scrapy.selector import Selector
  在Scrapy中,是使用Selector对象来提取页面中的数据,使用步骤如下:
  【第一步】创建Selector对象(有两种方法)

1
2
3
4
5
6
7
8
9
10
11
#方法一:用Selector构造器
Selector对象 = Selector(text='网页文本字符串')

#方法二:用HtmlResponse(在scrapy.html模块中)构造器 + Selector构造器
响应 = Response(url='网址',body='网页文本字符串',encoding='utf-8')
Selector对象 = Selector(response=响应)

#方法三:直接使用Response对象内置的Selector对象
#访问Response对象的selector属性时,Response对象会自动创建Selector对象,并缓存以便下次使用
response = HtmlResponse(...)
response.selector

  【第二步】用Selector对象的XPath或CSS选择器,选中数据
   为了方便使用,可以直接对Response对象调用xpath/css选择器(即可以跳过第一步构造selector对象)。比如response.xpath(‘…’),会返回一个SelectorList对象(它是一个列表,元素是Selector对象,可以用for循环迭代访问每一个Selector对象)。SelectorList对象也可以用XPath/CSS选择器,它会作用于每一个Selector对象。

1
2
3
4
5
6
7
8
# scrapy源码的相关实现
class TextResponse(Response):
……
def xpath(self,query,**kwargs):
return self.selector.xpath(query,**kwargs)

def css(self,query):
return self.selector.css(query)

  【第三步】提取数据
   extract() 返回Unicode字符串的列表
   extract_first() 只能SelectorList对象使用,用于返回第一个Selector对象的Unicode字符串,相当于s[0].extract()或者s.extract()[0]
   re() 用正则表达式提取选中内容的某部分
   re_first() 只能SelectorList对象使用

(5)XPath

  XPath是XML路径语言(XML Path Language),是一种用来确定xml文档中某部分位置的语言。xml文档(html属于xml)是有一些列节点构成的树。节点常见类型:①根节点:整个文档树的根;②元素节点:html、body、div、p、a;③属性节点:href;④文本节点:Hello word、Click here。节点之间的关系:①父子 ②兄弟 ③祖先、后裔
  【基础语法】
    /节点 从根开始搜索所有子节点
    //节点 从根开始搜索所有后代节点
    点’.’ 代表当前节点,’.//节点’会从当前节点开始搜索
    text() 选中文本
    @属性名 选中属性,比如选择中所有属性是某值的节点→节点[@属性=’值’]
  【其他函数】
    string(参数) 返回参数的字符串值,比如s.xpath(‘string(/html/body/a/strong)’).extract(),相当于s.xpath(‘/html/body/a/strong/text()’)。区别是:如果两个文本在不同的标签下,用string可以直接将不同地方的字符串连接起来,用text()会返回这些字符串组成的列表(不会连接起来)
    contains(str1,str2) 判断str1是否包含str2,返回布尔值

(6)CSS

  CSS是层叠样式表,其选择器可用来确定HTML文档中某部分的位置。与XPath比较:语法比XPath简单,但功能不如XPath强大(python内部会将CSS翻译成xpath标的调用xpath方法)
  【基本语法】
    .值 选中所有class为该值的元素
    #值 选中所有id为该值的元素
    节点 选中所有该节点
    节点1,节点2 同时选中两节点
    节点1 节点2 选中所有节点2(后代节点)
    节点1>节点2 选中所有节点2(子节点)
    [属性] 选中属性,有值→[属性=值],包含该值→[属性~=值]
    节点::text 选中节点下的所有文本
    节点::attr(属性) 选择节点下的属性文本

(7)提取新url

  提取url有两种方法:①Selector:因为链接也是页面中的数据,所以可以用与提取数据相同的方法进行提取,适用于提取少量的url或者提取规则比较简单的情形;②LinkExtractor类:是一个专门用于提取url的类,适用于提取大量url或规则比较复杂的情形
  本例用的是第一种方法提取下一页链接,先用CSS选择器选中包含下一页链接的a元素并获取其href属性,然后调用response.urljoin方法计算绝对url,最后构造Request对象并提交。接下来介绍一下如何用LinkExtractor提取链接。

1
2
3
4
5
6
7
8
9
10
from scrapy.linkextractors import LinkExtractor
class BooksSpider(scrapy.Spider):
……
def parse(self,response):
#提取链接,下一页的url在ul.page>li.next>a里面
le = LinkExtractor(restrict_css='ul.pager li.next') #创建一个LinkExtractor对象,输入提取规则。如果不传递任何参数,就提取页面所有链接
links = le.extract_links(response) #该方法根据提取规则,在response对象所包含的页面中提取链接,返回一个列表,元素是Link对象
if links:
next_url = links[0].url #用links[0]获取Link对象,它的url属性就是链接的绝对地址
yield scrapy.Request(next_url,callback=self.parse) #构造Request对象并提交

  LinkExtractor构造器的各个参数:①allow=正则表达式(或列表),提取与正则表达式匹配的链接;②deny 与allow相反,排除与正则表达式匹配的链接;③allow_domains=域名(或列表),提取指定域名的链接;④deny_domains;⑤restrict_xpaths=XPath表达式(或列表),提取XPath表达式选中区域下的链接;⑥restrict_css;⑦tags=标签(或列表),提取指定标签内的链接,默认是[‘a’,’area’];⑧attrs=属性(或列表),提取指定属性内的链接,默认是[‘href’];⑨process_value=回调函数,用该函数处理提取到的每个链接,一般返回一个字符串。

(8)构造下载请求

【构造Resquest对象(下载请求)的参数】
  url 必选,是请求页面下载的url地址
  callback 回调→页面解析函数,页面下载完成后调用页面解析函数
  method http请求的方法,默认为’GET’
  headers http请求的头部字典(dict类型)
  body http请求的正文
  cookies Cookie信息字典(dict类型)
  meta Request的元数据字典(dict类型),与其他模块互传信息
  encoding url和body的默认编码是’utf-8’
  priority 指定优先级,默认为0(值小的优先下载)
  dont_filte 默认为False→避免重复下载,当页面随时间变化则改为true
  errback 请求出现异常/出现http错误时的回调函数
【补充】Response对象(响应)
  Response只是一个基类,根据响应内容的不同有如下子类:TextResponse、HtmlResponse、XmlResponse。当一个页面下载完成时,下载器根据http响应头部中的Content-Type信息创建子类对象(爬取网页的内容是html文本,创建的是HtmlResponse对象)
  HtmlResponse对象的参数:
   url 响应的url地址(str)
   status 响应的状态码(int)
   headers 响应的头部(dict),可用get/getlist方法访问
   body 响应的正文(bytes)
   text 响应的正文(str)=response.body.decode(response.encoding)
   encoding 正文的编码,可能从headers/正文中解析出来
   request 产生该响应的Request对象
   meta response.request.meta=处理响应的函数,response.meta取信息
   selector Selector对象用于在Response中提取数据
   xpath/css 选择器,用来提取数据,是response.selector.xpath/css的快捷方式
   urljoin 当url是相对地址,response.urljoin(url)得到绝对地址