🐍 第7章 保存文件:纯文本、CSV 与 Google 试算表
📄 一句话概括

CSV 就是「逗号分隔的纯文本」,没有任何格式、没有类型、没有颜色。正因为什么都没有,它才谁也认。 读回来的每个格子都是字符串,包括数量和单价。要算钱得自己 int() 或 float()。 Excel 打开 CSV 乱码,是因为文件少了三个字节的 BOM。写 utf-8-sig 就有,写 utf-8 就没有。

📝 本章和前面几章的关系

上一篇把数据装进了字典,这一篇把它落到文件里。 教材第 7 章后三节是【创建 CSV 文件】【使用 Google 云端试算表保存数据】 还有一节【从 Python 程序访问 Google 试算表】。 CSV 能真跑, 云试算表不行——那一步需要 Google 账号和服务账号密钥,标了「平台原样 · 本沙箱不执行」。 教材把云试算表放在这一章,想说的一句话是: 存储介质可以换,数据的形状不变。不管存 CSV 还是存云端表格,你脑子里那张表还是四列。

一、CSV 是什么

CSV 的全名是逗号分隔值。一行一条记录,一行里的字段用逗号隔开,第一行是表头。就这样,没有别的规矩。 演示第一段写出来的文件 176 字节,打开就是四行数据加一行表头:日期,品名,数量,单价,下面是四行。 它没有任何格式、没有类型、没有颜色、没有合并单元格。 正因为什么都没有,它才谁也认——Excel、 Numbers、数据库导入工具、老掉牙的报表系统,全都能读。

写的时候用 csv.writer,不是自己拼字符串。为什么?因为字段里可能有逗号。 品名写成「螺栓, 加长版」,你自己拼就多出一列,csv.writer 会给它加引号。 还有一个细节:Windows 上要写 newline='',不加的话每行会多一个空行。 这是 csv 模块文档里明确要求的,不是可选。

二、读回来全是字符串

演示第二段读回来 5 行(含表头),第一行是表头, 第二行是 ['2026-11-01', '六角螺栓 M8', '120', '1.5']。 注意 '120' 和 '1.5' 都带引号——它们是字符串,不是数字。 CSV 里没有类型这回事。文件里就是一堆字符,谁是数字谁不是,全靠你解读。 所以算钱之前必须自己 int() 或 float()。

这也是 CSV 和 JSON 的一个根本差别: JSON 有类型,120 是数字、"120" 是字符串;CSV 全是字符串。 坏处是麻烦,好处是简单。而且因为它简单,出问题的时候一眼能看出来——用记事本打开就知道哪一行错了。 JSON 少一个括号,你得数半天。

三、用表头当键,取起来清楚

csv.reader 给的是列表,取值得数下标:row[1] 是品名。跟第 7.1 篇那个问题一样。 csv.DictReader 把每行变成字典,键就是表头。 演示里打出来是 {'日期': '2026-11-01', '品名': '六角螺栓 M8', '数量': '120', '单价': '1.5'}。 最大的好处是「以后表里插一列,按名字取的代码不用改」。 用下标的代码,插一列全错位。

写的时候也有对称的 DictWriter:先给 fieldnames,然后每行传一个字典。 它负责按表头顺序排好、该加引号的加引号。 用 DictReader/DictWriter 是处理表格数据的默认选择。 只有确定格式永不变的小脚本, 才值得省事用下标。

四、编码:Excel 打开是乱码的原因

这是 CSV 最常见的一个坑。你用 Python 写了个 CSV,中文正常;双击用 Excel 打开,一片乱码。 原因在文件最前面三个字节。 演示第四段打出来:utf-8-sig 开头是 b'\xef\xbb\xbf',纯 utf-8 开头是 b'\xe6\x97\xa5'。 那三个字节叫 BOM(字节顺序标记)。 Excel 靠它判断「这是 UTF-8」,没有它就当成系统默认编码(简体中文 Windows 上是 GBK), 于是中文全成乱码。

折中办法很清楚:给 Excel 看的 CSV 用 utf-8-sig,给程序读的用纯 utf-8。 为什么给程序读的不用 BOM? 因为 BOM 会给解析程序添麻烦——有的库会把它当成表名的第一个字符,于是你的 row['日期'] 取不到, 因为实际的键是 '\ufeff日期'。 这个 bug 极其难查:打印出来看着一模一样,== 比较却不相等。 所以写 CSV 给程序读时,一定要用纯 utf-8;读别人给的 CSV 时,先 lstrip('\ufeff') 一下更保险。

五、CSV 和 JSON 各多大

格式 字节数 人能直接看懂吗 带类型吗
CSV 168 能,双击就开 不带
JSON 340 能,但要会看括号 带

同一份四行数据,CSV 168 字节,JSON 340 字节。差一倍。 原因跟上一篇那个 8500 对 4542 是同一件事:CSV 的表头只写一次,JSON 每行都自带键名。 那 JSON 是不是更差?不是。JSON 每行自带键名,所以少一行不会错位; 而且它能表达嵌套结构,CSV 表达不了(CSV 里塞一个列表进去,只能拼成字符串)。

选哪个看下游。交给 Excel、交给别人的老系统,用 CSV;自己程序之间传、要嵌套结构,用 JSON。 两者都不是「更好」,是各管一段。知道自己在哪一段,比记「哪个先进」有用。

六、Google 试算表那一步是什么

教材接着讲把数据存到 Google 云端试算表。用的是 gspread 这个库。 流程是三步:拿一个服务账号的密钥文件 → 用密钥登录 → 按名字打开某张表,然后 append_row 追加一行。 key.json 等于一张长期通行证。 拿到它的人能以你的身份读写这些表, 所以绝对不能提交到代码仓库(第 10 章讲 Git 时会说怎么把它排除掉)。

云试算表的好处很实在:别人能直接打开看、能几个人一起改、手机上也能看。 坏处也很实在:要联网、要账号、要管密钥、还依赖那家公司的服务一直在。 本地 CSV 反过来:啥都不用配,但只能一个人看,传给别人得发文件。 这一段在演示里标了「平台原样 · 本沙箱不执行」——它需要 Google 账号和密钥,沙箱里没有。 代码是从教材里原样抄的,照贴到本机配上密钥就能跑。

七、动手 Demo:CSV:最土的表格格式,最耐用

先写一个 CSV,再用三种方式读回来:按行、按字典、看编码。 然后看同一份数据 CSV 和 JSON 各占多少字节。 最后看云试算表那段代码长什么样(平台原样,不执行)。

import csv, json, os, tempfile

d = tempfile.mkdtemp()
P = os.path.join(d, 'stock.csv')
rows = [['日期', '品名', '数量', '单价'],
        ['2026-11-01', '六角螺栓 M8', 120, 1.5],
        ['2026-11-01', '膨胀螺栓 10mm', 80, 2.3],
        ['2026-11-02', '不锈钢垫片', 500, 0.4],
        ['2026-11-02', '六角螺栓 M8', 60, 1.5]]

print('① 写一个 CSV')
with open(P, 'w', encoding='utf-8', newline='') as f:
    csv.writer(f).writerows(rows)
print('  文件 %d 字节' % os.path.getsize(P))
print(open(P, encoding='utf-8').read().strip())

print('② 读回来')
with open(P, encoding='utf-8', newline='') as f:
    data = list(csv.reader(f))
print('  行数 %d(含表头)' % len(data))
print('  第一行', data[0])
print('  第二行', data[1])
print('  注意 120 和 1.5 都是字符串:', [type(x).__name__ for x in data[1]])

print('③ 用表头当键,取起来清楚')
with open(P, encoding='utf-8', newline='') as f:
    for i, r in enumerate(csv.DictReader(f)):
        if i < 2:
            print('  ', r)
print('  DictReader 把每行变成字典,键就是表头。比数下标好——'
      '以后表里插一列,按名字取的代码不用改。')

print('④ 编码:Excel 打开是乱码的原因')
sig = os.path.join(d, 'a_sig.csv')
raw = os.path.join(d, 'a_raw.csv')
for path, enc in ((sig, 'utf-8-sig'), (raw, 'utf-8')):
    with open(path, 'w', encoding=enc, newline='') as f:
        csv.writer(f).writerow(['日期', '品名'])
print('  utf-8-sig 开头三个字节', open(sig, 'rb').read(3))
print('  utf-8     开头三个字节', open(raw, 'rb').read(3))
print('  就是这三字节 BOM,什么都没多')
with open(sig, encoding='utf-8', newline='') as f:
    first_key = next(csv.DictReader(f)).popitem()[0]
print('  但 BOM 会污染第一个键:%r' % first_key)

print('⑤ 同一份数据,CSV 和 JSON 各多大')
js = [dict(zip(rows[0], r)) for r in rows[1:]]
b1 = os.path.getsize(P)
b2 = len(json.dumps(js, ensure_ascii=False).encode('utf-8'))
print('| 格式 | 字节数 | 人能直接看懂吗 | 带类型吗 |')
print('|---|---|---|---|')
print('| CSV | %d | 能,双击就开 | 不带 |' % b1)
print('| JSON | %d | 能,但要会看括号 | 带 |' % b2)

print('⑥ Google 试算表那一步是什么')
print('  [平台原样 · 本沙箱不执行]')
print('  | # 本书第 7-5 节(平台原样,本沙箱不执行:需要 Google 账号与密钥)')
print('  | import gspread')
print("  | gc = gspread.service_account(filename='key.json')")
print("  | sh = gc.open('材料台账').sheet1")
print("  | sh.append_row(['2026-11-02', '不锈钢垫片', 500, 0.4])")
print('  | rows = sh.get_all_records()')
print('  key.json 是服务账号的凭证,等于一张长期通行证,别提交到代码仓库')

实跑输出:

--------------------------------------------------------------------
CSV:最土的表格格式,最耐用
--------------------------------------------------------------------

  ① 写一个 CSV
  --------------------------------------------------------------------
    文件 176 字节
    日期,品名,数量,单价
    2026-11-01,六角螺栓 M8,120,1.5
    2026-11-01,膨胀螺栓 10mm,80,2.3
    2026-11-02,不锈钢垫片,500,0.4
    2026-11-02,六角螺栓 M8,60,1.5


  ② 读回来
  --------------------------------------------------------------------
    行数 5(含表头)
    第一行 ['日期', '品名', '数量', '单价']
    第二行 ['2026-11-01', '六角螺栓 M8', '120', '1.5']
  读回来的每个格子都是字符串,包括数量和单价。
  要算钱得自己 int() 或 float()——CSV 里没有类型这回事。

  ③ 用表头当键,取起来清楚
  --------------------------------------------------------------------
    {'日期': '2026-11-01', '品名': '六角螺栓 M8', '数量': '120', '单价': '1.5'}
    {'日期': '2026-11-01', '品名': '膨胀螺栓 10mm', '数量': '80', '单价': '2.3'}
  DictReader 把每行变成字典,键就是表头。比数下标好——以后表里插一列,按名字取的代码不用改。

  ④ 编码:Excel 打开是乱码的原因
  --------------------------------------------------------------------
    utf-8-sig 开头三个字节 b'\xef\xbb\xbf'
    utf-8     开头三个字节 b'\xe6\x97\xa5'
    就是这三字节 BOM,什么都没多
  utf-8-sig 会在文件最前面写三个看不见的字节(BOM)。
  Excel 靠这三个字节判断「这是 UTF-8」,没有它就当成系统默认编码,中文就成了乱码。
  折中办法:给 Excel 看的 CSV 用 utf-8-sig,给程序读的用纯 utf-8。
  BOM 会给解析程序添麻烦——有的库会把它当成表名的第一个字符。

  ⑤ 同一份数据,CSV 和 JSON 各多大
  --------------------------------------------------------------------

| 格式 | 字节数 | 人能直接看懂吗 | 带类型吗 |
|---|---|---|---|
| CSV | 168 | 能,双击就开 | 不带 |
| JSON | 340 | 能,但要会看括号 | 带 |

  CSV 更小,因为表头只写一次;JSON 更大,但每行自带键名,少一行也不会错位,嵌套结构也表达得了。
  选哪个看下游:交给 Excel、交给别人的老系统,用 CSV;自己程序之间传,用 JSON。
  两者都不是「更好」,是各管一段。

  ⑥ Google 试算表那一步是什么
  --------------------------------------------------------------------

  [平台原样 · 本沙箱不执行]
  | # 本书第 7-5 节(平台原样,本沙箱不执行:需要 Google 账号与密钥)
  | import gspread
  | gc = gspread.service_account(filename='key.json')
  | sh = gc.open('材料台账').sheet1
  | sh.append_row(['2026-11-02', '不锈钢垫片', 500, 0.4])
  | rows = sh.get_all_records()
  key.json 是服务账号的凭证,等于一张长期通行证,别提交到代码仓库

  云试算表的好处是别人能直接打开看、能一起改;坏处是要联网、要账号、要管密钥。
  本地 CSV 反过来:啥都不用配,但只能一个人看。
  教材把它放在这一章,是想说「存储介质可以换,数据的形状不变」——不管存 CSV 还是存云端表格,
  你脑子里那张表还是四列。

第一段那个 newline='' 不是可选的。 不加它,Windows 上每行会多一个空行——这是 csv 模块文档里明确要求的。 第二段最后那行 type 打出来全是 str,说明 CSV 里没有类型。 要算钱必须自己转,而且转之前最好想一下「这格会不会是空的」。 第三段那个 DictReader 是本篇最实用的工具。按名字取,以后表里插一列不用改代码。 第四段是本篇的重点:BOM 那三个字节。不加它 Excel 认不出 UTF-8,中文乱码; 加了它,有的解析库会把 BOM 当成表名的第一个字符。所以给 Excel 用 sig,给程序用纯 utf-8。 第五段那个 168 对 340 的差距,原因是表头只写一次。跟上一篇那个 8500 对 4542 是同一件事。

30 秒实验

  1. 把演示第一段的 newline='' 删掉,重跑,用记事本打开那个 CSV,看多了什么。
  2. 把演示第二段的 type(x).__name__ 换成直接 sum(row[2] for row in data[1:]),看它报什么错, 再想想该怎么改。
  3. 把演示第五段的 ensure_ascii=False 去掉,看 JSON 的字节数变多少。

常见误区

  1. 「CSV 里的数字读出来就是数字。」全是字符串。 '120' + 1 会直接报错,'120' * 2 会变成 '120120'。
  2. 「CSV 用 utf-8 就行,Excel 能打开。 」简体中文 Windows 上的 Excel 会把纯 utf-8 当 GBK,中文乱码。 给 Excel 看的要用 utf-8-sig。
  3. 「JSON 比 CSV 大,所以 CSV 更好。 」JSON 大是因为每行自带键名,换来的是「少一行不错位」和「能表达嵌套」。 两者各管一段,没有谁更好。
自测三问(点开对答案) 1. 用 Python 写的 CSV,Excel 打开中文乱码,是哪里出了问题?怎么改? —— 文件最前面少了三个字节的 BOM(`\xef\xbb\xbf`),Excel 认不出这是 UTF-8, 就按系统默认编码(GBK)解读,中文全乱。写的时候用 `encoding='utf-8-sig'` 就有 BOM 了。 2. 为什么给程序读的 CSV 反而不能用 `utf-8-sig`? —— 因为 BOM 会被某些解析库当成第一个字段名的第一个字符。 于是 `row['日期']` 取不到,实际的键是 `'\ufeff日期'`。 打印出来看着一模一样,比较却不相等,这种 bug 极难查。 3. 同一份四行数据,CSV 168 字节、JSON 340 字节。那是不是应该一律用 CSV? —— 不是。 JSON 大是因为每行都自带键名,换来两件事:少一行不会整列错位, 而且能表达嵌套结构(字典套列表),CSV 表达不了。 交给 Excel 和老系统用 CSV,自己程序之间传用 JSON。

第 7 章结束。下一篇进第 8 章:当函数越写越多,怎么用「类」把它们和数据捆在一起。

相关笔记