Python 分布式爬虫框架 Scrapy 4-13 ItemLoader

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,
}

再次运行,一切正常了。

发布了101 篇原创文章 · 获赞 26 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/liujh_990807/article/details/100059448
今日推荐