上一节,我们的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中添加:
# 配置图片存放路径
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,我们需要保存的数据都填充好了,我们可以在这里做与数据保存相关的内容了。