IO 进阶技巧
本教程共 54 篇 · 第 16 篇 · 更新于 2026-08-11 · 约 9 分钟阅读
本节目标:学会处理乱码、按块读取大文件、提前指定列类型和压缩存储,让数据读写又快又稳。
编码问题:乱码的根源
CSV 是文本文件,文本就有编码。同一个文件用不同编码解读,结果完全不同。pandas 默认按 utf-8 读,碰到 GBK 等编码的中文文件,轻则出现乱码,重则直接报错:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd5 in position 0
解决办法是显式指定 encoding 参数:
import pandas as pd
import numpy as np
# 老中文系统导出的文件常见 GBK 编码
df = pd.read_csv("data.csv", encoding="gbk")
# 指定 utf-8
df2 = pd.read_csv("data.csv", encoding="utf-8")
常用编码速查:
- utf-8:现代默认,跨平台
- gbk / gb2312:老中文系统导出文件常见
- utf-16:部分 Excel 导出文件
- latin-1:西欧字符编码,容错性极强,什么字节都能读,可用来”抢救”文件
写出时同样可以指定编码。中文文件想用 Excel 打开不乱码,用 utf-8-sig(带 BOM 标记):
import pandas as pd
import numpy as np
df = pd.DataFrame({"姓名": ["张三", "李四"]})
df.to_csv("out.csv", encoding="utf-8-sig", index=False)
Tip不确定文件编码时,用记事本打开文件”另存为”,看它默认选中的编码;或者用 Python 的 chardet 库自动检测。
dtype:提前告诉 pandas 列的类型
read_csv 默认自己推断每列类型,多数时候没问题,但两个场景建议手动指定:
- 身份证号、手机号这类”长得像数字但不是数字”的列,会被读成数值,精度直接丢失
- 大文件里字符串列被误判成数值,内存暴涨
import pandas as pd
import numpy as np
df = pd.read_csv("data.csv", dtype={
"id": "string", # 强制按文本读,防止精度丢失
"age": "int64",
"score": "float64",
})
举个例子:文件里存的是 “001”、“002” 这种编号,自动推断会读成整数 1、2,前面的零全没了。用 dtype 声明成 “string”,原样保留。
在 pandas 3.0 里,字符串列默认就是 str 类型(StringDtype),比旧的 object 类型更规范(如 §04 所述)。已经读进来发现类型不对,也可以事后用 astype 补救(如 §07 所述)。
想一步到位用 Arrow 后端读数据,可以传 dtype_backend="pyarrow",pandas 3.0 的新特性(如 §04 所述),后面讲性能优化时再细说。
read_csv 常用参数速查
前面几节零散讲过一些参数,这里汇总一张表,方便查阅:
| 参数 | 作用 |
|---|---|
| sep | 分隔符,默认逗号,制表符用 \t |
| header | 第几行当表头,默认 0 |
| encoding | 文件编码,乱码时先查它 |
| dtype | 指定每列类型 |
| usecols | 只读部分列 |
| nrows | 只读前 N 行 |
| skiprows | 跳过前 N 行或指定行 |
| na_values | 把指定值当成缺失值 |
| chunksize | 分块读取,返回迭代器 |
| compression | 压缩格式,默认 infer |
两个容易忽略但很实用的:
import pandas as pd
import numpy as np
# skiprows:跳过文件开头两行说明文字
# na_values:把 "未知" 和 "N/A" 统一识别为缺失值
pd.read_csv("data.csv", skiprows=2, na_values=["未知", "N/A"])
chunksize:大文件分块读
文件太大、内存装不下时,可以分块读取。read_csv 加上 chunksize 后,返回的不是 DataFrame,而是一个可迭代对象(TextFileReader),每次循环拿到一块:
import pandas as pd
import numpy as np
total = 0
count = 0
# 每 10000 行一块,逐块处理
for chunk in pd.read_csv("big.csv", chunksize=10000):
total += chunk["value"].sum()
count += len(chunk)
print("总和:", total)
print("平均:", total / count)
每块处理完就释放内存,峰值内存只跟块大小有关。块越大越快但越吃内存,一般从 10000 到 100000 之间试,目标是每块控制在 50-200MB 左右。
逐块统计的思路都一样:先算每一块的局部结果,最后汇总。比如求最大值:
import pandas as pd
import numpy as np
max_value = None
for chunk in pd.read_csv("big.csv", chunksize=10000):
chunk_max = chunk["value"].max()
if max_value is None or chunk_max > max_value:
max_value = chunk_max
print("最大值:", max_value)
也可以配合 iterator=True 手动取块:
import pandas as pd
import numpy as np
reader = pd.read_csv("big.csv", iterator=True)
chunk1 = reader.get_chunk(5000) # 先拿 5000 行
chunk2 = reader.get_chunk(5000) # 再拿 5000 行
Note分块只适合”块与块互不影响”的操作,比如求和、计数、筛选。要做全表排序、去重这类跨块操作,分块就不好使了,需要换其他方案。
少读一点:usecols 和 nrows
文件列很多时,可以只读需要的列,大幅减少内存:
import pandas as pd
import numpy as np
df = pd.read_csv("wide.csv", usecols=["id", "value", "date"])
只想先看看数据长什么样,用 nrows 读前几行,不伤内存:
import pandas as pd
import numpy as np
preview = pd.read_csv("big.csv", nrows=1000)
这两个参数在调试阶段特别好用:先小规模读进来确认列名和格式,再放开全量读取。
compression:压缩文件直接读写
pandas 的读写函数大多支持 compression 参数,能直接读 .gz、.bz2、.zip、.xz、.zst 文件,也能压缩着写出去:
import pandas as pd
import numpy as np
# 读压缩文件,infer 表示按后缀自动判断
df = pd.read_csv("data.csv.gz", compression="infer")
# 压缩写入
df.to_csv("data.csv.gz", compression="gzip", index=False)
df.to_csv("data.csv.zst", compression="zstd", index=False)
默认就是 "infer",会按文件后缀猜压缩方式,所以平时不用特意写这个参数。想精细控制压缩级别,可以传字典:
import pandas as pd
import numpy as np
df = pd.DataFrame({"id": [1, 2, 3]})
# compresslevel 越小越快、文件越大;1 表示最快
df.to_csv("data.csv.gz", compression={"method": "gzip", "compresslevel": 1}, index=False)
性能对比:别让 IO 拖后腿
社区里有一组流传很广的基准测试,数据相同、只换存储格式。结果很有参考价值:
| 格式 | 写入速度 | 读取速度 | 磁盘占用 |
|---|---|---|---|
| Feather | 最快 | 最快 | 小 |
| HDF5 fixed | 快 | 快 | 中 |
| Parquet | 快 | 快 | 最小之一 |
| Pickle | 快 | 快 | 中(压缩后小) |
| CSV | 慢 | 慢 | 最大 |
| SQL | 很慢 | 慢 | 中 |
结论很简单:追求速度用 Feather,追求体积和通用性用 Parquet,能不用 CSV 就不用 CSV。小文件无所谓,一旦上了百万行,格式的选择就是几分钟和几秒钟的差别。
补充一个 IO 之外的小技巧:读进来之后如果发现某些列类型不合适(比如 int 列其实是小数值),先优化 dtype 再分析,内存能省一大截(如 §53 所述)。
Warning压缩率不是越高越好。gzip、xz 压缩率高但读写慢;snappy、lz4 压缩率低但速度快。追求性能选 snappy/lz4,追求空间选 gzip/zstd。
常见 IO 报错速查
- FileNotFoundError:路径写错了,检查文件名和目录
- UnicodeDecodeError:编码不对,按上面说的方法换 encoding
- ParserError:文件内容跟参数不符,比如分隔符猜错了
- ImportError:缺可选依赖,Parquet 要 pyarrow、HDF5 要 tables
报错不可怕,先看异常类型,再对照这个清单,多数问题几分钟能解决。IO 环节的技巧说到底是三件事:把编码搞对、把数据读全、把内存控住。
实在读不出来的文件,可以先用 errors="replace" 抢救一遍:遇到无法解码的字符就用占位符替换,先把数据弄进来,再决定怎么处理脏字符:
import pandas as pd
import numpy as np
df = pd.read_csv("data.csv", encoding="gbk", errors="replace") # 乱码字符替换为 �
这只是应急手段,正经用法还是先把编码搞清楚。
调试小技巧:从字符串读数据
还没生成文件就想试 read_csv 的各种参数?可以用 io.StringIO 把字符串伪装成文件,不用建临时文件:
import pandas as pd
import numpy as np
import io
data = """id,value
1,100
2,200
3,300"""
df = pd.read_csv(io.StringIO(data))
print(df)
调试 dtype、usecols、na_values 这类参数时,这个技巧能省不少事。