csv模块 — CSV文件读写

Python标准库精讲专题 · 数据持久化篇 · 掌握CSV文件处理

专题:Python标准库精讲系统学习

关键词:Python, 标准库, csv, CSV文件, reader, writer, DictReader, DictWriter, 方言, dialect, 数据导入导出

一、CSV格式概述

CSV(Comma-Separated Values,逗号分隔值)是一种极其常见且轻量级的表格数据交换格式。它以纯文本形式存储表格数据(数字和文本),每条记录占一行,字段之间用逗号分隔。尽管格式看似简单,但在实际数据交换中扮演着桥梁角色,几乎所有的数据库系统、电子表格软件(如Excel、Google Sheets)、数据分析工具(如Pandas、R)以及各类编程语言都提供了对CSV的原生支持。

CSV格式并没有一个统一的全球标准,但业界广泛遵循 RFC 4180 规范(于2005年正式发布),该规范定义了CSV格式的标准处理方式。RFC 4180 的核心规定包括:每条记录位于单独一行,由换行符(CRLF)分隔;记录中的每个字段由逗号分隔;如果字段内容包含逗号、换行符或双引号,则该字段必须用双引号包裹;如果字段内容中包含双引号,则需使用两个连续的双引号("")进行转义;文件中的每一行应包含相同数量的字段。

尽管比JSON和XML更早出现,CSV凭借其极高的可读性和极低的解析成本,在数据科学、ETL管道、系统间数据迁移、日志导出等场景中始终活跃。Python标准库中的 csv 模块封装了RFC 4180的解析逻辑,提供了高层API以降低开发者的心智负担,是数据持久化编程的必备工具。

Python的csv模块设计思想非常清晰:将视为数据处理的基本单位,通过reader/writerDictReader/DictWriter两套API,分别面向序列(列表/元组)和映射(字典)两种编程范式。同时引入方言(Dialect)机制来适配各种CSV变体,使得模块具有高度灵活性。

# CSV文件的基本结构示例(data.csv) # 每行一条记录,字段用逗号分隔 name,age,city Alice,30,New York Bob,25,Los Angeles Charlie,35,"San Francisco, CA"

上述示例中,第一行为表头(header),定义了字段名称;后续行为数据行。第三行中"San Francisco, CA"包含逗号,因此需要用双引号包裹。以下是一个包含特殊字符(双引号)的CSV行示例:

# 字段内容包含双引号时的转义 id,note 1,He said "Hello"

经过规范编码后,该行变为:

1,"He said ""Hello"""

在Python中,使用csv模块读取该数据会自动完成转义还原,开发者无需手动处理这些底层细节。

二、reader与writer

csv模块提供了最基本的 csv.readercsv.writer 两个函数/类,用于从CSV文件中读取行数据写入行数据。它们的共同特点是操作对象为序列(list或tuple),每一行被解析为一个列表,字段按顺序对应到列表元素。

2.1 csv.reader — 逐行读取CSV文件

csv.reader 接受一个可迭代对象(通常是打开的文件对象)作为参数,返回一个 reader 对象。这个reader对象是一个迭代器,对其执行for循环或调用 next() 将逐行返回解析后的字段列表。Python的csv.reader内部自动处理了以下复杂情况:引号内逗号的识别、引号内换行符的识别、双引号转义的还原以及行尾逗号的处理。

import csv # 方式一:使用for循环遍历 with open('data.csv', 'r', encoding='utf-8') as f: reader = csv.reader(f) for row in reader: print(row) # 每行是一个列表:['Alice', '30', 'New York'] # 方式二:转换为列表(小文件场景) with open('data.csv', 'r', encoding='utf-8') as f: rows = list(csv.reader(f)) print(rows[0]) # 表头:['name', 'age', 'city'] # 方式三:使用next跳过表头 with open('data.csv', 'r', encoding='utf-8') as f: reader = csv.reader(f) header = next(reader) # 获取并跳过表头行 for row in reader: print(f"Name: {row[0]}, Age: {row[1]}")

reader的行为特点:返回的row始终是一个list,字段按列顺序排列。如果文件末尾包含空行,reader会自动跳过。reader不会在内存中缓存全部数据,适合处理大文件。reader支持多种参数来控制解析行为,其中最常用的是 delimiterquotechar。默认的delimiter是逗号(,),默认的quotechar是双引号(")。

# 读取制表符分隔的TSV文件 with open('data.tsv', 'r', encoding='utf-8') as f: reader = csv.reader(f, delimiter='\t', quotechar="'") for row in reader: print(row)

2.2 csv.writer — 写入CSV文件

csv.writer 将一个可写文件对象包装为writer对象,提供了 writerow()writerows() 两个核心方法。writerow() 一次写入一行(传入一个可迭代对象如列表或元组),writerows() 一次写入多行(传入一个可迭代对象的可迭代对象,如列表的列表)。writer会自动处理需要转义的情形:如果字段内容包含逗号、换行符或引号字符,writer会自动用双引号包裹字段,并将字段内部的引号双写转义。

import csv headers = ['name', 'age', 'city'] rows = [ ['Alice', 30, 'New York'], ['Bob', 25, 'Los Angeles'], ['Charlie', 35, 'San Francisco, CA'], ] # 写入CSV文件 with open('output.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(headers) # 写入表头 writer.writerows(rows) # 写入多行数据 # 写入Windows平台文件时注意 newline='' 参数 # 这是官方文档明确推荐的做法,可以避免多余的空行

写入CSV时一个常见的陷阱是 换行符处理。Python官方文档明确建议:在打开文件用于写入CSV时,始终指定 newline='' 参数。这是因为csv模块自己控制行结束符,如果让Python的文件对象也参与换行符转换,会导致写入的CSV文件中出现多余的空行(尤其是在Windows平台上)。

# 错误示范——缺少 newline='' 会导致多余空行 # with open('bad.csv', 'w') as f: ← 不要这样做! # writer = csv.writer(f) # 正确写法 with open('good.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(['a', 'b'])

writerow与writerows的对比:

方法参数说明示例
writerow()单行可迭代对象一次写入一行writer.writerow(['a','b'])
writerows()可迭代对象的可迭代对象一次写入多行,内部循环调用writerowwriter.writerows([['a','b'],['c','d']])

writerows() 在底层仍然是对每一行分别调用 writerow(),因此两者的行末处理、转义逻辑完全一致。使用 writerows() 可以减少Python级别的循环代码,使代码更加简洁。

三、DictReader与DictWriter

与基于序列的reader/writer不同,DictReaderDictWriter 提供了 基于字典(映射)的读写接口。在这种模式下,每一行被表示为一个字典:键为列名(默认取自第一行表头),值为对应字段的内容。这种方式使得代码的可读性极大提升,尤其是在列数较多或列顺序可能发生变化的情况下。

3.1 DictReader — 字典形式读取

DictReader 将CSV文件的第一行自动解析为字段名列表(fieldnames),后续每一行转换为 OrderedDict(Python 3.6+中为普通dict),键为fieldnames中的列名,值为对应列的内容。如果CSV文件没有表头行,可以手动传入 fieldnames 参数指定列名。

import csv # CSV文件内容: # name,age,city # Alice,30,New York # Bob,25,Los Angeles with open('data.csv', 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: print(row['name'], row['age'], row['city']) # 输出:Alice 30 New York # 输出:Bob 25 Los Angeles # 手动指定 fieldnames(文件没有表头时) with open('data_no_header.csv', 'r', encoding='utf-8') as f: reader = csv.DictReader(f, fieldnames=['name', 'age', 'city']) for row in reader: print(row['name'])

重要提示:DictReader的行类型在Python 3.6+中为普通的 dict(保持了插入顺序),在更早版本中为 OrderedDict。如果使用手动指定fieldnames,第一行数据不会被当作表头跳过,而是作为普通数据行按fieldnames映射。

3.2 DictWriter — 字典形式写入

DictWriter 接受一个 fieldnames 参数(必填),定义了字典的键与CSV列之间的映射关系。写入时调用 writerow() 传入字典,DictWriter会根据fieldnames提取对应值并按顺序写出。如果字典中缺少某个fieldnames中定义的键,则该字段写出空字符串;如果字典中包含fieldnames之外的键,这些键会被忽略。writeheader() 方法可以将fieldnames作为表头行写入文件。

import csv fieldnames = ['name', 'age', 'city'] rows = [ {'name': 'Alice', 'age': '30', 'city': 'New York'}, {'name': 'Bob', 'age': '25', 'city': 'Los Angeles'}, {'name': 'Charlie', 'age': '35', 'city': 'San Francisco, CA'}, ] with open('output.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() # 写入表头:name,age,city writer.writerows(rows) # 写入多行数据 # 使用extrasaction参数控制多余列的处理 # 默认 extrasaction='raise',遇到不认识的键会抛 ValueError # 设置为 'ignore' 则静默忽略 with open('strict.csv', 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=['name', 'age'], extrasaction='ignore') writer.writeheader() writer.writerow({'name': 'Alice', 'age': '30', 'extra': 'ignored'})

DictWriter的extrasaction参数:当传入的字典包含fieldnames中没有的键时,默认行为是抛出 ValueError 异常。这是一个保护机制,可以防止因列名拼写错误导致数据静默丢失。如果确实需要忽略多余的键,设置 extrasaction='ignore'

总结:DictReader/DictWriter将CSV的列与字典的键建立了显式映射,虽然带来轻微性能开销(字典查找),但极大地提高了代码的可维护性和可读性。在列数较多、列顺序可能变化或需要选择性读写的场景下,强烈推荐使用字典接口。

四、方言Dialect

CSV格式虽然在概念上统一为"逗号分隔值",但在实际应用中存在大量变体:不同的操作系统使用不同的行结束符、不同的区域设置使用不同的分隔符(欧洲国家常用分号代替逗号)、不同的应用程序使用不同的引号规则……为了应对这种多样性,csv模块引入了 方言(Dialect) 的概念。方言是一组命名参数的集合,封装了CSV的格式设置,使得参数的复用变得简单。

4.1 标准方言

csv模块内置了三个标准方言:

方言delimiterquotecharquotinglineterminator说明
excel,"QUOTE_MINIMAL\r\n默认方言,兼容Excel导出的CSV
excel-tab\t"QUOTE_MINIMAL\r\n制表符分隔的Excel兼容格式
unix,"QUOTE_ALL\n类Unix系统风格,所有字段都加引号
# 使用内置方言 import csv # 使用 excel 方言读取 with open('data.csv', 'r', encoding='utf-8') as f: reader = csv.reader(f, dialect='excel') for row in reader: print(row) # 使用 excel-tab 方言读取制表符分隔文件 with open('data.tsv', 'r', encoding='utf-8') as f: reader = csv.reader(f, dialect='excel-tab') # 使用 unix 方言写入 with open('data_unix.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f, dialect='unix') writer.writerow(['a', 'b'])

4.2 自定义方言

通过继承 csv.Dialect 类可以定义自己的方言,或使用 csv.register_dialect() 函数注册一个命名方言。自定义方言的核心属性包括:

属性默认值说明
delimiter,字段分隔符,单字符
quotechar"引号字符,单字符
quotingQUOTE_MINIMAL引号使用策略
escapecharNone转义字符,设为\\可用反斜杠转义
lineterminator\r\n行结束符(writer使用)
skipinitialspaceFalse是否跳过分隔符后的空白
doublequoteTrue引号内双引号是否双写转义
strictFalse是否严格解析,设为True时遇到错误抛出异常
# 方式一:继承Dialect类 class MyDialect(csv.Dialect): delimiter = '|' quotechar = "'" quoting = csv.QUOTE_MINIMAL escapechar = None lineterminator = '\n' skipinitialspace = True doublequote = True strict = False csv.register_dialect('myformat', MyDialect) with open('data.psv', 'r', encoding='utf-8') as f: reader = csv.reader(f, dialect='myformat') for row in reader: print(row) # 方式二:直接使用参数,不显式注册方言 # 效果等价于一次性使用的匿名方言 with open('data.psv', 'r', encoding='utf-8') as f: reader = csv.reader(f, delimiter='|', quotechar="'", skipinitialspace=True)

4.3 Sniffer嗅探器

在实际项目中经常遇到这种情况:拿到一个CSV文件但不知道它的方言配置(分隔符是什么、是否有表头、引用字符是什么)。csv模块提供了 csv.Sniffer 类来解决这个问题,它可以自动嗅探(分析)CSV文件的方言。

import csv with open('unknown_format.csv', 'r', encoding='utf-8') as f: sample = f.read(1024) # 读取前1024字节作为样本 dialect = csv.Sniffer().sniff(sample) print(f"分隔符: {dialect.delimiter}") print(f"引号符: {dialect.quotechar}") # Sniffer还可以判断是否有表头 has_header = csv.Sniffer().has_header(sample) print(f"是否有表头: {has_header}") # 嗅探后直接使用自动识别的方言读取 f.seek(0) # 回到文件开头 reader = csv.reader(f, dialect=dialect) for row in reader: print(row)

Sniffer使用提示:Sniffer基于统计分析来推断方言,样本数据量越大、越有代表性,嗅探结果越准确。建议至少提供几百字节的样本。Sniffer在处理非常规分隔符(如有多个候选分隔符且它们在数据中出现频率相近)时可能误判,此时需要人工介入确认。

五、引号控制

csv模块通过 quoting 参数提供了四种引号控制策略,决定了writer在何时对字段值加引号,以及reader如何解释引号。这些策略定义在csv模块的四个常量中:QUOTE_ALLQUOTE_MINIMALQUOTE_NONNUMERICQUOTE_NONE

5.1 QUOTE_MINIMAL(默认)

只在必要时才加引号。具体来说,仅当字段包含以下字符之一时进行包裹:分隔符(默认逗号)、quotechar、换行符(\n或\r\n)或行结束符。这是最节省空间的策略,也是Excel的默认行为。适用于绝大多数通用场景。

# QUOTE_MINIMAL — 默认行为,仅在必要时加引号 import csv rows = [['normal', 'has, comma', 'has "quote"', 'has\nnewline']] with open('minimal.csv', 'w', newline='') as f: writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL) writer.writerows(rows) # 输出:normal,"has, comma","has ""quote""","has # newline"

5.2 QUOTE_ALL

对所有字段都加引号,无论字段内容是否包含特殊字符。这种方式生成的CSV文件最为统一和可预测,但文件体积会增大。在某些严格要求数据格式一致的系统中,QUOTE_ALL是推荐的策略。

# QUOTE_ALL — 所有字段都加引号 with open('all_quoted.csv', 'w', newline='') as f: writer = csv.writer(f, quoting=csv.QUOTE_ALL) writer.writerow(['Alice', '30', 'New York']) # 输出:"Alice","30","New York"

5.3 QUOTE_NONNUMERIC

对reader和writer的行为不同:

# QUOTE_NONNUMERIC — reader:未引号字段自动转float # CSV内容: # "Alice",30,"New York" # "Bob","25","Los Angeles" with open('mixed.csv', 'r') as f: reader = csv.reader(f, quoting=csv.QUOTE_NONNUMERIC) for row in reader: print(type(row[1])) # 未加引号的 30 会变成 float(30.0) # 加引号的字段 "25" 会作为字符串保留

5.4 QUOTE_NONE

从不对任何字段加引号。如果字段内容中包含分隔符、换行符或quotechar字符,writer会抛出异常(除非同时设置了 escapechar 用于转义)。reader场景中,字段内的引号被当作普通字符处理。这种模式适用于已经预处理好字段内容的场景,或者与escapechar配合使用。

# QUOTE_NONE + escapechar — 用反斜杠转义特殊字符 with open('escaped.csv', 'w', newline='') as f: writer = csv.writer(f, quoting=csv.QUOTE_NONE, escapechar='\\') writer.writerow(['hello', 'a,b']) # 输出:hello,a\,b

四种引号策略对比汇总:

策略writer行为reader行为
QUOTE_MINIMAL0仅特殊字符时加引号按引号原样解析
QUOTE_ALL1所有字段加引号去掉所有引号
QUOTE_NONNUMERIC2非数字字段加引号未加引号的转float
QUOTE_NONE3从不加引号(需escapechar)引号当作普通字符

六、错误处理

csv模块在解析过程中可能遇到各种格式问题,主要通过 csv.Error 异常类来汇报。csv.Error是 Exception 的直接子类,在解析不规范CSV文件时抛出。所有csv模块产生的异常都是 csv.Error 类型或它的子类。

6.1 常见异常场景

以下情形会触发 csv.Error:

import csv def safe_read_csv(filepath): try: with open(filepath, 'r', encoding='utf-8') as f: reader = csv.reader(f) for i, row in enumerate(reader, 1): print(f"第 {i} 行: {row}") except csv.Error as e: print(f"CSV解析错误(第 {i} 行附近): {e}") except UnicodeDecodeError as e: print(f"文件编码错误: {e}") except FileNotFoundError: print(f"文件不存在: {filepath}") # 使用strict模式检测格式异常 def strict_read_csv(filepath): try: with open(filepath, 'r', encoding='utf-8') as f: reader = csv.reader(f, strict=True) return list(reader) except csv.Error as e: print(f"CSV格式不严格符合规范: {e}") return None

6.2 编码问题处理

CSV文件最常见的问题之一是编码不一致。不同系统导出的CSV可能使用不同的编码(UTF-8、GBK/GB2312、Latin-1等)。Python的csv模块并不处理编码——编码解包由 open() 的文件对象负责。因此编码错误会在文件读取层抛出,而不是csv.Error。实际项目中推荐的做法是:先尝试UTF-8,失败后回退到其他编码。

import csv def open_csv_with_fallback(filepath): """尝试多种编码打开CSV文件""" encodings = ['utf-8', 'gbk', 'gb2312', 'latin-1'] for enc in encodings: try: return open(filepath, 'r', encoding=enc) except UnicodeDecodeError: continue raise ValueError(f"无法识别文件编码: {filepath}") # 使用回退编码打开CSV with open_csv_with_fallback('unknown_encoding.csv') as f: reader = csv.reader(f) for row in reader: print(row)

最佳实践:处理CSV文件时,始终将csv逻辑包裹在try/except块中,并区分csv.Error(格式问题)和IOError/FileNotFoundError(文件路径问题)以及UnicodeDecodeError(编码问题)。对于大规模数据处理(超过10万行),建议在遍历reader时添加行号计数和异常捕获,这样即使某行解析失败也能精准定位,而不至于整个任务崩溃。

七、实战案例与总结

本章通过几个贴近真实工作场景的案例,展示csv模块的综合应用。这些案例覆盖了从基础读写到进阶技巧的各个方面。

7.1 案例一:大文件分块处理

在处理超大CSV文件(如超过1GB的日志文件)时,不能一次性将整个文件读入内存。正确做法是使用reader迭代器逐行处理,并结合分块(chunk)思想进行批量写入——这本质上正是csv.reader的设计意图:作为迭代器,它始终只保留当前行在内存中。

import csv def process_large_csv(input_path, output_path, chunk_size=10000): """大文件CSV处理:读取、转换、写入""" with open(input_path, 'r', encoding='utf-8') as fin, \ open(output_path, 'w', newline='', encoding='utf-8') as fout: reader = csv.reader(fin) writer = csv.writer(fout) header = next(reader) writer.writerow(header) # 原样写入表头 processed = 0 chunk = [] for row in reader: # 对每行进行转换处理(示例:将第二列转为大写) row[1] = row[1].upper() chunk.append(row) processed += 1 if len(chunk) >= chunk_size: writer.writerows(chunk) chunk = [] print(f"已处理 {processed} 行...") # 处理剩余行 if chunk: writer.writerows(chunk) print(f"处理完成,共 {processed} 行")

7.2 案例二:CSV数据清洗

现实中的CSV数据往往不规整:包含空白字符、空值表示为不同形式(空字符串、"N/A"、"null")、数值含有千分位分隔符等。数据清洗是CSV处理中最常见的任务。

import csv def clean_csv(input_path, output_path): """清洗CSV数据:去除空白、统一空值、转换数值""" with open(input_path, 'r', encoding='utf-8') as fin, \ open(output_path, 'w', newline='', encoding='utf-8') as fout: reader = csv.DictReader(fin) fieldnames = reader.fieldnames writer = csv.DictWriter(fout, fieldnames=fieldnames) writer.writeheader() for row in reader: cleaned = {} for key, value in row.items(): if value is None: cleaned[key] = '' else: value = value.strip() if value.lower() in ('n/a', 'null', 'none', '-'): cleaned[key] = '' else: # 去除数值中的千分位逗号 if ',' in value and value.replace(',', '').isdigit(): value = value.replace(',', '') cleaned[key] = value writer.writerow(cleaned)

7.3 案例三:CSV编码转换

跨系统数据交换时常遇到编码问题:Windows中文系统默认使用GBK编码导出CSV,而现代系统更倾向于UTF-8。下面演示如何实现编码转换。

import csv def convert_csv_encoding(input_path, output_path, src_enc='gbk', dst_enc='utf-8'): """CSV文件编码转换""" with open(input_path, 'r', encoding=src_enc) as fin, \ open(output_path, 'w', newline='', encoding=dst_enc) as fout: reader = csv.reader(fin) writer = csv.writer(fout) for row in reader: writer.writerow(row) print(f"编码转换完成:{src_enc} → {dst_enc}") # 使用示例:将GBK编码的CSV转为UTF-8 # convert_csv_encoding('chinese_data.csv', 'chinese_data_utf8.csv')

7.4 总结:csv模块核心要点

Python的csv模块虽然小巧,但设计精良、功能完备。归纳其核心要点如下:

类别核心API适用场景
基础读写reader / writer简单场景、逐行处理大文件
字典读写DictReader / DictWriter列名可读性要求高、列数多、列序可变
方言控制Dialect / register_dialect非标准分隔符、跨系统兼容
自动检测Sniffer未知格式CSV文件
引号策略QUOTE_* 常量精确控制CSV输出格式
错误处理csv.Error稳定健壮的生产级代码

核心心得:处理CSV时始终坚持以下原则:(1) 始终使用 newline='' 打开写入文件;(2) 优先使用 with 语句确保文件关闭;(3) 明确定义编码,避免依赖系统默认编码;(4) 大文件使用迭代器而非一次性读取;(5) 用DictReader/DictWriter减少按索引取值的脆弱性。掌握这些要点后,Python的csv模块足以覆盖95%以上的CSV文件处理需求。