ItemLoader是为了便于我们对代码进行维护,便于维护的一种表现就是通用逻辑的封装,而ItemLoader机制让我们可以对于字段的提取规则进行有效的封装。
ItemLoader实际上提供了一个容器,它里面可以配置对于一个字段,我们使用何种提取规则来解析它,最后调用load_item方法就可以生成我们的Item。
在cnblogs.py中,引入:
from scrapy.loader import ItemLoader
将原来的parse_detail:
def parse_detail(self, response):
"""
解析文章具体字段
"""
if 'account' in response.url:
pass
else:
# 实例化一个CnblogsArticleItem对象
article_item = CnblogsArticleItem()
# 标题
title = response.xpath('//*[@id="news_title"]/a/text()').extract_first("Untitled")
# 发布日期
create_date = re.findall('\d{4}-\d{2}-\d{2}', response.xpath('//*[@id="news_info"]/span[2]/text()').extract_first("0000-00-00"))[0]
try:
create_date = datetime.datetime.strptime(create_date, "%Y-%m-%d").date()
except Exception as e:
create_date = datetime.datetime.now().date()
# 封面图
front_image_url = response.meta.get('front_image_url', DEFAULT_IMAGE_URL)
# 正文
content = response.xpath('//*[@id="news_body"]').extract_first("No content")
# 标签
tags = ','.join(response.xpath('//*[@id="news_more_info"]/div/a/text()').extract())
# 来源
source = response.xpath('//*[@id="link_source2"]/text()').extract_first('Unknown')
# 利用解析出的值填充实例化的CnblogsArticleItem对象
article_item["title"] = title
article_item["create_date"] = create_date
article_item["url"] = response.url
article_item["url_id"] = get_md5(response.url)
article_item["front_image_url"] = [front_image_url]
article_item["content"] = content
article_item["tags"] = tags
article_item["source"] = source
# 将CnblogsArticleItem实例yield给pipelines.py
yield article_item
pass
重写为:
def parse_detail(self, response):
"""
解析文章具体字段
"""
if 'account' in response.url:
pass
else:
# 通过item loader加载item
# 1.实例化一个item_loader
item_loader = ItemLoader(item=CnblogsArticleItem(), response=response)
# 2.填充字段的提取规则
item_loader.add_xpath("title", '//*[@id="news_title"]/a/text()')
item_loader.add_value("url", response.url)
item_loader.add_value("url_id", get_md5(response.url))
item_loader.add_xpath("create_date", '//*[@id="news_info"]/span[2]/text()')
item_loader.add_value("front_image_url", response.meta.get("front_image_url", ""))
item_loader.add_xpath("source", '//*[@id="link_source2"]/text()')
item_loader.add_xpath("tags", '//*[@id="news_more_info"]/div/a/text()')
item_loader.add_xpath("content", '//*[@id="news_body"]')
# 3.加载Item
article_item = item_loader.load_item()
# 4.将CnblogsArticleItem实例yield给pipelines.py
yield article_item
pass
为什么说ItemLoader机制更便于维护,因为它提高了代码的可配置性,上面写的提取规则实际上可以存放在数据库中,进行python映射后直接使用。
调试一下,发现article_item是这样的:
一方面,所有的字段都是list,包括front_image_url字段,我们没有为它的第二个参数加方括号,即是:
item_loader.add_value("front_image_url", response.meta.get("front_image_url", ""))
而不是:
item_loader.add_value("front_image_url", [response.meta.get("front_image_url", "")])
也同样是list。
另一方面,有些字段我们需要做进一步过滤。
为了解决上面的两个问题,我们需要对Item进行一定的修改。实际上,Item中的scrapy.Field中有两个参数是可以自定义的。
一个是input_processor,是说当值传进来时,可以做相应的预处理。为了使用它,我们需要引入:
from scrapy.loader.processors import MapCompose
input_processor指定的预处理可能是由多个函数完成的,MapCompose可以传入任意多个函数,从左到右的调用。
比如对于title这一字段,我们希望在传入时在其后方添加" by dmxjhg",我们可以定义这样一个函数:
def add_author(value):
return value + " by dmxjhg"
value就是在本次预处理函数执行前传入的titile值。
相应的title字段在Item中应为:
title = scrapy.Field(input_processor = MapCompose(add_author))
传入的甚至可以是lambda函数:
title = scrapy.Field(input_processor = MapCompose(lambda x: x + "——Cnblogs", add_author))
调试一下发现,果然:
对于create_date字段,我们需要在items.py中:
import datetime
import re
def date_convert(value):
create_date = re.findall('\d{4}-\d{2}-\d{2}', value)[0]
try:
create_date = datetime.datetime.strptime(create_date, "%Y-%m-%d").date()
except Exception as e:
create_date = datetime.datetime.now().date()
return create_date
相应的Field:
create_date = scrapy.Field(input_processor = MapCompose(date_convert))
调试,发现create_date是list,其内的元素已经是date类型了:
上面已经说了,有两个问题,一个是传来的是list,第二个是需要对字段进行进一步的过滤。对于第二个问题,通过上面的知识我们已经可以解决了,而对于第一个问题,我们可以在items.py中这样编码解决:
from scrapy.loader.processors import MapCompose, TakeFirst
在需要取第一个的字段的Field中添加:
output_processor = TakeFirst()
我们先为title和create_date添加:
# 标题
title = scrapy.Field(input_processor = MapCompose(lambda x: x + "——Cnblogs", add_author),
output_processor = TakeFirst())
# 创建时间
create_date = scrapy.Field(input_processor = MapCompose(date_convert),
output_processor=TakeFirst())
调试发现确实有效:
我们现在只添加了两个,难道以后成百上千,也要自己添么?我们可以定制自己的ItemLoader,在items.py中:
from scrapy.loader import ItemLoader
点进去看,发现它可以配置一些默认的参数:
default_item_class = Item
default_input_processor = Identity()
default_output_processor = Identity()
default_selector_class = Selector
我们重载这个类:
class CnblogsArticleItemLoader(ItemLoader):
"""
定制ItemLoader
"""
default_output_processor = TakeFirst()
这样一来,我们在cnblogs.py中就不能使用ItemLoader了,而是要使用我们自己定制的CnblogsArticleItemLoader:
from Spider.items import CnblogsArticleItem, CnblogsArticleItemLoader
变:
item_loader = ItemLoader(item=CnblogsArticleItem(), response=response)
为:
item_loader = CnblogsArticleItemLoader(item=CnblogsArticleItem(), response=response)
现在可以删去为title和create_date添加的output_processor了:
# 标题
title = scrapy.Field(input_processor = MapCompose(lambda x: x + "——Cnblogs", add_author))
# 创建时间
create_date = scrapy.Field(input_processor = MapCompose(date_convert))
调试发现:
都变成单元素了,接下来实际上我们只需要关注input_processor了。但是front_image_url不应该是str,而应该是list,所以对于这个字段,我们可以使用一个小技巧,一个函数什么都不做,直接返回value,用它覆盖掉default_output_processor:
def return_value(value):
return value
front_image_url = scrapy.Field(output_processor = return_value)
因为只需要一个处理函数,所以没有使用MapCompose亦可。
只剩tags了,我们需要对它做一个join,而tags实际上也不该TakeFirst,我们只需要:
from scrapy.loader.processors import MapCompose, TakeFirst, Join
tags = scrapy.Field(output_processor=Join(","))
此时发现,一切都正常了:
这里再给出两个比较常用的input_processor,第一个实现从“ 1 收藏”“ 评论”“ 23 点赞”这样的str中提取出int值:
import re
def extract_num(value):
# 从字符串value中提取出数字
match_re = re.match(".*?(\d+).*", value)
if match_re:
nums = int(match_re.group(1))
else:
nums = 0
return nums
第二个实现从一个list中删除含"评论"的元素进行返回:
def remove_comment(value):
"""
value是列表,元素类型为str
"""
if "评论" in value:
return ""
else:
return value
因为后面还要开发别的网站的爬虫,pipelines.py中,我们曾定制了ImagesPipeline:
class ArticleImagesPipeline(ImagesPipeline):
def item_completed(self, results, item, info):
if isinstance(item, dict) or self.images_result_field in item.fields:
item[self.images_result_field] = [x for ok, x in results if ok]
image_file_path = []
for is_success, value_dict in results:
image_file_path.append(value_dict.get("path", ""))
item["front_image_path"] = image_file_path
return item
这个Pipeline是对所有item都适用的,其他网站所提取的字段如果没有front_image_url这一字段,就会抛异常,故而我们增加一个判断逻辑:
class ArticleImagesPipeline(ImagesPipeline):
def item_completed(self, results, item, info):
if isinstance(item, dict) or self.images_result_field in item.fields:
item[self.images_result_field] = [x for ok, x in results if ok]
if "front_image_url" in item:
image_file_path = []
for is_success, value_dict in results:
image_file_path.append(value_dict.get("path", ""))
item["front_image_path"] = image_file_path
return item
启动发现有时会报这种异常:
猜测是由于tags可能为空导致的,但这个报错没有url地址,我们无法验证,于是乎,在pipelines.py中MysqlTwistedPipline的handle_error中加一句:
print(item['url'])
便可以了,发现确实是因为tags为空导致的异常。
在navicat中为该字段设置一个默认值也是报错,看来只能是在scrapy中处理了。
进一步的,我们发现,出现异常的item中不含tags:
我们可以考虑在item进入到某个pipeline中,判断tags是否存在,若不存在,为其赋默认值。
pipelines.py中:
class SetDefalutForTagsPipeline(object):
def process_item(self, item, spider):
if "tags" in item:
pass
else:
item["tags"] = "无"
return item
配置它,这里的配置必须在保存之前:
ITEM_PIPELINES = {
'Spider.pipelines.SpiderPipeline': 300,
# 'scrapy.pipelines.images.ImagesPipeline': 200,
'Spider.pipelines.ArticleImagesPipeline': 200,
'Spider.pipelines.SetDefalutForTagsPipeline': 210,
# 'Spider.pipelines.JsonSavePipeline': 210,
'Spider.pipelines.JsonExporterPipleline': 220,
# 'Spider.pipelines.MysqlPipeline': 230,
'Spider.pipelines.MysqlTwistedPipline': 230,
}
再次运行,一切正常了。