Python 分布式爬虫框架 Scrapy 4-9 图片下载以及图片路径的保存

上一节,我们的Item已经能传到pipeline,那么pipeline就能做很多处理。我们接下来继续完善item,因为我们可以看到,item中定义了的front_image_path和url_id是没有填充的。

对于front_image_path,一方面我们要下载图片并存储在某个路径之下,一方面我们要存储图片所存放的路径。

实际上,scrapy为我们提供了一个自动下载图片的机制,我们只需要配置即可使用,是以pipeline的形式提供的,下图是scrapy源码结构所展示的一些默认pipeline:

于是乎,我们在刚刚解除注释的ITEM_PIPELINES中,新增:

'scrapy.pipelines.images.ImagesPipeline': 200

ITEM_PIPELINES指的是我们所定义的Item会流经的所有处理类,其后的200表示pipeline处理顺序,越小越先执行。

此外,我们还要做一些配置,我们要告诉ImagesPipeline,我们所定义的CnblogsArticleItem中的哪一个字段是图片的url,这样它才能帮我们下载,同时我们还要指定它的下载路径。

对于指定图片的URL字段,我们只需要在settings.py中添加:

# 指定Item中的图片URL,供ImagesPipeline下载
IMAGES_URLS_FIELD = 'front_image_url'

对于配置图片存放路径,我们希望使用通用性较好的路径,故而在settings.py中引入:

import os

并获取当前项目文件(settings.py所在的文件夹)的路径:

# 当前文件所在文件夹(项目文件夹)的路径
PROJECT_DIR = os.path.abspath(os.path.dirname(__file__))

然后我们只需要在settings.py中添加:

扫描二维码关注公众号,回复: 8765929 查看本文章
# 配置图片存放路径
IMAGES_STORE = os.path.join(PROJECT_DIR, 'images')

同时建立一个images文件夹,与settings.py同级:

启动项目,看看是否能够下载图片并保存,发现报错:

ModuleNotFoundError: No module named 'PIL'

我们安装它:

pip install -i https://pypi.douban.com/simple pillow

再次运行,又报错:
 

raise ValueError('Missing scheme in request url: %s' % self._url)

这是因为,我们配置了:

IMAGES_URLS_FIELD = 'front_image_url'

之后,当Item传到pipeline的时候,这个字段会被当成list处理,所以在cnblogs.py中的parse_detail中,应该将:

article_item["front_image_url"] = front_image_url

改成:

article_item["front_image_url"] = [front_image_url]

运行,发现对于图片URL不存在的,是会报异常的,我们可以为没有封面的文章配置一个默认的URL,在settings.py中添加:

# 配置图片url不存在时的默认图片url
DEFAULT_IMAGE_URL = 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1566626671633&di=ba4e0040482738d23a6e045e9a8b7844&imgtype=0&src=http%3A%2F%2Fossimg.xinli001.com%2Fvisioncn%2F600x400%2FVCG41126333227.jpg%3Fx-oss-process%3Dimage%2Fquality%2CQ_80'

在cnblogs.py中引入它:

from Spider.settings import DEFAULT_IMAGE_URL

修改:

front_image_url = article_node_selector.xpath('div[@class="entry_summary"]/a/img/@src').extract_first("")

为:

front_image_url = article_node_selector.xpath('div[@class="entry_summary"]/a/img/@src').extract_first(DEFAULT_IMAGE_URL)

修改:

front_image_url = response.meta.get('front_image_url', '')

为:

front_image_url = response.meta.get('front_image_url', DEFAULT_IMAGE_URL)

发现还是会报异常,原因是有的图片的url的最前方没有添加:
 

https:

我们将刚刚做了修改的:

front_image_url = article_node_selector.xpath('div[@class="entry_summary"]/a/img/@src').extract_first(DEFAULT_IMAGE_URL)

进一步改成:

front_image_url = parse.urljoin(response.url, article_node_selector.xpath('div[@class="entry_summary"]/a/img/@src').extract_first(DEFAULT_IMAGE_URL))

启动,没有再报异常。

上面已经说了,对于图片,我们要完成两个任务,一个是下载图片并存放到指定目录,我们已经实现。那么下一个任务——保存存放路径该如何实现呢?

我们可以定制一个自己的pipeline来实现。

在pipelines.py中,引入图片下载的pipeline:

from scrapy.pipelines.images import ImagesPipeline

然后通过继承这个pipeline定制自己的pipeline:

class ArticleImagesPipeline(ImagesPipeline):
    pass

在编写逻辑之前,我们需要先读懂ImagePipeline中有哪些方法可以供我们重载拓展。

发现它功能很强大,有这些方法:

__init__
from_settings
file_downloaded
image_downloaded
get_images
convert_image
get_media_requests
item_completed
file_path
thumb_path

可以实现图片相关的转换和过滤等。比如我们只关心尺寸大于100*100的,我们可以在settings.py中配置(只是举个例子,如果有需要才设置):

# 下载图片的最小高度和宽度
IMAGES_MIN_HEIGHT = 100
IMAGES_MIN_WIDTH = 100

这两个变量在ImagesPipeline中的构造函数中被用于初始化了,里面的很多参数我们都可以进行配置。

比较重要的一个方法是get_media_requests:

def get_media_requests(self, item, info):
    return [Request(x) for x in item.get(self.images_urls_field, [])]

它所做的任务是对我们在settings.py中设置的:

IMAGES_URLS_FIELD = 'front_image_url'

做一个for循环,构成一个Request,交给下载器。这也映照了为什么我们需要将url放在list中。

我们真正需要重载的方法是item_completed,先把这个方法拷贝到自己编写的ArticleImagesPipeline中:

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]
        return item

在if处打断点,看看传入的参数的结构与内容。当然了为了让这个生效,我们需要修改ITEM_PIPELINES中的配置:

ITEM_PIPELINES = {
    'Spider.pipelines.SpiderPipeline': 300,
    # 'scrapy.pipelines.images.ImagesPipeline': 200,
    'Spider.pipelines.ArticleImagesPipeline': 200,
}

调试发现,在result中有path的值:

所以我们将item_completed重载为:

    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

最后把item进行返回,是因为下一个pipeline还需要进行处理。

我们配置pipeline的执行顺序时,ArticleImagesPipeline是先于SpiderPipeline执行的,故而当运行到SpiderPipeline时,item中应该已经有了front_image_path的信息,调试发现确实如此:

需要注意的是front_image_path同front_image_url一样,都是list类型。

最后一个未填充的字段是url_id,我们的目的对文章的url做一个md5,转成定长的数据存储。一旦有了一个转换函数,我们可以在cnblogs.py中的parse_details中就进行填充。

我们可以新建一个与settings.py同级的包utils,其下新建一个python脚本common.py存放一些通用的工具函数,编辑脚本为:

import hashlib


def get_md5(url):
    if isinstance(url, str):
        url = url.encode("utf-8")
    m = hashlib.md5()
    m.update(url)
    return m.hexdigest()


if __name__ == "__main__":
    print(get_md5("https://www.baidu.com"))

运行发现报错:

TypeError: Unicode-objects must be encoded before hashing

改进为:

import hashlib


def get_md5(url):
    if isinstance(url, str):
        url = url.encode("utf-8")
    m = hashlib.md5()
    m.update(url)
    return m.hexdigest()


if __name__ == "__main__":
    print(get_md5("http://jobbole.com".encode("utf-8")))

运行成功,把它引入到cnblogs.py中:

from Spider.utils.common import get_md5

在parse_detail中的利用解析出的值填充实例化的CnblogsArticleItem对象部分添加:

article_item["url_id"] = get_md5(response.url)

再次调试:

发现到了SpiderPipeline,我们需要保存的数据都填充好了,我们可以在这里做与数据保存相关的内容了。

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

猜你喜欢

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