bigywater.tech
Scrapy · 34

Feed 导出:FEEDS 的格式、批次与存储

把 item 导出成文件看起来只是一行配置,但中文会不会被转义?再跑一次会覆盖还是追加?csv 的列从哪来?怎么拆分成多个文件?这篇把 FEEDS 的常用选项都跑一遍,看真实的输出。

\uXXXX
json 格式默认把中文转义成这个样子(encoding 设 utf-8 才输出原字符)
追加
本地文件默认的写入方式;json 追加得到两个拼接的数组,不合法
10 / 10 / 5
25 个 item、batch_item_count=10 拆成三个文件的行数

格式

实测 · 本机执行输出
  o.json      422 字节  [ ⏎ {"id": 0, "name": "\u5546\u54c10", "price": 0.0, "tags": ["a", "b"], "note": "ok"}, …
  o.jsonl     415 字节  {"id": 0, "name": "\u5546\u54c10", "price": 0.0, "tags": ["a", "b"], "note": "ok"} ⏎ {"id": 1, "name": "\u5546\u54c11", "price": 1.5, "tags": ["a", "b"], "note": null} …
  o.csv       141 字节  id,name,price,tags,note ⏎ 0,商品0,0.0,"a,b",ok …
  o.xml       669 字节  <?xml version="1.0" encoding="utf-8"?> ⏎ <items> …
  o.pickle    432 字节  (二进制 432 字节)
  o.marshal   354 字节  (二进制 354 字节)
  stats:{'success_count/FileFeedStorage': 6}

FEEDS 是 {文件名: 选项} 的字典,format 决定格式。同一批 item 可以同时写多个文件。

编码

实测 · 本机执行输出
  a.json   170 字节  [ ⏎ {"id": 0, "name": "\u5546\u54c10", "price": 0.0, "tags": ["a", "b"], " ⏎ {"id": 1, "name": "\u5546\u54c11", "price": 1.5, "tags": ["a", "b"], " …
  b.json   158 字节  [ ⏎ {"id": 0, "name": "商品0", "price": 0.0, "tags": ["a", "b"], "note": "ok ⏎ {"id": 1, "name": "商品1", "price": 1.5, "tags": ["a", "b"], "note": nul …
  c.json   206 字节  [ ⏎ { ⏎   "id": 0, …
  d.csv     71 字节  id,name,price,tags,note ⏎ 0,商品0,0.0,"a,b",ok ⏎ 1,商品1,1.5,"a,b", …
  e.csv     71 字节  id,name,price,tags,note ⏎ 0,商品0,0.0,"a,b",ok ⏎ 1,商品1,1.5,"a,b", …
  只设 FEED_EXPORT_ENCODING=utf-8:f.json [ ⏎ {"id": 0, "name": "商品0", "price": 0.0, "tags": ["a", "b"], "note": "ok …

json 默认转义非 ASCII 字符,csv 默认就是 UTF-8;encoding 选项或 FEED_EXPORT_ENCODING 统一设置。

列与字段

实测 · 本机执行输出
  a.csv  price,id ⏎ 0.0,0 ⏎ 1.5,1 ⏎ 3.0,2 …
  b.json [ ⏎ {"name": "商品0", "id": 0}, ⏎ {"name": "商品1", "id": 1}, ⏎ {"name": "商品2", "id": 2} …
  c.csv  id,name,price,tags,note ⏎ 0,商品0,0.0,"a,b",ok ⏎ 1,商品1,1.5,"a,b", ⏎ 2,商品2,3.0,"a,b",ok …
  item 键不一致({a}、{a,b}、{b}):csv → a ⏎ 1 ⏎ 2 ⏎ "" …;jsonlines → {"a": 1} ⏎ {"a": 2, "b": "extra"} ⏎ {"b": "only-b"} ⏎ 

fields 选择并排序导出的字段;csv 不写 fields 时表头取第一个 item 的键。

追加与覆盖

实测 · 本机执行输出
  第 1 次运行后:o.jsonl 2 行;p.jsonl(overwrite=True)2 行;q.json 开头 [ …
  第 2 次运行后:o.jsonl 4 行;p.jsonl(overwrite=True)2 行;q.json 开头 [ …
  q.json(json 格式,默认追加)跑两次后:不合法:Extra data: line 4 column 2 (char 170)

本地文件默认追加,overwrite: True 才每次重写。json 格式追加会得到不合法的文件,最好 jsonlines 或 overwrite。

分批

实测 · 本机执行输出
  25 个 item,batch_item_count=10:['out-1-demo.jsonl', 'out-2-demo.jsonl', 'out-3-demo.jsonl'];每个文件行数 [10, 10, 5]
  stats:{'success_count/FileFeedStorage': 3}

batch_item_count 配合 %(batch_id)d 把输出拆成多个文件。

其它

实测 · 本机执行输出
  item_classes 里写不存在的类:all.jsonl 不存在;错误:ModuleNotFoundError: No module named 'nonexistent'
  item_classes: dict → dicts.jsonl 3 行;scrapy.Item → other.jsonl 0 行(item 都是 dict,被过滤掉)
  postprocessing=GzipPlugin,100 个 item:文件 864 字节,解压后 8606 字节,100 行
  FEEDS 的键是 “stdout:”(标准输出):{"id": 0, "name": "\u5546\u54c10", "price": 0.0, "tags": ["a | {"id": 1, "name": "\u5546\u54c11", "price": 1.5, "tags": ["a;stats {'success_count/StdoutFeedStorage': 1}
  没有任何 item 时(默认 store_empty=True):e1.json=4字节, e3.csv=0字节, e4.jsonl=0字节;e2.json(store_empty=False)不存在

item_classes 过滤类型,postprocessing 压缩,stdout: 输出到终端,store_empty 控制空结果是否创建文件。

分步演示

导出
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Scrapy 2.19.0 / Twisted 26.4.0 / lxml 6.1.3 / parsel 1.12.1(临时虚拟环境);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、重定向、压缩、断连等行为,并记录收到的原始请求;每个实验在临时目录里新建 Scrapy 项目或单文件 spider,命令输出来自真实的 scrapy 进程;计时用 time.perf_counter,数字每次运行会有小幅抖动。spider 在一个 data: 请求的回调里直接 yield 5~100 个 dict item,FEEDS 通过 -s FEEDS=JSON 传入,输出文件写在临时目录里
    • 没有验证:S3、FTP、GCS 等远程存储;scrapy.Item / dataclass 作为 item 时 csv 的列;FEED_URI 与 FEED_FORMAT(旧式单 feed 设置);item_export_kwargs 的其它选项;超大输出的内存占用;export 过程中进程被杀时文件是否完整
    京ICP备2026067502号-1京公网安备11010502063487号