下载与处理文件和图片
Scrapy 提供可复用的 item pipeline,用于下载某个条目关联的文件。例如,抓取商品时也希望将商品图片下载到本地。这些管道共享部分功能和结构,统称为媒体管道;通常会选择 Files Pipeline 或 Images Pipeline。[1]
这两种管道都实现了以下功能:
-
避免重复下载最近已经下载的媒体文件。
-
指定媒体存储位置:文件系统目录、FTP 服务器、Amazon S3 存储桶或 Google Cloud Storage 存储桶。
图片管道还提供一些图片处理功能:
-
将所有下载的图片转换为统一格式 JPG 和颜色模式 RGB。
-
生成缩略图。
-
检查图片的宽度和高度,确保满足最小尺寸要求。
管道还维护一个内部队列,记录当前已安排下载的媒体 URL;当包含相同媒体的响应到达时,会关联到该队列。这样多个条目共享同一媒体时,就不会重复下载。
使用文件管道
FilesPipeline 的典型工作流程如下:
-
在 Spider 中抓取条目,将所需文件的 URL 放入
file_urls字段。 -
Spider 返回条目,条目进入 item pipeline。
-
条目到达
FilesPipeline时,file_urls中的 URL 会由 Scrapy 标准下载器下载,因此会经过下载器中间件,但不会经过 Spider 中间件。条目将一直停留在该管道阶段,直到文件下载完成或因某种原因失败。 -
下载后,结果填入另一个字段
files。它是字典列表,每个字典包含下载路径、原始抓取 URL(来自file_urls)、文件校验和与状态等信息。files中的文件顺序与原file_urls相同。如果某个文件下载失败,会记录错误,该文件也不会出现在files中。
使用图片管道
注意
需要安装 images 可选依赖组。[2]
ImagesPipeline 的用法与 FilesPipeline 很相似,但默认字段名不同:图片 URL 放在 image_urls 中,下载后图片的信息填入 images 字段。[3]
用 ImagesPipeline 处理图片的优势是可以配置额外功能,例如生成缩略图、按尺寸过滤图片。[3]
启用媒体管道
要启用媒体管道,首先将其加入项目的 ITEM_PIPELINES 设置。[4]
图片管道配置如下:
ITEM_PIPELINES = {"scrapy.pipelines.images.ImagesPipeline": 1}
文件管道配置如下:
ITEM_PIPELINES = {"scrapy.pipelines.files.FilesPipeline": 1}
注意
文件管道与图片管道可以同时使用。
随后,必须为目标存储设置配置有效值,用来存放下载的图片。否则,即使已经加入 ITEM_PIPELINES,管道仍然处于禁用状态。[4]
文件管道需要设置 FILES_STORE:[5]
FILES_STORE = "/path/to/valid/dir"
图片管道需要设置 IMAGES_STORE:[6]
IMAGES_STORE = "/path/to/valid/dir"
文件命名
默认文件名
默认情况下,文件名由 URL 的 SHA-1 哈希生成。[7]
例如,下面这个图片 URL:
http://www.example.com/image.jpg
其 SHA-1 哈希为:
3afec3b4765f8f0a07b78f98c07b83f013567a0a
图片会使用你选择的存储方式,以如下文件名下载并保存:[8]
3afec3b4765f8f0a07b78f98c07b83f013567a0a.jpg
自定义文件名
你可能希望使用其他计算规则来命名文件。例如,在名称中加入元数据,便于对图片分类。
覆盖媒体管道的 file_path 方法,即可自定义文件名。
例如,某图片管道接收到以下 URL:
http://www.example.com/product/images/large/front/0000000004166
可以将它处理为包含较短哈希和 front 视角信息的文件名:
00b08510e4_front.jpg
相应的 file_path 实现如下:
import hashlib def file_path(self, request, response=None, info=None, *, item=None): image_url_hash = hashlib.shake_256(request.url.encode()).hexdigest(5) image_perspective = request.url.split("/")[-2] image_filename = f"{image_url_hash}_{image_perspective}.jpg" return image_filename
警告
如果自定义命名方案依赖的元数据在不同抓取之间发生变化,已有媒体可能会以新文件名再次下载,造成意外的重复下载。
例如,文件名包含商品标题,而站点在两次抓取之间修改了标题,Scrapy 就会使用更新后的文件名重新下载同一媒体。
有关 file_path 的更多说明,参见“扩展媒体管道”。[9]
根据响应命名文件
file_path 也会接收 response,因此可以根据响应数据命名。例如,对于 URL 不以文件名结尾的情况,可以从 Content-Type 响应头确定扩展名:
import mimetypes from scrapy.pipelines.files import FilesPipeline class ContentTypeFilesPipeline(FilesPipeline): def file_path(self, request, response=None, info=None, *, item=None): path = super().file_path(request, response, info, item=item) if response is None: return path content_type = response.headers["Content-Type"].decode() return path + (mimetypes.guess_extension(content_type) or "")
这要求将 FILES_EXPIRES 设为 0。为了判断文件是否已经下载,Scrapy 会在下载之前调用 file_path,此时 response 为 None,然后检查所得路径中文件的保存时间。依赖响应的路径永远无法匹配这次检查。设置 FILES_EXPIRES 为 0 可以禁用检查,代价是每次运行都会重新下载所有文件。[10][10]
设置请求头
媒体请求是根据条目构建的,并不来源于产出该条目的响应,因此 RefererMiddleware 等功能不会作用于它们。要发送 Referer 等请求头,可以在 Spider 中把值存入条目,再在自定义媒体管道的 get_media_requests 中读取:[11]
from scrapy import Request, Spider from scrapy.pipelines.files import FilesPipeline class MySpider(Spider): def parse(self, response): yield { "file_urls": response.css("a.file::attr(href)").getall(), "referrer": response.url, } class MyFilesPipeline(FilesPipeline): def get_media_requests(self, item, info): for file_url in item["file_urls"]: yield Request(file_url, headers={"Referer": item["referrer"]})
支持的存储方式
文件系统
文件系统存储会将文件保存到如下路径:
<IMAGES_STORE>/full/<FILE_NAME>
其中:
FTP 服务器
FILES_STORE 和 IMAGES_STORE 可以指向 FTP 服务器。Scrapy 会自动将文件上传到该服务器。[5][6]
ftp://username:password@address:port/path ftp://address:port/path
如果未提供 username 和 password,则分别使用 FTP_USER 和 FTP_PASSWORD 设置。[14][15]
FTP 支持主动和被动两种连接模式。Scrapy 默认使用被动模式;要改用主动模式,将 FEED_STORAGE_FTP_ACTIVE 设为 True。[16]
Amazon S3
注意
需要安装 s3 可选依赖组。[17]
FILES_STORE 和 IMAGES_STORE 可以表示 Amazon S3 存储桶,Scrapy 会自动上传文件。[5][6]
以下就是有效的 IMAGES_STORE 值:[6]
IMAGES_STORE = "s3://bucket/images"
可以通过 FILES_STORE_S3_ACL 和 IMAGES_STORE_S3_ACL 修改所存文件的访问控制列表(ACL)策略。默认值是 private;若要公开文件,使用 public-read:[18][19]
IMAGES_STORE_S3_ACL = "public-read"
更多信息见 Amazon S3 开发者指南中的预定义 ACL。[20]
也可以使用其他兼容 S3 的存储,例如自行部署的 Minio 或 Zenko CloudServer,只需在 Scrapy 设置中指定端点:[21][22]
AWS_ENDPOINT_URL = "http://minio.example.com:9000"
原文还列出了自行部署时不使用 SSL、不验证 SSL 连接的配置:
AWS_USE_SSL = False # or True (None by default) AWS_VERIFY = False # or True (None by default)
若要为并行检查或上传的文件复用足够多的连接,应相应设置 AWS_MAX_POOL_CONNECTIONS。[23]
Google Cloud Storage
注意
需要安装 gcs 可选依赖组。[24]
FILES_STORE 和 IMAGES_STORE 可以表示 Google Cloud Storage 存储桶,Scrapy 会自动上传文件。[5][6]
以下是有效的 IMAGES_STORE 与 GCS_PROJECT_ID 设置示例:[6][25]
IMAGES_STORE = "gs://bucket/images/" GCS_PROJECT_ID = "project_id"
认证方式参见相关文档。[26]
可以通过 FILES_STORE_GCS_ACL 和 IMAGES_STORE_GCS_ACL 修改文件的 ACL 策略。默认是空字符串 '',表示 Cloud Storage 对对象应用存储桶的默认对象 ACL。若要公开文件,使用 publicRead:[27][28]
IMAGES_STORE_GCS_ACL = "publicRead"
更多信息见 Google Cloud Platform 开发者指南中的预定义 ACL。[29]
使用示例
使用媒体管道前,先启用它。[30]
随后,Spider 返回的条目只要带有 URL 字段——文件管道为 file_urls,图片管道为 image_urls——管道就会将结果写入相应的 files 或 images 字段。[31]
如果条目类型要求预先定义字段,必须同时定义 URL 字段和结果字段。例如图片管道的条目必须包含 image_urls 与 images。下面以 dataclass 为例:[32]
from dataclasses import dataclass, field @dataclass class MyItem: # ... other item fields ... image_urls: list[str] = field(default_factory=list) images: list[dict] = field(default_factory=list)
URL 键和结果键也可以使用其他字段名,通过覆盖设置即可实现。
文件管道使用 FILES_URLS_FIELD 和/或 FILES_RESULT_FIELD:[33][34]
FILES_URLS_FIELD = "field_name_for_your_files_urls" FILES_RESULT_FIELD = "field_name_for_your_processed_files"
图片管道使用 IMAGES_URLS_FIELD 和/或 IMAGES_RESULT_FIELD:[35][36]
IMAGES_URLS_FIELD = "field_name_for_your_images_urls" IMAGES_RESULT_FIELD = "field_name_for_your_processed_images"
如果需要更复杂的功能并覆盖管道行为,参见“扩展媒体管道”。[9]
如果有多个继承 ImagesPipeline 的管道,并希望各自采用不同设置,可以在设置键前加上管道类名的大写形式。例如管道名为 MyPipeline,要自定义 IMAGES_URLS_FIELD,就定义 MYPIPELINE_IMAGES_URLS_FIELD,该管道会使用此自定义值。[3][35]
附加功能
文件过期
图片管道会避免重复下载最近下载过的文件。通过 FILES_EXPIRES,或图片管道的 IMAGES_EXPIRES,调整保留时间,单位为天:[10][37]
# 120 days of delay for files expiration FILES_EXPIRES = 120 # 30 days of delay for images expiration IMAGES_EXPIRES = 30
这两个设置的默认值都是 90 天。
如果自定义管道继承 FilesPipeline,并希望使用独立设置,可以在键前加上大写类名。例如 MyPipeline 使用:
MYPIPELINE_FILES_EXPIRES = 180
这样 MyPipeline 的过期时间就是 180 天。
系统根据文件最后修改时间计算已经过的天数,再与过期时间比较,判断文件是否过期。
生成图片缩略图
图片管道可以自动为下载的图片生成缩略图。
将 IMAGES_THUMBS 设置为字典即可启用:键是缩略图名称,值是其尺寸。[38]
例如:
IMAGES_THUMBS = { "small": (50, 50), "big": (270, 270), }
启用后,图片管道会按每个指定尺寸生成缩略图,路径格式如下:
<IMAGES_STORE>/thumbs/<size_name>/<image_id>.jpg
其中:
使用 small 与 big 名称时,保存的图片文件示例如下:
<IMAGES_STORE>/full/63bbfea82b8880ed33cdb762aa11fab722a90a24.jpg <IMAGES_STORE>/thumbs/small/63bbfea82b8880ed33cdb762aa11fab722a90a24.jpg <IMAGES_STORE>/thumbs/big/63bbfea82b8880ed33cdb762aa11fab722a90a24.jpg
第一项是从站点下载的完整图片。
过滤过小的图片
使用图片管道时,可以通过 IMAGES_MIN_HEIGHT 和 IMAGES_MIN_WIDTH 指定允许的最小尺寸,丢弃过小的图片。[39][40]
例如:
IMAGES_MIN_HEIGHT = 110 IMAGES_MIN_WIDTH = 110
注意
这些尺寸限制完全不会影响缩略图生成。
可以只限制一个方向,也可以同时限制宽高。同时设置时,只保存两个最小尺寸都满足的图片。在上面的例子中,105×105、105×200、200×105 的图片都会被丢弃,因为至少有一个方向小于限制。
默认不限制尺寸,会处理所有图片。
允许重定向
默认情况下,媒体管道忽略重定向:媒体 URL 请求若得到 HTTP 重定向,会被视为下载失败。
要处理媒体重定向,将以下设置改为 True:
MEDIA_ALLOW_REDIRECTS = True
扩展媒体管道
自定义文件管道可以覆盖以下方法:
- class scrapy.pipelines.files.FilesPipeline[source]
-
- file_path(self, request, response=None, info=None, *, item=None)[source]
-
每个下载条目都会调用一次此方法。它返回指定
response对应文件的下载路径。[41]除
response外,此方法还接收原始request、info和item。[42][43]可以覆盖此方法,自定义每个文件的下载路径。
例如,文件 URL 以普通路径结尾,如
https://example.com/a/b/c/foo.png,可以使用下面的方法,将所有文件按原文件名下载到files文件夹,例如files/foo.png:from pathlib import PurePosixPath from scrapy.utils.httpobj import urlparse_cached from scrapy.pipelines.files import FilesPipeline class MyFilesPipeline(FilesPipeline): def file_path(self, request, response=None, info=None, *, item=None): return "files/" + PurePosixPath(urlparse_cached(request).path).name
同样,也可以根据
item的某个属性确定路径,或者使用response;后者参见“根据响应命名文件”。[44]默认情况下,
file_path()返回full/<request URL hash>.<extension>。[45]
- get_media_requests(item, info)[source]
-
如工作流程所述,管道调用此方法,从条目中取得待下载文件的请求。可以覆盖此方法,改变返回的请求:
from itemadapter import ItemAdapter def get_media_requests(self, item, info): adapter = ItemAdapter(item) for file_url in adapter["file_urls"]: yield scrapy.Request(file_url)
也可以用它设置请求头,参见“设置请求头”。[46]
这些请求由管道处理,下载完成后,结果作为二元元组列表传给
item_completed()。每个元组为(success, file_info_or_error),其中:[47]-
success为布尔值:图片下载成功时为True,因某种原因失败时为False。 -
成功时
file_info_or_error是包含以下键的字典;发生问题时则为Failure。[48]
传入
item_completed()的元组列表保证与get_media_requests()返回的请求顺序相同。[47][49]results参数的典型值如下:[ ( True, { "checksum": "2b00042f7481c7b056c4b410d28f33cf", "path": "full/0a79c461a4062ac383dc4fade7bc09f1384a3910.jpg", "url": "http://www.example.com/files/product1.pdf", "status": "downloaded", }, ), (False, Failure(...)), ]
-
- item_completed(results, item, info)[source]
-
单个条目的所有文件请求完成后,无论成功下载还是失败,都会调用
FilesPipeline.item_completed()。[52]item_completed()必须返回传给后续条目管道阶段的输出。因此与其他管道一样,必须返回条目,或者丢弃条目。[47]下面的示例把 results 中的文件路径保存到条目的
file_paths字段;如果没有任何文件,则丢弃该条目:[47]from itemadapter import ItemAdapter from scrapy.exceptions import DropItem def item_completed(self, results, item, info): file_paths = [x["path"] for ok, x in results if ok] if not file_paths: raise DropItem("Item contains no files") adapter = ItemAdapter(item) adapter["file_paths"] = file_paths return item
默认情况下,
item_completed()返回条目。[47]
自定义图片管道可以覆盖以下方法:
- class scrapy.pipelines.images.ImagesPipeline[source]
-
ImagesPipeline扩展了FilesPipeline,定制字段名,并添加针对图片的行为。[3]- file_path(self, request, response=None, info=None, *, item=None)[source]
-
每个下载条目都会调用一次此方法,返回指定
response对应文件的下载路径。[41]除
response外,此方法还接收原始request、info和item。[42][43]可以覆盖此方法来自定义每个文件的下载路径。
例如,文件 URL 以普通路径结尾,如
https://example.com/a/b/c/foo.png,可以用下面的方法按原文件名下载到files文件夹,例如files/foo.png:from pathlib import PurePosixPath from scrapy.utils.httpobj import urlparse_cached from scrapy.pipelines.images import ImagesPipeline class MyImagesPipeline(ImagesPipeline): def file_path(self, request, response=None, info=None, *, item=None): return "files/" + PurePosixPath(urlparse_cached(request).path).name
同样,可以根据
item的属性或response确定路径;后者见“根据响应命名文件”。[44]默认
file_path()返回full/<request URL hash>.<extension>。[53]
- thumb_path(self, request, thumb_id, response=None, info=None, *, item=None)[source]
-
每个下载条目会针对
IMAGES_THUMBS中的每一项调用此方法,返回指定response对应图片的缩略图下载路径。[38][41]除
response外,此方法还接收原始request、thumb_id、info和item。[42][43]可以覆盖此方法自定义每张图片的缩略图路径,也可以根据
item的某个属性确定路径。默认
thumb_path()返回thumbs/<size name>/<request URL hash>.<extension>。[54]
访问下载失败时的响应
下载失败时,item_completed() 接收到一个 Failure,其 value 是 media_downloaded() 抛出的异常。对于非 2xx 响应,该异常是只用消息构造的 FileException,不保留响应引用。若要访问响应,例如把 status 记录到条目中,可以覆盖 media_downloaded(),将响应附加到自定义异常:[48]
from scrapy.pipelines.files import FilesPipeline from scrapy.pipelines.media import FileException class ResponseFileException(FileException): def __init__(self, response): super().__init__(f"download-error ({response.status})") self.response = response class MyFilesPipeline(FilesPipeline): async def media_downloaded(self, response, request, info, *, item=None): try: return await super().media_downloaded(response, request, info, item=item) except FileException: raise ResponseFileException(response) def item_completed(self, results, item, info): errors = [ value.value.response.status for ok, value in results if not ok and value.check(ResponseFileException) ] if errors: item["download_errors"] = errors return super().item_completed(results, item, info)
自定义图片管道示例
下面是前述图片管道方法的完整示例:
import scrapy from itemadapter import ItemAdapter from scrapy.exceptions import DropItem from scrapy.pipelines.images import ImagesPipeline class MyImagesPipeline(ImagesPipeline): def get_media_requests(self, item, info): for image_url in item["image_urls"]: yield scrapy.Request(image_url) def item_completed(self, results, item, info): image_paths = [x["path"] for ok, x in results if ok] if not image_paths: raise DropItem("Item contains no images") adapter = ItemAdapter(item) adapter["image_paths"] = image_paths return item
要启用自定义媒体管道组件,必须将其类导入路径加入 ITEM_PIPELINES,例如:[4]
ITEM_PIPELINES = {"myproject.pipelines.MyImagesPipeline": 300}
基于内容的图片过滤管道
这个示例覆盖 get_images(),使用 TensorFlow 模型等分类器过滤图片。将你的分类逻辑写入 is_valid_image():[57]
from scrapy.pipelines.images import ImagesPipeline, ImageException class ImageClassifierPipeline(ImagesPipeline): def is_valid_image(self, image): raise NotImplementedError def get_images(self, response, request, info, *, item=None): for path, image, buf in super().get_images(response, request, info, item=item): if not self.is_valid_image(image): raise ImageException("Image does not match criteria") yield path, image, buf











暂无评论内容