下载与处理文件和图片

下载与处理文件和图片

Scrapy 提供可复用的 item pipeline,用于下载某个条目关联的文件。例如,抓取商品时也希望将商品图片下载到本地。这些管道共享部分功能和结构,统称为媒体管道;通常会选择 Files Pipeline 或 Images Pipeline。[1]

这两种管道都实现了以下功能:

  • 避免重复下载最近已经下载的媒体文件。

  • 指定媒体存储位置:文件系统目录、FTP 服务器、Amazon S3 存储桶或 Google Cloud Storage 存储桶。

图片管道还提供一些图片处理功能:

  • 将所有下载的图片转换为统一格式 JPG 和颜色模式 RGB。

  • 生成缩略图。

  • 检查图片的宽度和高度,确保满足最小尺寸要求。

管道还维护一个内部队列,记录当前已安排下载的媒体 URL;当包含相同媒体的响应到达时,会关联到该队列。这样多个条目共享同一媒体时,就不会重复下载。

使用文件管道

FilesPipeline 的典型工作流程如下:

  1. 在 Spider 中抓取条目,将所需文件的 URL 放入 file_urls 字段。

  2. Spider 返回条目,条目进入 item pipeline。

  3. 条目到达 FilesPipeline 时,file_urls 中的 URL 会由 Scrapy 标准下载器下载,因此会经过下载器中间件,但不会经过 Spider 中间件。条目将一直停留在该管道阶段,直到文件下载完成或因某种原因失败。

  4. 下载后,结果填入另一个字段 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>

其中:

  • <IMAGES_STORE> 是图片管道 IMAGES_STORE 设置指定的目录。[6]

  • full 是用于区分完整图片与缩略图的子目录(如果启用了缩略图)。详见“生成图片缩略图”。[12]

  • <FILE_NAME> 是分配给文件的名称,详见“文件命名”。[13]

FTP 服务器

FILES_STORE 和 IMAGES_STORE 可以指向 FTP 服务器。Scrapy 会自动将文件上传到该服务器。[5][6]

这两个设置应采用以下形式之一:[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

其中:

  • <size_name> 是 IMAGES_THUMBS 字典中的键,例如 small、big。[38]

  • <image_id> 是图片 URL 的 SHA-1 哈希。[7]

使用 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]

    • url:下载文件的 URL,也就是 get_media_requests() 返回的请求 URL。[49]

    • path:文件保存路径,相对于 FILES_STORE。[5]

    • checksum:图片内容的 MD5 哈希。[50]

    • status:文件状态。

      可能取以下值:

      • downloaded:文件已下载。

      • uptodate:依据过期策略,文件最近已经下载,因此本次未重新下载。

      • cached:文件来自缓存,响应带有 cached 标记,例如由 HttpCacheMiddleware 提供。[51]

传入 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]

get_media_requests(item, info)[source]

工作方式与 FilesPipeline.get_media_requests() 相同,但图片 URL 使用不同的字段名。

必须为每个图片 URL 返回一个 Request。

item_completed(results, item, info)[source]

单个条目的所有图片请求完成后,无论成功还是失败,都会调用 ImagesPipeline.item_completed()。[55]

工作方式与 FilesPipeline.item_completed() 相同,但使用不同字段名来保存图片下载结果。

默认 item_completed() 返回条目。[56]

访问下载失败时的响应

下载失败时,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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容