原文:https://pythoncaff.com/docs/pymotw/csv-comma-separated-value-files/125
csv
模块主要用于处理从电子数据表格或数据库中导入到文本文件的数据,通常简称为comma-separated value (CSV)格式因为逗号用于分离每条记录的各个字段。
读取##
从 CSV 文件中读取数据, 可以使用 reader()
函数来创建一个读取对象。 这个读取对象顺序处理文件的每一行,可以把它当成迭代器使用, 例如:
csv_reader.py
import csv
import sys
with open(sys.argv[1], 'rt') as f: reader = csv.reader(f) for row in reader: print(row)
reader()
的第一个参数指源文本,在这个例子中,是一个文件,但它可以是任何可迭代对象( StringIO
实例,list
等)。第二个参数是可选的,可用于控制输入的数据如何被解析。
"Title 1","Title 2","Title 3","Title 4" 1,"a",08/18/07,"å" 2,"b",08/19/07,"∫" 3,"c",08/20/07,"ç"
它被读取时,输入数据的每一行被转换为一个字符串列表。
$ python3 csv_reader.py testdata.csv
['Title 1', 'Title 2', 'Title 3', 'Title 4'] ['1', 'a', '08/18/07', 'å'] ['2', 'b', '08/19/07', '∫'] ['3', 'c', '08/20/07', 'ç']
解析器会自动处理嵌入在一行字符串中的换行符,这也是输出中的一行数据可能和输入的一行不同的原因。
"Title 1","Title 2","Title 3" 1,"first line second line",08/18/07
输入中带有换行符的字段在解析器返回时保留内部换行符。
$ python3 csv_reader.py testlinebreak.csv
['Title 1', 'Title 2', 'Title 3'] ['1', 'first line\nsecond line', '08/18/07']
写入##
写入 CSV 文件和读取它们一样简单。使用 writer()
方法创建一个写入对象,然后使用 writerow()
去输出每一行。
csv_writer.py
import csv
import sys
unicode_chars = 'å∫ç'
with open(sys.argv[1], 'wt') as f: writer = csv.writer(f) writer.writerow(('Title 1', 'Title 2', 'Title 3', 'Title 4')) for i in range(3): row = ( i + 1, chr(ord('a') + i), '08/{:02d}/07'.format(i + 1), unicode_chars[i], ) writer.writerow(row) print(open(sys.argv[1], 'rt').read())
这个例子的输出和上面读取的例子看起来有些不同,是因为这里有的值没有加引号。
$ python3 csv_writer.py testout.csv
Title 1,Title 2,Title 3,Title 4 1,a,08/01/07,å 2,b,08/02/07,∫ 3,c,08/03/07,ç
引用##
写入时,默认的引用行为不同,所以之前示例中的第二和第三个字段未被引用。 要添加引号,请将 quoting
参数设置为其他引用模式。
writer = csv.writer(f, quoting=csv.QUOTE_NONNUMERIC)
在这个例子中, QUOTE_NONNUMERIC
会给所有字段值不是数字的值添加引号。
$ python3 csv_writer_quoted.py testout_quoted.csv
"Title 1","Title 2","Title 3","Title 4" 1,"a","08/01/07","å" 2,"b","08/02/07","∫" 3,"c","08/03/07","ç"
有四种不同的引用选项,在 csv 模块中被定义为常量。
QUOTE_ALL
无论什么类型的字段都会被引用。
QUOTE_MINIMAL
这是默认的选项, 使用指定的字符引用各字段(如果解析器被配置为相同的dialect和选项时,可能会让解析器在解析时产生混淆)。
QUOTE_NONNUMERIC
引用那些不是整数或浮点数的字段。当使用读取对象时, 如果输入的字段是没有引号的, 那么它们会被转换成浮点数。
QUOTE_NONE
对所有的输出内容都不加引用,当使用读取对象时,引用字符看作是包含在每个字段的值里(但在正常情况下,它们被当成定界符而被去掉)。
编码风格##
其实没有一个标准定义这类逗号分隔值的文件,所以解析器需要很灵活,通过很多参数去控制如何解析 csv
或给其写入数据。但这并不是每个参数在写入或读取 csv 时分别传入,而是统一分组为一个 编码风格 对象。
Dialect 类可以通过名字注册,因此 csv
模块调用它时不必预先知道相关的参数设置。所有注册过的编码风格列表可以通过 list_dialects()
方法查看。
csv_list_dialects.py
import csv
print(csv.list_dialects())
标准库提供了三种编码风格,分别为: excel
, excel-tabs
和 unix
。 excel
编码风格用来处理默认来自 Microsoft Excel 格式的数据的,同样可用于处理来自 LibreOffice 格式的。 unix
编码风格将所有字段通过双引号引用,并用 \n
做为每条记录的分隔符。
$ python3 csv_list_dialects.py
['excel', 'excel-tab', 'unix']
创建一个编码风格##
如果不使用逗号分隔字段,输入文件使用竖杠( |
),就像这样
"Title 1"|"Title 2"|"Title 3" 1|"first line second line"|08/18/07
一个新的编码风格可以使用不同的分隔符进行注册。
csv_dialect.py
import csv
csv.register_dialect('pipes', delimiter='|') with open('testdata.pipes', 'r') as f: reader = csv.reader(f, dialect='pipes') for row in reader: print(row)
使用「竖杠」的编码风格,可以像使用逗号一样读取文件。
$ python3 csv_dialect.py
['Title 1', 'Title 2', 'Title 3'] ['1', 'first line\nsecond line', '08/18/07']
编码风格参数##
编码风格指定解析或写入数据文件时使用的所有标记。下表列出了可以设定的属性,从字段的分隔方式到用于转义标记的字符。
CSV 编码风格参数
属性 | 默认 | 含义 |
---|---|---|
delimiter | , | 字段分隔符(单字符) |
doublequote | True | 控制 quotechar 实例是否翻倍 |
escapechar | None | 用于表示转义序列的字符 |
lineterminator | \r\n | 写入时用来换行的字符 |
quotechar | " | 引用含特殊值字段的字符(一个字符) |
quoting | QUOTE_MINIMAL | 控制前面表述的引用行为 |
skipinitialspace | False | 是否在字段分隔符后忽略空格 |
csv_dialect_variations.py
import csv
import sys
csv.register_dialect('escaped', escapechar='\\', doublequote=False, quoting=csv.QUOTE_NONE, ) csv.register_dialect('singlequote', quotechar="'", quoting=csv.QUOTE_ALL, ) quoting_modes = { getattr(csv, n): n for n in dir(csv) if n.startswith('QUOTE_') } TEMPLATE = '''\ Dialect: "{name}" delimiter = {dl!r:<6} skipinitialspace = {si!r} doublequote = {dq!r:<6} quoting = {qu} quotechar = {qc!r:<6} lineterminator = {lt!r} escapechar = {ec!r:<6} ''' for name in sorted(csv.list_dialects()): dialect = csv.get_dialect(name) print(TEMPLATE.format( name=name, dl=dialect.delimiter, si=dialect.skipinitialspace, dq=dialect.doublequote, qu=quoting_modes[dialect.quoting], qc=dialect.quotechar, lt=dialect.lineterminator, ec=dialect.escapechar, )) writer = csv.writer(sys.stdout, dialect=dialect) writer.writerow( ('col1', 1, '10/01/2010', 'Special chars: " \' {} to parse'.format( dialect.delimiter)) ) print()
这段程序演示了当使用几种不同的编码风格格式化时,相同的数据如何展示。
$ python3 csv_dialect_variations.py
Dialect: "escaped"
delimiter = ',' skipinitialspace = 0 doublequote = 0 quoting = QUOTE_NONE quotechar = '"' lineterminator = '\r\n' escapechar = '\\' col1,1,10/01/2010,Special chars: \" ' \, to parse Dialect: "excel" delimiter = ',' skipinitialspace = 0 doublequote = 1 quoting = QUOTE_MINIMAL quotechar = '"' lineterminator = '\r\n' escapechar = None col1,1,10/01/2010,"Special chars: "" ' , to parse" Dialect: "excel-tab" delimiter = '\t' skipinitialspace = 0 doublequote = 1 quoting = QUOTE_MINIMAL quotechar = '"' lineterminator = '\r\n' escapechar = None col1 1 10/01/2010 "Special chars: "" ' to parse" Dialect: "singlequote" delimiter = ',' skipinitialspace = 0 doublequote = 1 quoting = QUOTE_ALL quotechar = "'" lineterminator = '\r\n' escapechar = None 'col1','1','10/01/2010','Special chars: " '' , to parse' Dialect: "unix" delimiter = ',' skipinitialspace = 0 doublequote = 1 quoting = QUOTE_ALL quotechar = '"' lineterminator = '\n' escapechar = None "col1","1","10/01/2010","Special chars: "" ' , to parse"
自动检测编码风格##
配置一个输入文件的编码风格的最好的办法是提前知道哪种编码风格是正确的。对于那些编码风格未知的参数, Sniffer
类可用于做有效的猜测。 sniff()
方法会获取输入数据的一个样本和一个可选参数,给出可能的分隔符。
csv_dialect_sniffer.py
import csv
from io import StringIO
import textwrap csv.register_dialect('escaped', escapechar='\\', doublequote=False, quoting=csv.QUOTE_NONE) csv.register_dialect('singlequote', quotechar="'", quoting=csv.QUOTE_ALL) # 为所有已知的编码风格生成样本数据 samples = [] for name in sorted(csv.list_dialects()): buffer = StringIO() dialect = csv.get_dialect(name) writer = csv.writer(buffer, dialect=dialect) writer.writerow( ('col1', 1, '10/01/2010', 'Special chars " \' {} to parse'.format( dialect.delimiter)) ) samples.append((name, dialect, buffer.getvalue())) # 猜测样本的编码风格,然后用猜测结果来解析数据。 sniffer = csv.Sniffer() for name, expected, sample in samples: print('Dialect: "{}"'.format(name)) print('In: {}'.format(sample.rstrip())) dialect = sniffer.sniff(sample, delimiters=',\t') reader = csv.reader(StringIO(sample), dialect=dialect) print('Parsed:\n {}\n'.format( '\n '.join(repr(r) for r in next(reader))))
sniff()
方法返回一个包含了解析数据的参数的 Dialect
实例。结果并不一定是正确的,例如这个例子中的「escaped」。
$ python3 csv_dialect_sniffer.py
Dialect: "escaped"
In: col1,1,10/01/2010,Special chars \" ' \, to parse Parsed: 'col1' '1' '10/01/2010' 'Special chars \\" \' \\' ' to parse' Dialect: "excel" In: col1,1,10/01/2010,"Special chars "" ' , to parse" Parsed: 'col1' '1' '10/01/2010' 'Special chars " \' , to parse' Dialect: "excel-tab" In: col1 1 10/01/2010 "Special chars "" ' to parse" Parsed: 'col1' '1' '10/01/2010' 'Special chars " \' \t to parse' Dialect: "singlequote" In: 'col1','1','10/01/2010','Special chars " '' , to parse' Parsed: 'col1' '1' '10/01/2010' 'Special chars " \' , to parse' Dialect: "unix" In: "col1","1","10/01/2010","Special chars "" ' , to parse" Parsed: 'col1' '1' '10/01/2010' 'Special chars " \' , to parse'
使用字段名称##
除了处理数据序列之外,csv
模块还提供了用于处理将字典作为行的类,以便可以对字段进行命名。 DictReader
和 DictWriter
将行翻译成字典而不是序列。字典的键可以被传入或者从输入的第一行(当该行包含标题时)推断出来。
csv_dictreader.py
import csv
import sys
with open(sys.argv[1], 'rt') as f: reader = csv.DictReader(f) for row in reader: print(row)
基于字典的读取器和写入器被实现为基于序列的类的包装器,并使用相同的方法和参数。读取器唯一的区别是行作为 OrderedDict
实例返回而不是列表或者元组(早期的 Python 版本中,行作为常规 dict
实例返回)。
$ python3 csv_dictreader.py testdata.csv
OrderedDict([('Title 1', '1'), ('Title 2', 'a'), ('Title 3', '08/18/07'), ('Title 4', 'å')]) OrderedDict([('Title 1', '2'), ('Title 2', 'b'), ('Title 3', '08/19/07'), ('Title 4', '∫')]) OrderedDict([('Title 1', '3'), ('Title 2', 'c'), ('Title 3', '08/20/07'), ('Title 4', 'ç')])
必须要给 DictWriter
一个字段名称列表,以至于让它在输出的时候如何排序。
csv_dictwriter.py
import csv
import sys
fieldnames = ('Title 1', 'Title 2', 'Title 3', 'Title 4') headers = { n: n for n in fieldnames } unicode_chars = 'å∫ç' with open(sys.argv[1], 'wt') as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() for i in range(3): writer.writerow({ 'Title 1': i + 1, 'Title 2': chr(ord('a') + i), 'Title 3': '08/{:02d}/07'.format(i + 1), 'Title 4': unicode_chars[i], }) print(open(sys.argv[1], 'rt').read())
字段名称不会被自动写入到文件,但是他们可以显示地使用 writeheader()
方法写入。
$ python3 csv_dictwriter.py testout.csv
Title 1,Title 2,Title 3,Title 4 1,a,08/01/07,å 2,b,08/02/07,∫ 3,c,08/03/07,ç