files_store必须用绝对路径,相对路径会导致静默跳过下载;文件名由file_path()方法决定,需继承filespipeline自定义;files_expires=0不禁止缓存而是立即过期,负值才强制重下载;并发时需防file_path冲突。

FILES_STORE 必须是绝对路径,相对路径会导致下载失败
Scrapy 的 FILES_STORE 配置项不支持相对路径(比如 "./downloads" 或 "files/"),运行时不会报错,但 FilesPipeline 会静默跳过文件下载,日志里只显示 File (ignored)。根本原因是 Scrapy 内部用 os.path.abspath() 解析路径,相对路径会被转成意外位置(如从 scrapy crawl 所在目录开始算),而该目录往往无写权限或根本不存在。
实操建议:
- 始终用
os.path.join(os.path.dirname(__file__), "files")构造绝对路径,放在settings.py中 - 启动前手动验证路径可写:
python -c "import os; print(os.access('你的绝对路径', os.W_OK))" - Linux/macOS 下注意 SELinux 或 sandbox 限制(Docker 容器尤其常见)
自定义文件名必须重写 file_path() 方法,不能靠 FILES_STORE 拼接
FILES_STORE 只指定根目录,最终文件名由 FilesPipeline.file_path() 返回值决定。默认实现用 URL 的 hash 命名(如 2a7e4f1b5d8c9e0a2f3b4c5d6e7f8a9b.jpg),无法满足按业务归档需求(比如按日期+ID+原始名)。直接改 FILES_STORE 值无效,它不参与文件名生成逻辑。
实操建议:
- 继承
FilesPipeline,重写file_path(self, request, response=None, info=None) - 从
request.meta传入上下文字段(如item_id,category),避免在file_path()里反查 item(此时 item 尚未传入 pipeline) - 原始文件名需从
response.headers.get("Content-Disposition")或request.url解析,不要硬编码扩展名
示例(提取 URL 中的 ID 并保留原扩展名):
def file_path(self, request, response=None, info=None):
url = request.url
filename = url.split("/")[-1]
if "." not in filename:
filename += ".bin"
return f"docs/{filename}"
FILES_EXPIRES 影响去重逻辑,设为 0 不代表禁用缓存
FILES_EXPIRES 控制已下载文件的缓存有效期(单位:天)。设为 0 表示“立即过期”,但 Scrapy 仍会检查文件是否存在并跳过下载——这看起来像“禁用缓存”,实际是“强制重命名冲突”。如果两个不同 URL 下载到同一 file_path(),后一个会覆盖前一个,且无警告。
实操建议:
- 需要严格去重时,确保
file_path()返回唯一值(例如拼接 URL hash 或时间戳) - 想彻底跳过本地检查,把
FILES_EXPIRES设为负数(如-1),Scrapy 会忽略本地文件直接重新下载 - 批量采集中若附件 URL 带临时 token,token 过期后
FILES_EXPIRES=0可能导致反复失败,应配合handle_httpstatus_list = [403, 404]处理
并发下载附件时,FILES_STORE 目录结构要防冲突
多个 spider 或高并发下,FilesPipeline 的文件写入不是原子操作。如果多个请求返回相同 file_path()(比如都解析出 report.pdf),可能产生文件损坏或大小异常(部分写入)。这不是 Scrapy bug,而是底层 OS 文件系统行为。
实操建议:
- 在
file_path()中加入随机后缀或时间戳微秒:f"docs/{int(time.time()*1000000)}_{filename}" - 用
os.makedirs(path_dir, exist_ok=True)确保子目录存在,避免因并发创建目录失败中断 - 生产环境建议将
FILES_STORE指向 NFS 或对象存储(通过自定义 Pipeline),避开本地文件系统瓶颈
真正麻烦的是归档逻辑本身:Scrapy 不管你下载完怎么分类、压缩、上传。这些必须在 item_completed() 或单独的 post-process 脚本里做,而且得自己处理失败重试和断点续传——这部分最容易被当成“Scrapy 已经做了”而漏掉。










