Scrapy ImagesPipeline与FilesPipeline完全指南 - 多媒体资源下载与处理技术详解
📂 所属阶段:第二阶段 — 数据流转(数据处理篇)
🔗 相关章节:Pipeline管道实战 · 数据清洗与校验
目录
玩 Scrapy 的同学一定遇到过这样的场景:爬虫抓到了几百张商品大图,想顺手把图片下载到本地,还得生成不同尺寸的缩略图;或者需要把一堆 PDF、视频文件保存下来,同时自动去重,且文件名合理便于检索。如果每次都自己写下载逻辑,不仅重复造轮子,还容易踩坑。
Scrapy 内置的 ImagesPipeline 和 FilesPipeline 就是为这种多媒体下载场景而生的。它们可以自动完成 下载、去重、格式校验、路径管理 等一系列繁琐工作,让你的爬虫代码干净又高效。
本篇文章将带你从辨别两者差异开始,一步步走完基础配置、实战用法,再到高级自定义技巧,最后把常见问题一网打尽。看完之后,你完全可以驾驭任何多媒体资源的下载流水线。
ImagesPipeline vs FilesPipeline速览
新手经常把这两个 Pipeline 搞混。其实它们的定位非常清晰,记住这张对比表就行:
一句话总结:只下载图片用 ImagesPipeline,要处理混合文件或非图片资源用 FilesPipeline。 如果你既有图片又有其他文件,两个可以同时启用,Scrapy 会按优先级顺序执行。
💡 提示:ImagesPipeline 底层继承自 FilesPipeline,它额外增加了 PIL/Pillow 的图片处理能力,所以图片也能用 FilesPipeline 下载,但就享受不到缩略图、格式转换等福利了。
基础配置与最简实践
下面我们一步步把 Pipeline 跑起来。无论是图片还是文件,配置思路完全一致:启用 → 设置参数 → 定义 Item → Spider 产出 URL。
第一步:启用 Pipeline
在 settings.py 里把目标 Pipeline 加入到 ITEM_PIPELINES 字典中。数值代表优先级,数值越小越先执行。如果你两个都要,建议让 ImagesPipeline 先跑(纯属习惯,不是强制)。
# settings.py
ITEM_PIPELINES = {
# 只下载图片
# 'scrapy.pipelines.images.ImagesPipeline': 1,
# 只下载文件
# 'scrapy.pipelines.files.FilesPipeline': 1,
# 图片+文件并存
'scrapy.pipelines.images.ImagesPipeline': 1,
'scrapy.pipelines.files.FilesPipeline': 2,
}
第二步:配置核心参数
ImagesPipeline 常用配置
# settings.py - ImagesPipeline 相关配置
IMAGES_URLS_FIELD = 'image_urls' # Item 中存放图片 URL 列表的字段名(默认值)
IMAGES_RESULT_FIELD = 'images' # 下载完成后,图片信息写回的字段名(默认值)
IMAGES_STORE = './static/images' # 图片保存的本地根目录(支持 S3、FTP 等 URI)
IMAGES_EXPIRES = 90 # 图片缓存过期天数,过期后会重新下载(默认 90 天)
IMAGES_MIN_WIDTH = 100 # 宽度低于此值(px)的图片会被过滤
IMAGES_MIN_HEIGHT = 100 # 高度低于此值(px)的图片会被过滤
IMAGES_THUMBS = { # 自动生成指定尺寸的缩略图
'small': (100, 100),
'medium': (300, 300),
}
⚠️ 注意:IMAGES_MIN_WIDTH 和 IMAGES_MIN_HEIGHT 只能过滤下载成功后的图片,如果图片 URL 本身失效,过滤条件不会触发,需额外处理。
FilesPipeline 常用配置
# settings.py - FilesPipeline 相关配置
FILES_URLS_FIELD = 'file_urls' # Item 中存放文件 URL 列表的字段名(默认值)
FILES_RESULT_FIELD = 'files' # 下载完成后,文件信息写回的字段名(默认值)
FILES_STORE = './static/files' # 文件保存的本地根目录
FILES_EXPIRES = 90 # 文件缓存过期天数
📌 两个 Pipeline 的 *_URLS_FIELD 和 *_RESULT_FIELD 均可在 Item 中自定义命名,只需在 settings 里同步修改即可。
第三步:定义 Item
Item 中需要提供两个字段:一个存放 URL 列表,另一个用于接收下载结果。可以定义在同一个 Item 里,也可以分开,这里用一个示例统一展示:
# items.py
import scrapy
class MediaItem(scrapy.Item):
# 通用字段
id = scrapy.Field()
title = scrapy.Field()
# 图片专用
image_urls = scrapy.Field() # 列表,每个元素为图片 URL
images = scrapy.Field() # 下载完成后,Pipeline 自动填充结果列表
# 文件专用
file_urls = scrapy.Field() # 列表,每个元素为文件 URL
files = scrapy.Field() # 下载完成后,Pipeline 自动填充结果列表
Scrapy 的约定是:URL 字段必须是可迭代对象(通常为 list),即使只有一个 URL 也要写成列表。
第四步:Spider 中提取 URL 并 yield Item
最后在 Spider 里把抓到的 URL 填进 Item 并 yield 出去,Pipeline 即可自动接管后续下载流程。
# spiders/media_spider.py
import scrapy
from my_project.items import MediaItem
class ExampleSpider(scrapy.Spider):
name = 'media_demo'
start_urls = ['https://example.com/media-page']
def parse(self, response):
item = MediaItem()
item['id'] = '123'
item['title'] = '示例媒体包'
# 提取所有产品图片的 src
item['image_urls'] = response.css('.product-image::attr(src)').getall()
# 提取所有文件下载链接
item['file_urls'] = response.css('.download-link::attr(href)').getall()
yield item
运行爬虫后,你会在 ./static/images/full/ 下看到原始图片,thumbs/small/ 和 thumbs/medium/ 下看到对应缩略图;文件则会出现在 ./static/files/full/ 下。同时,item['images'] 和 item['files'] 会被填充为字典列表,包含 url、path、checksum 等信息,方便后续入库。
高频实用自定义技巧
默认 Pipeline 能覆盖约 80% 的下载需求,但现实世界中你大概率会遇到这些诉求:想自定义文件名、要带防反爬请求头、想把所有图片统一转成 WebP 格式…… 这时候就需要继承并重写对应方法了。
自定义文件名,告别哈希乱码
默认文件名是 URL 的 SHA1 哈希值,人类完全无法辨认。我们可以通过重写 file_path 方法,将文件名设计为 分类/时间戳-ID-原始文件名 的结构,既防重复又便于查找。
ImagesPipeline 自定义文件名
# pipelines.py
import scrapy
from scrapy.pipelines.images import ImagesPipeline
from itemadapter import ItemAdapter
from urllib.parse import urlparse
import time
import os
class CustomImagesPipeline(ImagesPipeline):
def file_path(self, request, response=None, info=None, *, item=None):
adapter = ItemAdapter(item)
item_id = adapter.get('id', 'unknown')
category = adapter.get('category', 'uncategorized')
# 从 URL 提取原始文件名
url_path = urlparse(request.url).path
original_name = os.path.basename(url_path)
if not original_name:
original_name = 'image.jpg'
timestamp = str(int(time.time()))
return f"{category}/{timestamp}-{item_id}-{original_name}"
def thumb_path(self, request, thumb_id, response=None, info=None, *, item=None):
# 基于主图路径,生成缩略图路径,例如 xxx_small.jpg
base_path = self.file_path(request, item=item)
name, ext = os.path.splitext(base_path)
return f"{name}_{thumb_id}{ext}"
🧠 小技巧:thumb_path 不重写也完全能用,但如果你希望缩略图和原图同名、仅加后缀,就需要像上面这样处理。
FilesPipeline 的自定义方式一模一样,只需将继承类改为 FilesPipeline 即可,这里不再赘述。
很多资源服务器会校验 Referer、User-Agent,甚至要求携带 Cookie。默认 Pipeline 生成的 Request 相当“裸”,我们必须重写 get_media_requests 来定制请求。
class CustomImagesPipeline(ImagesPipeline):
# 前面的 file_path、thumb_path 省略...
def get_media_requests(self, item, info):
adapter = ItemAdapter(item)
# 可从 Item 里传入当前页面的 URL,作为 Referer
referer = adapter.get('source_url', 'https://example.com')
for url in adapter.get(self.images_urls_field, []):
yield scrapy.Request(
url,
headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
'Referer': referer,
},
meta={
'download_timeout': 60, # 大文件可以适当加长超时
}
)
⚠️ 关于 Cookie:通常建议用 Scrapy 的全局 COOKIES_ENABLED=True 配合中间件管理,但若有特殊需要,也可以在这里通过 headers 或 cookies 参数传入。
统一图片格式为 WebP(进阶)
WebP 格式比 JPG/PNG 体积小 30%~80%,在现代化网站中已是标配。通过重写 convert_image 方法,我们可以将所有下载的图片统一转存为 WebP,同时完成必要的模式转换和尺寸缩放。
from PIL import Image
class CustomImagesPipeline(ImagesPipeline):
# ... 其他方法
def convert_image(self, image, size=None):
# 处理透明通道:如果原图为 RGBA / LA / P,直接保持或转为 RGBA(WebP 支持透明)
if image.mode in ('RGBA', 'LA'):
pass # 保持原样
elif image.mode == 'P':
image = image.convert('RGBA')
else:
# 如果不需要透明,也可以统一转 RGB,然后用白底填充(看需求)
pass
# 按需缩放
if size:
image = image.resize(size, Image.Resampling.LANCZOS)
# 注意:这里不主动做格式转换,真正的格式由 file_path 中扩展名决定
return image
同时,在 file_path 里把扩展名改成 .webp,Scrapy 在写入文件时会自动调用 Pillow 的 WebP 编码器。
def file_path(self, request, response=None, info=None, *, item=None):
# ... 构造路径
base_path = super().file_path(request, response, info, item=item)
# 把原有扩展名统一替换为 .webp
path_without_ext, _ = os.path.splitext(base_path)
return path_without_ext + '.webp'
至此,一套全自动的图片下载并转 WebP 的流水线就搭建完成了。
常见问题与解决方案
❓ 图片/文件下载失败,但没有任何日志告警
原因:默认 Pipeline 会忽略单条下载失败,只有当 Item 内所有 URL 都失败时才抛出 DropItem。
解决办法:
- 在爬虫运行时加上
--logfile=scrapy.log,然后查看 DEBUG 级别日志,搜索 File (images) download error。
- 也可以重写
media_failed 方法,主动记录失败 URL 和原因。
def media_failed(self, failure, request, info):
self.logger.error(f'下载失败: {request.url} - {failure.value}')
# 可选:将失败 URL 存入数据库或文件供后续重试
❓ 自定义文件名仍然出现覆盖
原因:文件名生成规则不够唯一,比如多个 Item 的 id 重复,或没有加入时间戳/哈希。
解决办法:在拼接文件名时,至少加入 时间戳 + Item 唯一 ID + URL 短哈希(前8位) 的组合。
from hashlib import sha1
url_hash = sha1(request.url.encode()).hexdigest()[:8]
file_name = f"{timestamp}-{item_id}-{url_hash}-{original_name}"
❓ 图片缓存过期不想重新下载,想永久保留
原因:IMAGES_EXPIRES / FILES_EXPIRES 设得太短。
解决办法:将过期天数设为一个极大值,例如 36500 天(100 年),或者干脆不限制(设为 0 表示永不过期,但 Scrapy 官方文档建议使用一个较大的数值以避免意外清理)。
IMAGES_EXPIRES = 36500 # 约 100 年
❓ 处理大图片时内存爆炸或进程崩溃
原因:未对图片尺寸或内存进行限制,一次性加载超大图片并缩放。
解决办法:
- 在
convert_image 中限制最大尺寸,例如若宽度或高度超过 2048px 则等比缩放。
- 主动释放中间变量(虽然 Python 有 GC,但处理大对象时主动
del 可加速回收)。
MAX_SIZE = 2048
def convert_image(self, image, size=None):
# 限制最大尺寸
if max(image.size) > MAX_SIZE:
image.thumbnail((MAX_SIZE, MAX_SIZE), Image.Resampling.LANCZOS)
# ... 其他处理
return image
❓ 缩略图生成后,原图仍然存在于 full 目录,想只保留缩略图
默认 Pipeline 会同时保留原图,如果你希望只保留缩略图(不保留原图),可以重写 item_completed 方法,在处理完成后手动删除原图文件。
import os
from scrapy.pipelines.images import ImagesPipeline
class ThumbOnlyPipeline(ImagesPipeline):
def item_completed(self, results, item, info):
# results 是 [(success, image_info), ...] 的列表
for ok, image_info in results:
if ok:
# 删除原图
original_path = os.path.join(self.store.basedir, image_info['path'])
if os.path.exists(original_path):
os.remove(original_path)
return item
⚠️ 注意:这种做法意味着你放弃了原图,再次需要原图时必须重新下载。请根据实际业务权衡。
写在最后
ImagesPipeline 和 FilesPipeline 是 Scrapy 送给多媒体爬虫开发者的一份厚礼。掌握好它们,你就能快速搭起一套健壮的资源下载流水线,把精力集中在数据提取和业务逻辑上。再结合自定义文件名、防反爬请求头、格式统一等技巧,完全可以应对绝大多数生产环境需求。
如果你在实践中还遇到了其他棘手问题,不妨回头看看官方文档,或者在代码中打印 info.spider 的相关属性,Scrapy 的 Pipeline 远比看上去要强大和灵活。祝你的爬虫下载无忧!
📌 延伸阅读:接下来可以了解一下 Item Pipeline 的更多玩法,以及如何对下载后的数据进行 清洗与校验。