CSV 比看上去简单(也比你想的更棘手)
CSV 文件就是纯文本:行与行之间用换行符分隔,字段之间用逗号分隔。思路就这么简单。但在实际中,你会碰到包含逗号的带引号字符串、字段里嵌着换行符、不同地区的不同惯例(逗号还是分号)、从 Excel 保存的带 BOM 的文件,边界情况多到你自己写 line.split(",") 几乎总是个错误。
Python 内置的 csv 模块能处理所有这些情况。对中小型文件来说,你很少需要用别的东西。
用 csv.reader 读取 CSV
csv.reader 把每一行作为一个字符串列表逐个返回:
import csv
with open("people.csv", newline="") as f:
reader = csv.reader(f)
for row in reader:
print(row)
有几个不那么显而易见的细节:
- 一定要给
open传newline=""。 csv 模块会自己处理行结束符;不加这个参数,在 Windows 上会多出空行。 - 每个值都是字符串。
"42"在你调用int(...)之前一直是字符串。CSV 没有类型。 - 表头行只是普通的一行。 如果你的文件有表头,要么手动跳过第一行,要么改用
DictReader。
跳过表头行
import csv
with open("people.csv", newline="") as f:
reader = csv.reader(f)
headers = next(reader) # pulls the first row out
for row in reader:
print(row)
next(reader) 让迭代器前进一步,并返回那一行。
用 DictReader 读取为字典
csv.DictReader 把第一行当作表头,把之后的每一行作为一个字典给你:
import csv
with open("people.csv", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["name"], row["email"])
这几乎总是你想要的。列名本身就说明了含义,即使源文件中的列换了顺序,你的代码也不会出错。
如果文件没有表头,就用 fieldnames=["name", "email", ...] 显式传入。
用 csv.writer 写入 CSV
csv.writer 把行(列表)转换成 CSV 的行:
import csv
rows = [
["name", "age", "city"],
["Rosa", 30, "Lisbon"],
["Ada", 36, "London"],
]
with open("out.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerows(rows)
writerow(row) 写入一行;writerows(rows) 一次写入整个可迭代对象。当字段包含逗号、引号或换行符时,两者都会自动给字段加上引号,你完全不用操心。
用 DictWriter 写入字典
当你的数据本来就是字典形式时,DictWriter 可以省掉“转换成列表”这一步:
import csv
people = [
{"name": "Rosa", "age": 30, "city": "Lisbon"},
{"name": "Ada", "age": 36, "city": "London"},
]
with open("out.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["name", "age", "city"])
writer.writeheader()
writer.writerows(people)
fieldnames 参数同时决定表头和列的顺序。字典中不在 fieldnames 里的键会被悄悄丢掉(你也可以用 extrasaction="raise" 让它抛出异常)。
不同的分隔符和引号规则
并不是所有的“CSV”都用逗号。欧洲地区常用 ;,制表符分隔的文件用 \t,有些系统用 |。传入 delimiter= 参数即可:
import csv
with open("data.tsv", newline="") as f:
reader = csv.reader(f, delimiter="\t")
for row in reader:
print(row)
对于引号规则特殊的文件,csv.register_dialect(...) 让你配置一次就能反复使用。对大多数文件来说,默认设置加上 delimiter= 就够了。
编码
CSV 文件是文本,所以有编码。UTF-8 是现在的默认编码;在 Windows 上由 Excel 生成的文件有时会用 cp1252,或者带有 UTF-8 BOM。请显式指定编码:
with open("data.csv", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
...
如果你看到 UnicodeDecodeError,说明文件的编码不是你猜的那种。常见的嫌疑对象有 utf-8-sig(能处理 Excel 的 BOM)、cp1252 和 latin-1,逐个试试。
把 CSV 的行转换成有用的类型
既然每个值读进来都是字符串,解析就得靠你自己:
(StringIO 让我们不需要真实的文件就能运行这个例子,在实际代码中你会用 open(path)。)
对于类型比较棘手的 CSV(日期、可能为空的数字、有十种不同写法的 true/false),可以考虑 pandas,它为其中大部分情况内置了处理惯例。
什么时候改用 pandas
pandas.read_csv(path) 返回一个 DataFrame。一旦你想做下面这些事,它就是合适的数据结构:
- 筛选行:
df[df["active"] == True] - 聚合:
df.groupby("city")["age"].mean() - 与另一张表连接
- 用简单的格式写回文件
import pandas as pd
df = pd.read_csv("people.csv")
adults = df[df["age"] >= 18]
adults.to_csv("adults.csv", index=False)
对于小型的、按顺序读取的场景,pandas 有点大材小用,而且它是一个很重的依赖(要在虚拟环境中用 pip 安装)。但只要是和数据分析相关的工作,它就是大多数 Python 分析师首选的工具。
流式处理超大文件
csv.reader 本身就是惰性的:它一次只读一行。保持这种方式,也就是逐行迭代(而不是一开始就调用 list(reader)),无论文件多大,内存占用都保持平稳:
import csv
with open("huge.csv", newline="") as f:
reader = csv.DictReader(f)
error_count = 0
for row in reader:
if row["status"] == "error":
error_count += 1
print(f"Found {error_count} errors.")
只要你不把所有行都攒进一个列表,处理 10 GB 的文件和处理 10 KB 的文件一样轻松。
几个好习惯
- 读写 CSV 时,总是给
open传newline=""。 - 只要文件有表头,就用
DictReader/DictWriter,比整数索引更易读。 - 显式指定编码,尤其是来自 Excel 或非英文来源的文件。
- 在读取时就转换好类型,这样后面的代码就不用再处理。
- 当你想“分析”数据,而不只是搬运数据时,就改用 pandas。
接下来
现在你已经能读取 JSON 和 CSV 了。我们要讲的最后一项实用技能是通过网络获取数据,也就是下一篇文档:用 requests 库发送 HTTP 请求。
常见问题
在 Python 中怎么读取 CSV 文件?
使用内置的 csv 模块。csv.reader 把每一行作为一个字符串列表给你;csv.DictReader 把第一行当作表头,把每一行作为一个字典逐个返回。打开文件时加上 newline='',这样 Python 就不会弄乱换行符:with open('data.csv', newline='') as f:。
在 Python 中怎么写入 CSV 文件?
把 csv.writer 和一个以写入模式、带 newline='' 打开的文件配合使用。每一行调用一次 writer.writerow([...]),或者用 writer.writerows([[...], [...]]) 一次写入多行。如果数据是字典形式的,就用 csv.DictWriter,它会自动处理表头。
该用 csv 模块还是 pandas?
快速读写、逐行处理文件,以及不想再多加一个依赖时,用 csv。需要筛选、分组或连接数据时,或者文件大到向量化操作能带来明显差别时,用 pandas。它们能处理同样的文件;怎么选,取决于加载数据之后你要拿它做什么。
为什么在 Windows 上我的 CSV 每行之间都有空行?
你打开文件时没有加 newline=''。csv 模块会自己写入行结束符;不加这个参数,Python 在 Windows 上会再多加一些。无论读还是写,打开 CSV 文件时都要用 open(path, newline='')。