Spider-第09讲-三格式落盘CSV-Excel-JSON

第 9 讲 · 三格式落盘:CSV / Excel / JSON

本讲目标 :把内存里的 list[dict],变成「同事能打开、程序能读、还能接着往下用」的文件。

三种格式各管一段:CSV 通用、Excel 交付、JSON 给程序。

配套代码 :spiders/s09_save_formats.py

运行 :python spiders/s09_save_formats.py

本讲产物 :data/output/ 下的 第09讲_books.csv、第09讲_books.xlsx、第09讲_图书报表.xlsx、第09讲_books_sample.json、第09讲_books.jsonl

---下载地址:git clone https://gitee.com/wuyikui555/python-spider.git

一、CSV:最通用,但有两个必踩的坑

爬完不存 = 白爬。而「存」这件事,很多人是这么写的:

python 复制代码
# 反面教材
with open("books.csv", "w") as f:          # 没有 encoding
    f.write("title,price\n")
    f.write("极简操作系统,116.0\n")

在 Windows 上用 Excel 打开这个文件,你会看到两件事同时发生:

  1. 中文列变成一串「烫烫烫」式的乱码;
  2. 每两行数据之间多出一整行空行。

它们都不报错,只是安静地让你的文件变成废品------这才是最麻烦的。

正确写法(utils/store.py):

python 复制代码
def save_csv(rows: list[dict], path: Path, fieldnames: list[str] | None = None) -> Path:
    path.parent.mkdir(parents=True, exist_ok=True)
    fields = fieldnames or list(rows[0].keys())
    with open(path, "w", newline="", encoding="utf-8-sig") as f:   # ← 两个关键参数
        writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(rows)
    return path

实测输出:

复制代码
写出:第09讲_books.csv  1072 字节
文件头三字节:b'\xef\xbb\xbf'  →  utf-8-sig(BOM) 已写入 ✔

坑 1:不加 BOM,Excel 打开中文就是乱码。
      解法:encoding='utf-8-sig'(多写三个字节,救回一整列中文)
坑 2:open 时不写 newline='',Windows 上每行之间会多一个空行。
      原因:csv 模块自己会写 \r\n,文本模式又把 \n 翻译成 \r\n。

两个坑的根因,一张表说清:

现象 根因 解法
Excel 打开中文乱码 Excel 不认「无 BOM 的 UTF-8」,它会按本机 GBK 猜 encoding="utf-8-sig",写入 3 字节 BOM EF BB BF
每行之间多空行 csv 模块写 \r\n,文本模式又把 \n 翻译成 \r\n → 变 \r\r\n open(..., newline="") 关掉翻译

utf-8-sig 这个编码名的意思是「UTF-8 with BOM」:写文件时会自动补上 EF BB BF 三字节,读文件时又会自动吃掉它们。上面那行 文件头三字节:b'\xef\xbb\xbf' 就是脚本自己读回来验证的------BOM 确实写进去了。

读回来时的一个真相:CSV 没有类型

复制代码
读回来 12 行,第一条:极简操作系统 / ¥116.0
★ 注意:读回来的 price 是字符串 '45.0'。CSV 没有类型,全是文本。

CSV 是一张纯文本表。写进去的 116.0,读出来是字符串 '116.0'(日志里那条 45.0 是脚本里写死的对照示例)。如果你读回来直接拿它做算术,得先 float()。 想要保住类型,就别用 CSV------往下看 JSON 和 SQLite。


二、列表字段:CSV 的单元格装不下列表

我们的数据里有 tags 这种字段,它的值是 ['彩印', '签名版', '畅销']------一个 list。

CSV 的每个格子只能装一个字符串。那列表怎么办?实测给了两个方案:

复制代码
原始数据 : ['彩印', '签名版', '畅销'] (list)
方案 A 拼字符串 : '彩印|签名版|畅销'  → 简单,但取出来要再 split
方案 B 存 JSON   : '["彩印", "签名版", "畅销"]'  → 保留结构,Excel 里看着丑

★ 本工程用方案 A:'畅销|新书'。原因是交付给同事的表格,
  可读性比结构完整性更重要。要还原就 split('|')。

两个方案的本质区别:

方案 存进去的样子 还原方式 适合谁
A 拼接 `彩印 签名版 畅销`
B JSON ["彩印", "签名版", "畅销"] json.loads(s) 程序二次处理

选哪个没有标准答案,但要提前定死 。最怕的是同一个字段,这批用 |、下批用 ,、再下批没人知道用了什么------文件就废了。

(顺带提醒:如果你选的拼接符是 ,,而它正好又是 CSV 的分隔符,那这个字段会被劈成好几列。用 |、; 或 \x1f 这种不常见字符更稳。)


三、Excel:交付场景的默认选择

要给领导、同事交东西,.xlsx 比 .csv 体面得多。原因就一个:

CSV 一个文件只能装一张表。而一份交付报表,往往既要明细、又要汇总。

本工程的做法是把「图书明细」和「分类统计」塞进同一个文件的两张 sheet:

python 复制代码
p2 = save_multi_sheet({"图书明细": rows, "分类统计": summary},
                      out_path("第09讲_图书报表.xlsx"))

实测输出:

复制代码
单表:第09讲_books.xlsx  5909 字节
多表:第09讲_图书报表.xlsx  6588 字节  (含 图书明细 / 分类统计 两张表)

    计算机      3 本  均价 ¥118.5
    文学       3 本  均价 ¥51.5
    历史       2 本  均价 ¥73.4
    经济       2 本  均价 ¥77.2
    科普       2 本  均价 ¥76.6

★ 为什么不是 CSV?因为 CSV 一个文件只能装一张表。
  交付报表时,Excel + 多 Sheet 是唯一体面的选择。

底层就是 pandas + openpyxl,一个 ExcelWriter 里循环 to_excel 即可(utils/store.py):

python 复制代码
def save_multi_sheet(sheets: dict[str, list[dict]], path: Path) -> Path:
    import pandas as pd
    path.parent.mkdir(parents=True, exist_ok=True)
    with pd.ExcelWriter(path, engine="openpyxl") as writer:
        for name, rows in sheets.items():
            pd.DataFrame(rows).to_excel(writer, index=False,
                                        sheet_name=name[:31] or "Sheet1")
    return path

Excel 的两个隐性坑

坑一:sheet 名有 31 字符上限。 上面代码里那个 name[:31] 不是装饰------Excel 硬性规定 sheet 名最多 31 个字符,超了 openpyxl 直接抛异常。截断 + or "Sheet1" 兜底,是为了防止你把一整句话当表名。

坑二:列宽是默认值,中文全被挤成「####」。 openpyxl 不会自动调列宽。交付前如果想好看一点,可以自己设:

python 复制代码
# 生产写法(本工程不做,因为示例数据短)
from openpyxl.utils import get_column_letter
for i, col in enumerate(df.columns, start=1):
    width = max(len(str(col)) * 2, *(len(str(v)) for v in df[col].astype(str)))
    ws.column_dimensions[get_column_letter(i)].width = min(width + 4, 60)

一个必须知道的表格安全问题:公式注入

CSV 和 Excel 都有这个问题,而且后果比乱码严重得多。

如果单元格的内容以 =、+、-、@ 开头,Excel / WPS 会把它当公式执行 。而爬来的文本是不可信的------万一某条数据的标题是以 =cmd|' /C calc'!A0 开头(老版本 DDE 攻击),同事双击打开表格的那一刻,公式就跑了。

防御很简单:导出前给这些字符开头的单元格加个前缀,让它变成纯文本。

python 复制代码
# 生产写法:导出前清洗
def _defuse(v: str) -> str:
    s = str(v)
    return "'" + s if s[:1] in ("=", "+", "-", "@") else s

rows = [{k: _defuse(v) for k, v in r.items()} for r in rows]

记住:只要数据来源不是你自己,导出成表格前就该走一遍这步。 本工程的靶场数据是安全的,所以示例里没加,但真实项目别省。


四、JSON 与 JSONL:给程序读的格式

给程序读的格式,首选 JSON。它保类型、保结构(列表还是列表),不会像 CSV 那样把 116.0 变成 '116.0'。

python 复制代码
def save_json(rows, path: Path, indent: int = 2) -> Path:
    path.write_text(json.dumps(rows, ensure_ascii=False, indent=indent),
                    encoding="utf-8")
    return path

实测输出(注意 ensure_ascii=False 的效果------中文原样保留,不是 \uXXXX):

复制代码
写出:第09讲_books_sample.json
内容片段:
[
  {
    "id": 1,
    "title": "极简操作系统",
    "author": "郑一鸣",
    "category": "计算机",
    "price": 116.0,
    "rating": 9.6,
    "stock": 83,
    "tags": [
      "彩印",
      "签名版",
      "畅销"
    ]
  },

这里 price 是 116.0(数字)、tags 是真正的数组------类型和结构全保住了,这是 CSV 给不了的。

但 JSON 有个规模问题:它是一个大数组,必须一次性读完。几万条还行,几百万条就会吃光内存。

于是有了 JSONL(JSON Lines)------每一行是一个独立的 JSON 对象:

复制代码
JSONL:第09讲_books.jsonl  每行一条,共 12 行
★ JSONL 的好处:进程崩了,已经写出去的几万行还在,不用从头再爬。
python 复制代码
with open(jsonl, "w", encoding="utf-8") as f:
    for r in rows:
        f.write(json.dumps(r, ensure_ascii=False) + "\n")

两种格式怎么选:

JSON JSONL
结构 一个大数组 [...] 一行一对象,无外层数组
读取 一次性 json.load,全进内存 逐行读,内存恒定
追加 要改整个文件 直接 open(..., "a") 追加一行
崩了之后 文件可能不完整,整个作废 已写出的行都还在
适合 几千条以内、要给人看结构 百万级、要边爬边存

一句话记忆:JSON 是「一次性交付」,JSONL 是「边爬边写」。


五、把 base_url 换成真实站点时,多留意这几处

本讲所有案例打的是本地靶场,编码、分页、字段都规规矩矩。换成真实站点,下面几点会立刻找你麻烦:

  • 编码不一定是 UTF-8。 国内不少老站还是 GBK/GB2312。存 CSV 时 encoding="utf-8-sig" 没问题(你统一转码),但读取响应 时要按 resp.encoding 走,否则解析阶段就已经乱码了。
  • 表头顺序会漂。 save_csv 默认 fieldnames=list(rows[0].keys())------如果某条数据的 key 顺序不同,或者缺字段,列就对不齐。真实项目里显式传 fieldnames,把顺序钉死。
  • 字段里可能混着 HTML。 有的接口把简介原样塞进 JSON,带着 <p> <br>。落盘前用 clean()(utils/parse.py)压一遍空白、剥掉标签。
  • 分页字段名五花八门。 靶场是 total_pages,别的地方可能是 pages、totalPage、last_page。翻页边界判断不能想当然。
  • 导出前先问一句「这份数据给谁看」。 给人看 → Excel;给程序看 → JSON/JSONL;要反复增量爬 → 直接进库(下一讲)。

本讲小结

能力 一句话要点
CSV 编码 encoding="utf-8-sig" 加 BOM,否则 Excel 打开中文乱码
CSV 换行 open(..., newline=""),否则 Windows 上多空行
CSV 类型 CSV 全是文本,读回来要自己 float();要保类型用 JSON
列表字段 提前定死分隔符(`
Excel 交付首选;多 sheet 塞明细 + 统计;注意 sheet 名 ≤ 31 字符、列宽要不要调
表格安全 单元格以 = + - @ 开头会被当公式执行,导出前 _defuse 一遍
JSON / JSONL JSON 一次性交付;JSONL 边爬边写、可流式、崩了不丢

下一讲

文件格式解决的是「怎么交出去 」。但只要你的爬虫会跑第二次,文件格式立刻暴露短板:

用 CSV 追加,跑两遍数据就翻倍;跑一百遍,你有 100 份一样的表,而且没人分得清哪行是最新的。

下一讲上数据库。核心只有一个词------幂等:同一个数据源,抓一次和抓一百次,结果一样。

我们会拆开 SQLite 的三种写法(INSERT / INSERT OR REPLACE / ON CONFLICT DO UPDATE),

看看到底哪个才是最安全的 upsert------其中有一个是「先删后插」的陷阱,用错了会悄悄丢字段。

相关推荐
青少儿编程课堂10 分钟前
蓝桥杯青少组图形化 + 全国青少年信息素养大赛 + 电子学会图形化考级:校园大富翁经营闯关解析
c++·python·算法·bfs·信息学竞赛
特立独行的猫A7 小时前
用仓颉语言 cj-tauri 框架,五分钟给娃做了个鸿蒙形近字游戏
java·后端·python
大可不必了11 小时前
03 函数定义和调用
开发语言·python
承渊政道12 小时前
【Python编程—从入门到实践】(记分与闯关:用Python+Pygame,打造越战越精彩的太空大战!)
开发语言·python·pycharm·游戏程序·pygame
爱学习的程序媛12 小时前
Python常用框架梳理
开发语言·后端·python·ai·web·后端框架·python框架
imgsq12 小时前
GESP Python 一到四级:备考路线与真题特点
python·青少年编程
eggwyw14 小时前
Anaconda安装卸载的详细流程指南
开发语言·python
2601_9679791214 小时前
纯手写:Python安装pip完整教程,搞了几小时,终于成功
python·教程·pip·安装·包管理
Norna14 小时前
缓存文件从没写出来:一个 .json 被当成目录用了
python