文件与 IO(一)
1.读写文本数据
你需要读写各种不同编码的文本数据,比如 ASCII
,UTF-8
或 UTF-16
编码等。
使用带有 rt
模式的 open()
函数读取文本文件。如下所示:
python
# Read the entire file as a single string
with open('somefile.txt', 'rt') as f:
data = f.read()
# Iterate over the lines of the file
with open('somefile.txt', 'rt') as f:
for line in f:
# process line
...
🚀 在 Python 中,
open()
函数的rt
模式表示以 文本模式读取 (read text)文件。
r
:只读模式 (默认模式),文件必须存在,否则会报错FileNotFoundError
。t
:文本模式 (默认模式),文件内容会被解码为字符串(Python 3 中默认是str
类型,Unicode 编码)。
因此,rt
等价于单独使用 r
(因为 t
是默认值)。以下两种写法完全等效:
python
open('file.txt', 'rt') # 显式指定 rt
open('file.txt', 'r') # 隐式使用默认的 t 模式
类似的,为了写入一个文本文件,使用带有 wt
模式的 open()
函数,如果之前文件内容存在则清除并覆盖掉。如下所示:
python
# Write chunks of text data
with open('somefile.txt', 'wt') as f:
f.write(text1)
f.write(text2)
...
# Redirected print statement
with open('somefile.txt', 'wt') as f:
print(line1, file=f)
print(line2, file=f)
...
🚀 在 Python 中,
open()
函数的wt
模式表示以 文本模式写入 (write text)文件。
w
:写入模式 (如果文件已存在,会 清空内容 ;如果文件不存在,会 创建新文件)。t
:文本模式 (默认模式),写入的内容必须是字符串(str
类型),而非二进制数据(bytes
)。
因此,wt
等价于单独使用 w
(因为 t
是默认值)。以下两种写法完全等效:
python
open('file.txt', 'wt') # 显式指定 wt
open('file.txt', 'w') # 隐式使用默认的 t 模式
如果是在已存在文件中添加内容,使用模式为 at
的 open()
函数。
模式 | 描述 |
---|---|
wb |
二进制模式写入(需 bytes 类型数据) |
w+ 或 w+t |
可读写文本模式(先清空文件) |
a 或 at |
追加文本模式(不覆盖原内容) |
x 或 xt |
独占创建模式(文件存在则报错) |
文件的读写操作默认使用系统编码,可以通过调用 sys.getdefaultencoding()
来得到。在大多数机器上面都是 utf-8
编码。如果你已经知道你要读写的文本是其他编码方式,那么可以通过传递一个可选的 encoding
参数给 open()
函数。如下所示:
python
with open('somefile.txt', 'rt', encoding='latin-1') as f:
...
Python 支持非常多的文本编码。几个常见的编码是 ascii
、latin-1
、utf-8
和 utf-16
。
- 在 Web 应用程序中通常都使用的是 UTF-8。
ascii
对应从U+0000
到U+007F
范围内的 7 位字符。latin-1
是字节 0-255 到 U+0000 至 U+00FF 范围内 Unicode 字符的直接映射。
当读取一个未知编码的文本时使用 latin-1
编码永远不会产生解码错误。使用 latin-1
编码读取一个文件的时候也许不能产生完全正确的文本解码数据,但是它也能从中提取出足够多的有用数据。同时,如果你之后将数据回写回去,原先的数据还是会保留的。
🚀 Latin-1(也称为 ISO-8859-1)是一种单字节字符编码标准,属于 ISO/IEC 8859 系列编码的一部分。
- 编码范围:
- 使用 8 位(1字节)表示一个字符
- 共 256 个码位(
0x00
-0xFF
)- 其中
0x00
-0x7F
与 ASCII 完全一致0x80
-0xFF
用于扩展字符(西欧语言字符)
- 覆盖语言:
- 主要支持西欧语言(英语、法语、德语、西班牙语、葡萄牙语等)
- 包含重音字母、货币符号等特殊字符
- Unicode关系:
- Latin-1 字符集中的字符在 Unicode 中的码位与 Latin-1 编码值相同(
U+0000
到U+00FF
)
读写文本文件一般来讲是比较简单的。但是也有几点是需要注意的。首先,在例子程序中的 with
语句给被使用到的文件创建了一个上下文环境, 但 with
控制块结束时,文件会自动关闭。你也可以不使用 with
语句,但是这时候你就必须记得手动关闭文件:
python
f = open('somefile.txt', 'rt')
data = f.read()
f.close()
另外一个问题是关于换行符的识别问题,在 Unix 和 Windows 中是不一样的(分别是 \n
和 \r\n
)。 默认情况下,Python 会以统一模式处理换行符。这种模式下,在读取文本的时候,Python 可以识别所有的普通换行符并将其转换为单个 \n
字符。 类似的,在输出时会将换行符 \n
转换为系统默认的换行符。如果你不希望这种默认的处理方式,可以给 open()
函数传入参数 newline=''
,就像下面这样:
python
# Read with disabled newline translation
with open('somefile.txt', 'rt', newline='') as f:
...
在 Python 的
open()
函数中,newline
参数用于控制文本模式下的 换行符(行结束符)处理方式。它在不同操作系统的换行符兼容性中尤为重要。
为了说明两者之间的差异,下面我在 Unix 机器上面读取一个 Windows 上面的文本文件,里面的内容是 hello world!\r\n
:
python
>>> # Newline translation enabled (the default)
>>> f = open('hello.txt', 'rt')
>>> f.read()
'hello world!\n'
>>> # Newline translation disabled
>>> g = open('hello.txt', 'rt', newline='')
>>> g.read()
'hello world!\r\n'
>>>
取值 | 读取时(输入) | 写入时(输出) |
---|---|---|
None (默认) |
自动将不同系统的换行符(\n 、\r\n 、\r )统一转换为 \n 。 |
根据当前操作系统转换: (1)Unix/Linux:\n (2)Windows:\r\n (3)旧版 Mac:\r |
'' (空字符串) |
保留原始换行符(不转换),文件中的 \n 、\r\n 、\r 会原样保留。 |
直接输出写入的 \n ,不进行任何转换。 |
其他字符串(如 '\n' 、'\r\n' ) |
强制将指定的字符串作为换行符解析(极少用)。 | 将所有的 \n 替换为指定的字符串(如 '\r\n' )。 |
最后一个问题就是文本文件中可能出现的编码错误。但你读取或者写入一个文本文件时,你可能会遇到一个编码或者解码错误。比如:
python
>>> f = open('sample.txt', 'rt', encoding='ascii')
>>> f.read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.3/encodings/ascii.py", line 26, in decode
return codecs.ascii_decode(input, self.errors)[0]
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position
12: ordinal not in range(128)
>>>
如果出现这个错误,通常表示你读取文本时指定的编码不正确。你最好仔细阅读说明并确认你的文件编码是正确的(比如使用 UTF-8 而不是 Latin-1 编码或其他)。如果编码错误还是存在的话,你可以给 open()
函数传递一个可选的 errors
参数来处理这些错误。 下面是一些处理常见错误的方法:
python
>>> # Replace bad chars with Unicode U+fffd replacement char
>>> f = open('sample.txt', 'rt', encoding='ascii', errors='replace')
>>> f.read()
'Spicy Jalape?o!'
>>> # Ignore bad chars entirely
>>> g = open('sample.txt', 'rt', encoding='ascii', errors='ignore')
>>> g.read()
'Spicy Jalapeo!'
>>>
如果你经常使用 errors
参数来处理编码错误,可能会让你的生活变得很糟糕。对于文本处理的首要原则是确保你总是使用的是正确编码。当模棱两可的时候,就使用默认的设置(通常都是 UTF-8)。
注意事项
- 二进制模式无效 :
newline
仅在文本模式(如rt
、wt
)下生效,二进制模式(如rb
、wb
)会忽略它。- CSV 文件处理 :
csv
模块在写入时通常需要指定newline=''
,以避免额外空行(官方文档推荐)。- 性能影响:频繁的换行符转换可能轻微影响性能,但对大多数场景可忽略。
2.打印输出至文件中
你想将 print()
函数的输出重定向到一个文件中去。
在 print()
函数中指定 file
关键字参数,像下面这样:
python
with open('d:/work/test.txt', 'wt') as f:
print('Hello World!', file=f)
关于输出重定向到文件中就这些了。但是有一点要注意的就是文件必须是以文本模式打开。如果文件是二进制模式的话,打印就会出错。
3.使用其他分隔符或行终止符打印
你想使用 print()
函数输出数据,但是想改变默认的分隔符或者行尾符。
可以使用在 print()
函数中使用 sep
和 end
关键字参数,以你想要的方式输出。比如:
python
>>> print('ACME', 50, 91.5)
ACME 50 91.5
>>> print('ACME', 50, 91.5, sep=',')
ACME,50,91.5
>>> print('ACME', 50, 91.5, sep=',', end='!!\n')
ACME,50,91.5!!
>>>
使用 end
参数也可以在输出中禁止换行。比如:
python
>>> for i in range(5):
... print(i)
...
0
1
2
3
4
>>> for i in range(5):
... print(i, end=' ')
...
0 1 2 3 4 >>>
当你想使用非空格分隔符来输出数据的时候,给 print()
函数传递一个 sep
参数是最简单的方案。 有时候你会看到一些程序员会使用 str.join()
来完成同样的事情。比如:
python
>>> print(','.join(('ACME','50','91.5')))
ACME,50,91.5
>>>
str.join()
的问题在于它仅仅适用于字符串。这意味着你通常需要执行另外一些转换才能让它正常工作。比如:
python
>>> row = ('ACME', 50, 91.5)
>>> print(','.join(row))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sequence item 1: expected str instance, int found
>>> print(','.join(str(x) for x in row))
ACME,50,91.5
>>>
你当然可以不用那么麻烦,只需要像下面这样写:
python
>>> print(*row, sep=',')
ACME,50,91.5
>>>
4.读写字节数据
你想读写二进制文件,比如图片,声音文件等等。
使用模式为 rb
或 wb
的 open()
函数来读取或写入二进制数据。比如:
python
# Read the entire file as a single byte string
with open('somefile.bin', 'rb') as f:
data = f.read()
# Write binary data to a file
with open('somefile.bin', 'wb') as f:
f.write(b'Hello World')
在读取二进制数据时,需要指明的是所有返回的数据都是字节字符串格式的,而不是文本字符串。类似的,在写入的时候,必须保证参数是以字节形式对外暴露数据的对象(比如字节字符串,字节数组对象等)。
在读取二进制数据的时候,字节字符串 和 文本字符串 的语义差异可能会导致一个潜在的陷阱。特别需要注意的是,索引和迭代动作返回的是字节的值而不是字节字符串。比如:
python
>>> # Text string
>>> t = 'Hello World'
>>> t[0]
'H'
>>> for c in t:
... print(c)
...
H
e
l
l
o
...
>>> # Byte string
>>> b = b'Hello World'
>>> b[0]
72
>>> for c in b:
... print(c)
...
72
101
108
108
111
...
>>>
如果你想从二进制模式的文件中读取或写入文本数据,必须确保要进行解码和编码操作。比如:
python
with open('somefile.bin', 'rb') as f: # 以二进制只读模式打开文件
data = f.read(16) # 读取前16字节的二进制数据
text = data.decode('utf-8') # 将二进制数据解码为字符串(UTF-8)
with open('somefile.bin', 'wb') as f: # 以二进制写入模式打开文件
text = 'Hello World' # 定义字符串
f.write(text.encode('utf-8')) # 将字符串编码为二进制后写入文件
二进制 I/O 还有一个鲜为人知的特性,就是数组和 C 结构体类型能直接被写入,而不需要中间转换为自己对象。比如:
python
import array
nums = array.array('i', [1, 2, 3, 4])
with open('data.bin','wb') as f:
f.write(nums)
这个适用于任何实现了被称之为 缓冲接口 的对象,这种对象会直接暴露其底层的内存缓冲区给能处理它的操作。二进制数据的写入就是这类操作之一。
很多对象还允许通过使用文件对象的 readinto()
方法直接读取二进制数据到其底层的内存中去。比如:
python
>>> import array
>>> a = array.array('i', [0, 0, 0, 0, 0, 0, 0, 0])
>>> with open('data.bin', 'rb') as f:
... f.readinto(a)
...
16
>>> a
array('i', [1, 2, 3, 4, 0, 0, 0, 0])
>>>
🚀
readinto()
是 Python 文件对象(io.FileIO
或二进制模式打开的文件)的一个方法,用于 高效地将文件数据读取到预分配的字节缓冲区(bytearray
或memoryview
) ,而不是返回一个新的bytes
对象。它的主要用途是 减少内存分配,提高 I/O 性能,尤其是在处理大文件或高频读取时。
但是使用这种技术的时候需要格外小心,因为它通常具有平台相关性,并且可能会依赖字长和字节顺序(高位优先和低位优先)。
方法 | 返回值 | 内存分配 | 适用场景 |
---|---|---|---|
read(n) |
bytes |
每次返回新对象 | 简单读取 |
readinto(buffer) |
int (实际读取字节数) |
复用现有缓冲区 | 高性能 I/O |
5.文件不存在才能写入
你想向一个文件中写入数据,但是前提必须是这个文件在文件系统上不存在。也就是不允许覆盖已存在的文件内容。
w
模式(写入模式)
行为 :
- 如果文件 已存在 ,会 直接覆盖(清空原有内容)。
- 如果文件 不存在 ,会 创建新文件。
风险:可能意外覆盖重要文件!
x
模式(独占创建模式)行为 :
- 如果文件 已存在 ,会抛出
FileExistsError
异常。- 如果文件 不存在 ,会 创建新文件。
用途:确保不会意外覆盖已有文件,适合需要安全写入的场景。
可以在 open()
函数中使用 x
模式来代替 w
模式的方法来解决这个问题。比如:
python
>>> with open('somefile', 'wt') as f:
... f.write('Hello\n')
...
>>> with open('somefile', 'xt') as f:
... f.write('Hello\n')
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileExistsError: [Errno 17] File exists: 'somefile'
>>>
如果文件是二进制的,使用 xb
来代替 xt
。
这一小节演示了在写文件时通常会遇到的一个问题的完美解决方案(不小心覆盖一个已存在的文件)。一个替代方案是先测试这个文件是否存在,像下面这样:
python
>>> import os
>>> if not os.path.exists('somefile'):
... with open('somefile', 'wt') as f:
... f.write('Hello\n')
... else:
... print('File already exists!')
...
File already exists!
>>>
显而易见,使用 x
文件模式更加简单。要注意的是 x
模式是一个 Python3 对 open()
函数特有的扩展。在 Python 的旧版本或者是 Python 实现的底层 C 函数库中都是没有这个模式的。