使用文件对象包装文件描述符
1. 读写文本数据
对文本数据的读写是一个常用的操作。通常,open 函数都可以满足需求。如果需要读取一个文本,可以使用 open 函数的 rt 模式:
>>> # read(),一次性读取全部文本
>>> with open("./file1", "rt") as f:
... f.read()
...
'This is 1 line.\nThis is 2 line.\nThis is 3 line.\nThis is 4 line.\n'
>>> # 逐行读取文本
>>> with open("./file1", "rt") as f:
... for line in f:
... print(line)
...
This is 1 line.
This is 2 line.
This is 3 line.
This is 4 line.
>>>
如果需要对一个文本执行写操作,可以使用 open 函数的 wt 模式,如果文本已存在,该模式将会清除并覆盖原内容:
>>> # write(text),向文件写入文本
>>> with open("./file1", "rt") as f:
... f.write(text)
如果要在已有文本后面追加内容,可以使用 open 函数的 at 模式:
>>> # write(text),向文件写入文本
>>> with open("./file1", "at") as f:
... f.write(text)
默认情况下,文本的读取会使用系统默认的编码方式,系统默认编码可以使用 sys.getdefaultencoding() 来查询。
>>> sys.getdefaultencoding()
'utf-8'
注意
在使用 open() 函数时,有几点细节需要注意:
- 一般来说,使用 open 函数时会使用 with 语句为使用的文件创建一个上下文环境,当程序离开 with 后,文件自动关闭。如果不使用 with 语句,需注意在文件使用结束后,手动使用 f.close() 来关闭文件。
- 由于不同系统的换行符略有差别(UNIX的 ‘\n’ 和 WINDOWS的 ‘\r\n’),Python 默认工作在 “通用型换行符” 模式下。该模式下,所有常见的换行格式都能识别,并正确处理。如果不使用默认模式,可以通过 open 函数的 newline=‘\n’ 参数指定。
- open 函数也可以使用 encoding 参数手动指定编码格式,如果还会出现错误,还可以通过 errors 参数(replace/ignore)来指定如果处理编码错的字符。
2. 读写二进制数据
除文本数据外,通常也需要读取二进制数据,如图像、声音文件。简单地,可以使用 open 函数的 rb 或 wb 模式来实现读写二进制数据:
>>> with open('file2', 'rb') as f:
... f.read()
...
b'This is a line.\n'
>>> with open('file2', 'wb') as f:
... f.write(b'This is an now line.')
...
20
读取二进制数据时,所有的数据是以字节串的形式返回的,而不是文本字符串。字节串和字符串之间存在一些差异,在做索引和迭代操作时,字节串会返回整数值而不是字符串:
>>> for c in b'example':
... print(c)
...
101
120
97
109
112
108
101
>>> for c in 'example':
... print(c)
...
e
x
a
m
p
l
e
在执行写入操作时,可能会碰到这样一个需求:我们需要在只有文件不存在时,才执行写入操作。要实现这样的操作,可以使用 x 模式,使用 xt、xb 替换 wt、 wb:
>>> with open('file2', 'xb') as f:
... f.write(b'This is an now line.')
3. open 函数使用文件对象包装文件描述符
open 函数以文本模式和以字节模式打开文件的处理有些不同。直觉上,字节到文件之间多了一个编码过程。那么,是否可以将以字节模式打开的文本重新添加编码呢?答案是可以的,使用 io.TextIOWrapper 装饰器可以达成这样的目的:
>>> with open('file2', 'rb') as f:
... f.read()
...
b'This is an example.\n'
>>>
>>>
>>> with open('file2', 'rb') as f:
... f = io.TextIOWrapper(f, encoding='utf-8')
... f.read()
...
'This is an example.\n'
>>>
这是如何实现的呢?首先观察文件对象 f :
>>> f = open('file2', 'r')
>>> f
<_io.TextIOWrapper name='file2' mode='r' encoding='UTF-8'>
>>> f.buffer
<_io.BufferedReader name='file2'>
>>> f.buffer.raw
<_io.FileIO name='file2' mode='rb' closefd=True>
>>>
可以看到,I/O 系统是以不同的层次来构建的,逐层包装文件描述符。首先最上层 io.TextIOWrapper 是一个文本处理层,它负责编码和解码 Unicode。下一层 io.BufferedReader 是一个缓冲 I/O 层,负责处理二进制数据。最后一层,io.FileIO 是原始文件在操作系统层面的文件描述符。所以,可以在最上层的 io.TextIOWrapper 添加和修改文本的编码。
注意
如果使用如下的形式修改编码,会出现问题:
>>> f = io.TextIOWrapper(f.buffer, encoding='utf-8') >> f <_io.TextIOWrapper name='file2' encoding='utf-8'> >> f.read() Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: I/O operation on closed file.
这是因为 f 之前的值已经被销毁,这个过程中底层的文件被关闭了。为了解决这个问题,可以使用文件对象的 detach() 方法,detach() 方法会将最上层的 io.TextIOWrapper 层分离出来并返回下一个层次 io.BufferedReader。
>>> f = open('file2', 'r') >>> f.detach() <_io.BufferedReader name='file2'>
f = io.TextIOWrapper(f.detach(), encoding='utf-8')
这种特征可以用来实现一些特别的操作,比如,将字节数据形式读取以文本模式打开的文件。可以通过直接访问文件对象的 buffer 实现,buffer 属性简单地指向了底层的文件,直接访问可以绕过文件对象的解码/编码层。
>>> f = open('file2', 'rt')
>>> f.read()
'This is an example.\n'
>>> # 因为文件指针已经直到了文件底部,所以再次读取时为空
>>> f.buffer.read()
b''
>>> f.close()
>>>
>>> f = open('file2', 'rt')
>>> f.buffer.read()
b'This is an example.\n'
>>> f.close()