CrawlSpider
CrawlSpider 是 Scrapy 提供的一个通用 Spider。在 Spider 里,我们可以指定一些爬取规则来实现页面的提取,这些爬取规则有一个专门的数据结构 Rule 表示。Rule 里包含提取和跟进页面的配置,Spider 会根据Rule来确定当前页面中的哪些链接需要继续爬取、哪些页面的爬取结果需要用到哪个方法进行解析等。
Rule
CrawlSpider 里最重要的就是Rule的定义了,它的定义和参数如下所示:
class scrapy.spiders.Rule(
link_extractor,
callback = None,
cb_kwargs = None,
follow = None,
process_links = None,
process_request = None
)
link_extractor:一个Link Extractor对象,用于定义爬取规则。通过它,Spider可以知道从爬取的页面中提取出哪些链接。提取出的链接会自动生成Request请求对象。
callback:即回调函数,和之前定义Request的callback有相同的意义。每次从link_extrcctor中获取到链接后,该函数将会被调用。该回调函数接收一个response作为其第一个参数。注意,避免使用parse()作为回调函数,否则CrawlSpider将会运行失败。
cb_kwargs:字典,它包含传递给回调函数的参数。
follow:布尔值,及True或False,它指定根据该规则从response中提取的链接是否需要跟进。
process_links:从link_extractor中获取到链接后会传递给这个函数,用来过滤不需要爬取的链接。
process_request:根据该Rule提取到的每个request时,该函数都会调用,对Request进行处理,该函数必须返回Request或者None。
案例实现
下面我们以中华网科技类新闻为例,来了解CrawlSpider的用法。官网链接为:http://tech.china.com/。我们需要爬取它的科技类新闻内容,链接为http://tech.china.com.articles/,页面如下图所示:
我们要抓取新闻列表内所有分页的新闻详情,包括标题、正文、时间、来源等信息。
新建项目
首先新建一个Scrapy项目,名为technology,如下所示:
scrapy startproject technology
接下来创建一个CrawlSpider,需要先制定一个模板。我们可以先看看有哪些可用模板,命令如下所示:
scrapy genspider -l
运行结果如下:
Available templates:
basic
crawl
csvfeed
xmlfeed
之前创建普通Spider的时候,我们默认使用了第一个模板basic。这次要创建CrawlSpider,就需要使用第二个模板crawl,创建命令如下:
scrapy genspider -t crawl china tech.china.com
运行之后便会生成一个CrawlSpider,其内容如下所示:
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class ChinaSpider(CrawlSpider):
name = 'china'
allowed_domains = ['tech.china.com']
start_urls = ['http://tech.china.com/']
rules = (
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)
def parse_item(self, response):
item = {}
#item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
#item['name'] = response.xpath('//div[@id="name"]').get()
#item['description'] = response.xpath('//div[@id="description"]').get()
return item
这次生成的Spider内容多了一个rules属性的定义。Rule的第一个参数是LinkExtractor,就是之前说的爬取规则,同时,默认的回调函数也不再是parse,而是parse_item。
定义Rule
要实现网页的爬取,我们需要做的就是定义好相应的Rule,然后再实现解析函数。
首先将start_urls修改为起始链接,代码如下所示:
start_url = ['http://tech.china.com/articles/']
之后,Spider爬取start_urls里面的每一个链接。所以这里第一个爬取的页面就是我们刚才所定义的链接。得到Response之后,Spider就会根据每一个Rule来提取这个页面内的超链接,去生成进一步的Request。接下来就要分析页面以此来定义Rule。
当前页面如下所示:
这里是新闻的列表页,下一步自然就是将其中的每个新闻详情链接提取出来。查看源代码,所有的链接都在id为rank-defList的节点内,具体来说是它内部的class为wntjItem item_defaultView clearfix的节点,如下图所示:
此处,我们可以通过LinkExtractor的restrict_xpath属性来指定,之后Spider就会从这个区域提取到相应的超链接并生成Request。另外,这些链接对应的页面其实就是新闻详情页,而我们需要解析的就是新闻的详情信息,所有此处还需要指定一个回调函数callback。
到现在,我们就可以构造出一个Rule了,代码如下所示:
Rule(LinkExtractor(restrict_xpaths='//div[@id="rank-defList"]/div[@class="wntjItem item_defaultView clearfix"]//h3/a'), callback='parse_item')
接下来,我们还要让当前页面实现分页功能,所以需要提取下一页的链接。分析可以发现下一页的链接是在class为pages的div标签下的a标签内,如下图:
但是,下一页节点和其他分页链接区分度不高,要取出此链接我们可以直接用XPath的文本匹配方式,所以这里我们直接用LinkExtractor的restrict_xpath属性来指定提取的链接即可。另外,我们不需要像新闻详情页一样去提取对应的页面详细信息,也就是不需要生成Item,所以不需要加callback参数。但是我们需要加一个follow参数为True,代表继续跟进匹配分析。此处的Rule定义如下:
Rule(LinkExtractor(restrict_xpaths='//div[@class="pages"]/a[contains("下一页")]'), follow=True)
解析页面
接下来我们需要做的就是解析页面内容了,将想要爬取的数据提取出来即可,可以定义一个Item,如下所示:
import scrapy
class TechnologyItem(scrapy.Item):
title = scrapy.Field()
url = scrapy.Field()
text = scrapy.Field()
datetime = scrapy.Field()
source = scrapy.Field()
pass
这里的字段分别指新闻标题、链接、正文、发布时间、来源。详情页预览图如下所示:
如果像之前一样提取内容,就直接调用response对象的xpath()、css()等方法即可。这里parse_item()方法的实现如下所示:
def parse_item(self, response):
item = TechnologyItem()
item['title'] = response.xpath('//h1[@id="chan_newsTitle"]/text()').extract_first()
item['url'] = response.url
item['text'] = response.xpath('//div[@id="chan_newsDetail"]/p/text()').extract()
item['datetime'] = response.xpath('//div[@class="chan_newsInfo_source"]/span/text()').extract()[0]
item['source'] = response.xpath('//div[@class="chan_newsInfo_source"]/span/text()').extract()[1]
yield item
这样我们就把每条新闻的信息提取出来了。
此时运行一下Spider,输出内容如下图所示:
现在我们就成功的把每条新闻的信息提取出来了。
Item Loader
Scrapy还提供了一个Item Loader模块来帮助我们方便的提取Item。我们可以利用它对Item进行赋值,Item提供的是保存抓取数据的容器,而Item Loader提供的是填充容器的机制。
Item Loader的API如下所示:
class scrapy.loader.ItemLoader([item, selector, response, ] **kwargs)
item:它是Item对象,可以调用add_xpath()、add_css()或add_value()等方法来填充Item对象。
selector:它是Selector对象,是用来提取数据的选择器。
response:Response对象,用于使用构造选择器的Response。
一个比较典型的Item Loader实例如下所示:
from scrapy.loader import ItemLoader
from project.items import Product
def parse(self, response):
loader = ItemLoader(item=Product(), response=response)
loader.add_xpath('name', '//div[@id="title"]')
loader.add_xpath('price', '//p[@id="price"]')
loader.add_css('stock', 'p.stock')
loader.add_value('date', 'today')
return loader.load_item()
这里首先声明一个Product Item,用该Item和Response对象实例化ItemLoader,然后调用一系列方法对不同的属性进行赋值,最后调用load_item()方法实现Item的解析。
另外,其中有一些内置的Processor,我们可以自己定义一些Processor来处理数据,在调用load_item()方法时,会先调用Output Processor来处理收集到的数据,然后再存入Item中,这样才会生成最终的Item。
下面介绍一些内置的Processor。
Identity
Identity是最简单的Processor,不进行任何的处理,直接返回原来的数据。
TakeFirst
TakeFirst返回列表的第一个非空值,类似于extract_first()的功能,常用作Output Processor,如下所示:
from itemloaders import processors
processor = processors.TakeFirst()
print(processor(['',1,2,3]))
输出结果如下所示:
1
经过此Processor处理后的结果返回了第一个不为空的值
Join
Join方法相当于字符串的join()方法,可以把列表拼接为字符串,字符串默认使用空格分隔,如下所示:
from itemloaders import processors
processor = processors.Join()
print(processor([0,1,2,3]))
输出结果如下所示:
0 1 2 3
也可以通过参数更改默认的分隔符,例如改成逗号:
from itemloaders import processors
processor = processors.Join(',')
print(processor([0,1,2,3]))
结果如下:
0,1,2,3
Compose
Compose是用给定的多个函数组合而成的Processor,每个输入值会被传递到第一个参数,其输出结果再传递到第二个函数,以此类推,直到最后一个函数返回整个处理器的输出,如下所示:
from itemloaders import processors
processor = processors.Compose(str.upper, str.strip)
print(processor(' hello world'))
运行结果如下:
HELLO WORLD
在这里我们构造了一个Compose Processor,传入一个开头带有空格的字符串。它的参数有两个:第一个是str.upper,就是将字母全部转为大写;第二个是str.strip,就是取出头尾空白字符。Compose会依次调用两个参数,最终的结果就是字符串全部转化为大写且去除了开头的空格。
MapCompose
与Compose类似,不同之处在于它可以迭代处理一个列表输入值,如下所示:
from itemloaders import processors
processor = processors.Compose(str.upper, str.strip)
print(processor(['hello','world']))
运行结果如下:
['HELLO','WORLD']
被处理的内容是一个可迭代对象,MapCompose会将该对象遍历然后依次处理。
案例
在上述案例中,如果我们要使用Item Loader进行Item的操作,我们可以改写parse_item(),如下所示:
def parse_item(self,response):
loader = ChinaLoader(item=TechnologyItem(),response=response)
loader.add_xpath('title','//h1[@id="chan_newsTitle"]/text()')
loader.add_value('url',response.url)
loader.add_xpath('text','//div[@id="chan_newsDetail"]/p/text()')
loader.add_xpath('datetime','//div[@class="chan_newsInfo_source"]/span/text()')
loader.add_xpath('source','//div[@class="chan_newsInfo_source"]/span[position()=2]/text()')
yield loader.load_item()
这里我们定义了一个ItemLoader的子类,名为ChinaLoader,其实现如下所示:
from scrapy.loader import ItemLoader
from itemloaders import processors
class NewsLoader(ItemLoader):
default_output_processor = processors.TakeFirst()
class ChinaLoader(NewsLoader):
text_out = processors.Compose(processors.Join(),lambda s:s.strip())
source_out = processors.Compose(lambda a:a[0].split(':')[1],lambda s:s.strip())
ChinaLoader继承了NewsLoader类,其内部定义了一个默认的Output Processor为TakeFirst,这相当于之前定义的extract_first()方法的功能。我们在ChinaLoader中定义了text_out和source_out字段,分别实现了相应的功能。
至此,将代码重新运行,提取效果是一样的。