官方文档很罗嗦,长篇大论例子少。本文将举例说明 csv
包的用法,然后补充一些必要的说明。
CSV(Comma-Separated Values,逗号分隔值)文件是一种常见的以纯文本形式存储数据的文件格式。它使用逗号作为字段之间的分隔符,并且每一行表示一个记录。
以下是一个简单的示例,展示了一个包含姓名、年龄和城市信息的 CSV 文件:
Name, Age, City
John, 25, New York
Alice, 30, Los Angeles
Bob, 35, Chicago
在上述示例中,第一行是标题行,包含了三个字段(field)的名称:Name、Age 和 City。接下来的每一行都是一个记录,每个字段之间由逗号进行分隔。
在 Python 中,可以使用内置的 csv
模块来读取和写入 CSV 文件。该模块提供了方便的函数和方法来处理 CSV 数据,例如 csv.reader()
用于读取 CSV 文件,csv.writer()
用于写入 CSV 文件,以及其他用于设置分隔符、引号字符等的选项。
reader(iterable, dialect='excel', *args, **kwargs)
这个函数标签是 PyCharm 生成的伪标签,实际的代码在底层,不是 python 语言,故无法查看。读:
import csv
# 记刚才生成的 csv 文件名为 'demo.csv'
with open('demo.csv', 'r', newline='') as csvfile: # 这个 newline='' 是官网建议的
samples = csv.reader(csvfile)
for sample in samples:
print(sample)
### output ###
['Name', ' Age', ' City']
['John', ' 25', ' New York']
['Alice', ' 30', ' Los Angeles']
['Bob', ' 35', ' Chicago']
【注】这个 newline=''
是官网建议的,读的时候没必要,但写的时候不设置为 ''
就会出现多于的空行(winsows下),大概是 windows 使用 \r\n
换行吧,Linux 下就不会有多于的空行。
按照 reader(iterable, dialect='excel', *args, **kwargs)
,它是可以接收任何 iterable
对象的,我试了一下,确实可以:
for row in csv.reader('abc', delimiter=','):
print(row)
### output ###
['a']
['b']
['c']
# 如果 ('abc', 'def'), 则 ['abc'] ['def']
但是这个迭代器要返回 str
才行,如果 csv.reader((1, 2))
,则出现错误:
_csv.Error: iterator should return strings, not int (did you open the file in text mode?)
由此看来,csv.reader
是需要 iterable
返回 str
,然后再对 str
根据 delimiter
进行分割:
for row in csv.reader(('a,bc', 'def'), delimiter='b'):
print(row)
### output ###
['a,', 'c']
['def']
果然把 ‘a,bc’ 中的 ‘b’ 当分隔符了。还发现,每行的分割数量可以不一致。
小结:reader(iterable, dialect='excel', *args, **kwargs)
接收一个字符串迭代器,返回一个字符串列表迭代器:迭代过程中,根据 delimiter
把字符串分割成字符串列表。如 csv.reader('abc', delimiter=',')
,迭代 a
, b
, c
三个字符串,每个字符串根据 ','
分割,形成列表 ['a'], ['b'], ['c']
。
open()
那么 open('demo.py', 'r')
是一个 str
迭代器咯?是的:
open
返回的对象是 TextIOWrapper
,它在 io
包中,类结构如上图所示,它是个迭代器。
之前只知道常见的文件读取函数:
f.read() # 读取全部文本,返回一个字符串。这种方法适用于一次性处理小的文件。
f.readline() # 按行读取文件,每次返回一个字符串。
f.readlines() # 读取全部文本,返回一个字符串列表。
不知道 f
本身是什么,既然现在知道它是迭代器,那看一看 for ... in ...
会发生什么:
with open('demo.csv', 'r') as f:
for item in f:
print(item)
### output ###
Name, Age, City
John, 25, New York
Alice, 30, Los Angeles
Bob, 35, Chicago
每个迭代元素是文件中文本的一行,并带有 \n
换行符,这和 f.readlines()
是一样的,只不过后者是一次性读取所有行,前者是边读边迭代。不止如此,既然能 with
,那么它肯定也是上下文管理器。这个迭代器是可关闭的。
回到 csv.reader(csvfile)
,看一看文件迭代器关闭后继续迭代 csv.reader(csvfile)
会怎样:
with open('demo.py', 'r') as csvfile:
samples = csv.reader(csvfile)
for sample in samples: # csv 文件已关闭
print(sample)
# ValueError: I/O operation on closed file.
报了 ValueError: I/O operation on closed file.
错,这和读写已关闭的文件是一样的。也就是说,csv.reader(csvfile)
并不是一次性读取文件返回给 samples
对象,它只是包装了 csvfile
,实际的读写操作还是由 csvfile
对象完成的。但很遗憾,只能到这,samples = csv.reader(csvfile) 返回的 _reader
类型到底是怎样的,无法得知。
小结:reader(csvfile)
包装一个文件对象 csvfile
(是个字符串迭代器),每次迭代一行,根据 delimiter
把这行字符串分割成字符串列表,过程中 csvfile
不能关闭。
再读文档就明白了:
The “iterable” argument can be any object that returns a line of input for each iteration, such as a file object or a list. The optional “dialect” parameter is discussed below. The function also accepts optional keyword arguments which override settings provided by the dialect.
The returned object is an iterator. Each iteration returns a row of the CSV file (which can span multiple input lines).
从 reader(iterable, dialect='excel', *args, **kwargs)
和 csv.reader('abc', delimiter=',')
来看,参数可以通过类似 delimiter=','
的键值对进行配置,但不知道都有哪些参数可配置。注意到 dialect='excel'
,在文档中有这么一段话:
The function also accepts optional keyword arguments which override settings provided by the dialect.
也即 dialect='excel'
提供了参数设置。继续看文档怎么写(翻译):
Readers
和 Writers
都支持 dialect
参数,该参数是对一组 setting 的方便 handle。当 dialect
参数是字符串时,它标识模块中预先注册的一种 dialect
。如果它是一个类或实例,则参数的属性用作读取器或写入器的设置:
class excel(Dialect):
delimiter = ','
quotechar = '"'
escapechar = None
doublequote = True
skipinitialspace = False
lineterminator = '\r\n'
quoting = QUOTE_MINIMAL
SETTINGS 意义:
quotechar
- 指定一个字符作为 quoting character (引号符);delimiter
- 指定一个字符作为字段 separator (分隔符) ;skipinitialspace
- 如何解释 whitespace which immediately follows a delimiter,如果为 False,则把分隔符后的空格当作字段值的一部分;lineterminator
- 结束一行的字符序列,应该是 '\n'
之类的;quoting
- 控制 writer
函数给字段值加引号的规则,可以是以下 module constants:csv.QUOTE_MINIMAL = 0
– 必要时才加引号,例如,当字段值包含引号或分隔符时;csv.QUOTE_ALL = 1
– “always” 加引号;csv.QUOTE_NONNUMERIC = 2
– 非数字的字段值加引号;csv.QUOTE_NONE = 3
– never 加引号;escapechar
- 指定在引号设置为 QUOTE_NONE
时用于转义分隔符的字符;doublequote
- 处理字段内部的引号,如果为 True,读文件时两个连续的引号解释为一个,写入时每个引号写成两个。还有两个定义好的 Dialect
类:
class excel_tab(excel):
"""Describe the usual properties of Excel-generated TAB-delimited files."""
delimiter = '\t'
register_dialect("excel-tab", excel_tab)
class unix_dialect(Dialect):
"""Describe the usual properties of Unix-generated CSV files."""
delimiter = ','
quotechar = '"'
doublequote = True
skipinitialspace = False
lineterminator = '\n'
quoting = QUOTE_ALL
register_dialect("unix", unix_dialect)
如果想自己定义 Dialect
,只需要模仿着继承 class Dialect
就行。然后就是注册。
注册 register_dialect("unix", unix_dialect)
:
def register_dialect(name, dialect=None, **fmtparams=None): # real signature unknown; restored from __doc__
"""
Create a mapping from a string name to a dialect class.
dialect = csv.register_dialect(name[, dialect[, **fmtparams]])
"""
pass
注册 Dialect
,就是给定义的 Dialect
关联一个字符串名字,以便以类似 dialect='excel'
的方式设置参数,当然直接给类或实例也是可以的。
Dialect
类中的参数可以直接以 **kwargs
键值对的方式送到 csv.reader()
函数中。
Dialect
相关的几个函数这些都是 PyCharm 根据文档生成的函数伪标签:
def get_dialect(name): # real signature unknown; restored from __doc__
"""
Return the dialect instance associated with name.
dialect = csv.get_dialect(name)
打印出来得到类似 <_csv.Dialect object at 0x000002606C5EC990> 的东西
"""
pass
def list_dialects(): # real signature unknown; restored from __doc__
"""
Return a list of all know dialect names.
names = csv.list_dialects()
已注册的 Dialect 列表吧
"""
pass
def register_dialect(name, dialect=None, **fmtparams=None): # real signature unknown; restored from __doc__
"""
Create a mapping from a string name to a dialect class.
dialect = csv.register_dialect(name[, dialect[, **fmtparams]])
"""
pass
def unregister_dialect(name): # real signature unknown; restored from __doc__
"""
Delete the name/dialect mapping associated with a string name.
csv.unregister_dialect(name)
"""
pass
试验:
class B(csv.excel):
delimiter = 'b' # 设置字符 b 为分隔符
csv.register_dialect('b', B) # 注册名为 b
print(csv.list_dialects())
### output ###
['excel', 'excel-tab', 'unix', 'b'] # 多了一个 `b`
def field_size_limit(limit=None): # real signature unknown; restored from __doc__
"""
Sets an upper limit on parsed fields. csv.field_size_limit([limit])
Returns old limit. If limit is not given, no new limit is set and
the old limit is returned
"""
pass
设置了字段的最大长度,如果超过 limit
,则报错:
_csv.Error: field larger than field limit (limit值)
def writer(fileobj, dialect='excel', *args, **kwargs): # real signature unknown; NOTE: unreliably restored from __doc__
"""
csv_writer = csv.writer(fileobj [, dialect='excel'] [optional keyword args])
for row in sequence:
csv_writer.writerow(row)
[or]
csv_writer = csv.writer(fileobj [, dialect='excel'] [optional keyword args])
csv_writer.writerows(rows)
The "fileobj" argument can be any object that supports the file API.
"""
pass
试一试:
sequence = [
['A', 18, 'Zhoukou'],
['B', 19, 'Nanjing']
]
with open('./demo.csv', 'w') as csvfile:
csv_writer = csv.writer(csvfile, quoting=csv.QUOTE_NONNUMERIC)
for row in sequence:
csv_writer.writerow(row)
csv_writer.writerows(sequence)
发现文件内容变成了:
"A",18,"Zhoukou"
"B",19,"Nanjing"
"A",18,"Zhoukou"
"B",19,"Nanjing"
即是覆盖写入的,如果把 open('./demo.csv', 'w')
中的 'w'
改成 'a'
,就可以追加写入了。再一次证实了 csv.reader(...)
和 csv.writer(...)
只是包装了文件读写对象。
关于 io.TextIOWrapper
,GPT3.5 给出的教程:
TextIOWrapper
是一个文本文件包装器,它用于对字节流进行编码和解码,完成文本和字节流的相互转换,并允许以文本形式读取或写入文件。用法:
TextIOWrapper
对象:import io
# 打开二进制文件并创建 TextIOWrapper 对象
with open('./data/demo.csv', 'rb') as binary_file: # io.BufferedReader
text_file = io.TextIOWrapper(binary_file, encoding='utf-8')
# 通过 TextIOWrapper 对象读取文本内容
content = text_file.read()
print(content)
在上述示例中,我们首先使用 open() 函数打开一个以二进制模式(‘rb’)读取的文件(type(binary_file)==io.BufferedReader
),并将其传递给 TextIOWrapper
的构造函数。我们还指定了所使用的编码方式(encoding='utf-8'
)。这样就创建了一个 TextIOWrapper
对象 text_file
,可以以文本形式读取文件内容。
TextIOWrapper
对象写入文本内容也是一样的,只是它包装一个写入流 open('./data/demo.csv', 'wb')