文章目录
- 第五章 文件与IO
- 5.1 读写文本数据
- 5.2 打印输出至文件中
- 5.3 使用其它分隔符或行终止符打印
- 5.4 读写字节数据
- 5.5 文件不存在才能写入
- 5.6 字符串的 I/O 操作
- 5.7 读写压缩文件
- 5.8 固定大小记录的文件迭代
- 5.9 读取二进制数据到可变缓冲区
- 5.10 内存映射的二进制文件
- 5.11 文件路径名的操作
- 5.12 测试文件是否存在
- 5.13 获取文件夹中的文件列表
- 5.14 忽略文件名编码
- 5.15 打印不合法的文件名
- 5.16 增加或改变已打开文件的编码
- 5.17 将字节写入文本文件
- 5.18 将文件描述符包装成文件对象
- 5.19 创建临时文件和文件夹
- 5.20 与串行端口的数据通信
- 5.21 序列化 Python 对象
第五章 文件与IO
5.1 读写文本数据
-
open() 函数的使用。
三种模式: r,w,a。
常用编码:ascii,latin-1,utf-8,utf-16。
对未知编码的文件,使用latin-1读取不会出现解码错误。之后再把数据回写回去,原先的数据还是保留的。 -
with 语句自动关闭上下文环境。
-
换行符的问题。Unix 和 Windows 的换行符是不一样的。python 读取的时候会统一转换为 \n ,输出时会把 \n 转换为系统默认的值。如果不想这样,需要给 open() 函数传入参数 newline=‘’。
-
编码错误的处理。参数 errors 指定如何处理编码错误。默认 ‘strict’,有错误就会报异常;‘ignore’,跳过错误;‘replace’,解码时使用 U+FFFD 字符替换,编码时使用 ‘?’ 替换。
5.2 打印输出至文件中
print() 函数的 file 关键字参数:
with open('test.txt', 'w') as f:
print('Hi, Jay!', file=f)
5.3 使用其它分隔符或行终止符打印
print() 函数的 sep 和 end 关键字参数。
>>> print(1,2,3,4, sep='-')
1-2-3-4
>>> for i in range(5):
... print(i, end='-')
...
0-1-2-3-4->>>
用 sep 指定分隔符输出是最简单的方式 (str.join() 只能用于字符串)。
5.4 读写字节数据
open() 函数的 ‘wb’、‘rb’ 模式。
写入的必须是字节字符串;读取时读到的也是字节字符串。
数组和 C 结构体对象能直接被写入二进制 I/O。
有的对象允许使用文件对象的 readinto() 方法读取二进制数据到其底层内存中去。
import array
a = array.array('i', [0, 0, 0, 0, 0, 0, 0, 0])
with open('data.bin', 'rb') as f:
f.readinto(a)
# a: [1, 2, 3, 4, 0, 0, 0, 0]
这种做法具有平台相关性。
5.5 文件不存在才能写入
使用 open() 函数的 ‘x’ 或 ‘xb’ 模式。文件已存在时会异常。
5.6 字符串的 I/O 操作
想使用操作文件对象的程序操作字符串对象。
io.StringIO() 或 io.BytesIO() 类创建的类文件对象操作字符串数据。
>>> import io
>>> s = io.StringIO()
>>> s.write('test01')
>>> s.getvalue() # 取出目前写入的所有数据
'test01'
>>> print('test02', file=s)
>>> s.getvalue()
'test01test02\n'
io.StringIO() 和 io.BytesIO() 类能在单元测试中创建一个用于测试的类文件对象。
这两个类的实例没有正确的整数类型的文件描述符,在某些需要用到真实文件的程序中不可使用。
5.7 读写压缩文件
gzip 和 bz2 模块。
import gzip
with gzip.open('test.gz', 'rt') as f:
print(f.read())
with gzip.open('test.gz', 'wt') as f:
f.write('test01')
gzip.open() 和 bz2.open() 函数的默认模式是二进制模式。除了接收和内置的 open() 一样的参数外,还可接收 compresslevel() 参数,指定一个压缩等级。默认为最高压缩级别 9。
这两个函数还能作用在一个已存在并且是二进制模式的文件对象上。
f = open('test', 'rb')
with gzip.open(f, 'rt') as g:
print(g.read())
很多类文件对象都能这样被操作。
5.8 固定大小记录的文件迭代
iter 和 functools.partial() 联合使用。
from functools import partial
RECORD_SIZE = 64
with open('testfile', 'rb') as f:
records = iter(partial(f, RECORD_SIZE), b'')
for r in records:
print(r)
partial() 创建的对象每次调用时会读取指定大小字节的内容。
读取固定大小的记录一般是二进制文件需要的,文本文件通常按行读取。
5.9 读取二进制数据到可变缓冲区
文件对象的 readinto() 函数。
import os.path
def read_into_buffer(filename):
buf = bytearray(os.path.getsize(filename))
with open(filename, 'rb') as f:
f.readinto(buf)
return buf
with open('test.bin', 'wb') as f:
f.write(b'test byte')
buf = read_to_buffer('test.bin')
buf[:4] = b'byte'
print(buf) # bytearray(b'byte byte')
readinto() 能用来为预先分配内存的数组填充数据。readinto() 填充已经存在的内存而不是为对象分配内存后再返回它。可避免大量内存分配的操作。
使用时需要检查返回值,如果返回值小于指定大小,说明数据被截断或破坏了。
record_size = 32
buf = bytearray(record_size)
with open('test.bin', 'rb') as f:
while True:
n = f.readinto(buf)
if n < record_size:
break
print(buf)
memoryview,可以零复制的情况下对缓冲区执行切片操作,还能修改内容。
>>> buf = bytearray(b'test byte')
>>> m1 = memoryview(buf)
>>> m2 = m1[:4]
>>> m2
<memory at 0x0000002AB61C8D08>
>>> m2[:] = b'byte'
>>> buf
bytearray(b'byte byte')
5.10 内存映射的二进制文件
内存映射一个二进制文件到可变数组中,想随机访问或原地修改它。
mmap 能模块将文件映射到内存中,访问时不需要执行大量的 seek()、read()、write() 等操作,直接使用切片操作即可访问数据。
import os
import mmap
def memory_map(filename, access=mmap.ACCESS_WRITE):
"""打开并内存映射一个文件"""
size = os.path.getsize(filename)
fd = os.open(filename, os.O_RDWR)
return mmap.mmap(fd, size, access=access)
使用该函数之前,需要有一个已创建并且不为空的文件。
size = 1000000
with open('testdata', 'wb') as f:
f.seek(size-1)
f.write(b'\x00')
>>> m = memory_map('testdata')
>>> len(m)
1000000
>>> m[:10]
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> m[:4] = b'test'
>>> m.close()
>>> with open('testdata', 'rb') as f:
... print(f.read(4))
...
b'test'
mmap() 返回的对象也能作为一个上下文管理器使用。
with memory_map('testdata') as m:
print(len(m))
memory_map() 打开的文件,修改的内容都会复制到原来的文件中。如果想用只读的访问模式,可以将 access 设为 mmap.ACCESS_READ;想在本地修改,但不想改变源文件内容,可以设为 mmap.ACCESS_COPY。
内存映射文件时,文件并未读取到内存中,而是保留了一段虚拟内存。访问文件的不同区域时,这些区域的内容才被读取映射到内存区域,没访问的部分还在磁盘上。
5.11 文件路径名的操作
os.path 模块
os.path.basename(path):获取路径的最后一个元素;
os.path.dirname(path):获取路径的目录部分;
os.path.join(path, *paths):组合路径;
os.path.expanduser(path):填加根目录路径;
os.path.splitext(path):分离路径中的文件扩展名;
>>> import os
>>> path = 'data/text/test.txt'
>>> os.path.dirname(path)
'data/text'
>>> os.path.basename(path)
'test.txt'
>>> os.path.splitext(path)
('data/text/test', '.txt')
涉及路径操作时,尽量使用 os.path 模块,而不是字符串操作。该模块能可靠地处理 Unix 和 Windows 之间的差异。
5.12 测试文件是否存在
os.path.exists(path):测试一个文件或目录是否存在;
os.path.isfile(path):是否为文件;
os.apth.isdir(path):是否为目录;
os.path.islink(path):是否为符号链接;
os.path.realpath(path):得到被链接的文件;
os.path.getsize(path):获取文件大小;
os.path.getmtime(path):获取修改日期。
5.13 获取文件夹中的文件列表
os.listdir(path):返回目录中的所有文件列表。
可以结合 os.path 模块的其他函数使用列表推导式进行过滤。
需要匹配文件名时,可以使用 glob 或 fnmatch 模块。
需要获取文件的元信息,除了上面的两个之外,还能用 os.stat() 函数。
5.14 忽略文件名编码
默认情况下,文件名会根据 sys.getfilesystemencoding() 返回的文本编码来编码或解码。
想忽略这种方式时,使用一个原始字节字符串来指定文件路径即可。
>>> # 创建文件名包含uincode字符的文件
>>> with open('\xe7\xa7\x91\xe5\xad\xa6', 'w') as f:
... f.write('科学')
...
2
>>> import os
>>> os.listdir('.') # 显示的文件名是解码后的
['科学']
>>> os.listdir(b'.') # 传入字节字符串路径
[b'\xe7\xa7\x91\xe5\xad\xa6']
>>> with open(b'\xe7\xa7\x91\xe5\xad\xa6', 'r') as f:
... print(f.read()) # 使用字节字符串打开
...
'Science.'
这样的操作适合批量处理文件时,避免不符合默认编码的文件名引发错误。
5.15 打印不合法的文件名
文件名不合法,传给 open() 函数能正常工作,但在打印时会出现 UnicodeEncodeError 异常。
def bad_filename(filename):
return repr(filename)[1:-1]
try:
print(filename)
except UnicodeEncodeError:
print(bad_filename(filename))
也能在 bad_filename() 中转使用其它编码。
def bad_filename(filename):
t = filename.encode(sys.getfilesystemencoding(), errors='surrogateescape')
return t.decode('latin-1')
surrogateescape 是 python 面向 OS 的 API 中使用的错误处理器。解码出错时,会把出错的字节存到很少用的 Unicode 编码范围,编码时又还原为原先的字节序列。
5.16 增加或改变已打开文件的编码
I/O 系统的层次:
>>> f = open('test.txt', 'w')
>>> f
<_io.TextIOWrapper name='test.txt' mode='w' encoding='cp936'>
>>> f.buffer
<_io.BufferedWriter name='test.txt'>
>>> f.buffer.raw
<_io.FileIO name='test.txt' mode='wb' closefd=True>
io.TextIOWrapper 是编码、解码 Unicode 的文本处理层。io.BufferedWriter 是处理二进制数据的带缓冲的 I/O 层。io.FileIO 是一个表示操作系统底层文件描述符的原始文件。
改变文本编码会涉及改变最上层的 io.TextIOWrapper 层。
但是如果使用上面的属性访问的方式修改上层编码,底层的文件也会被关闭。
>>> f = io.TextIOWrapper(f.buffer, encoding='latin-1')
>>> f
<_io.TextIOWrapper name='test.txt' encoding='latin-1'>
>>> f.write('ok!')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.
detach() 方法能断开文件的顶层,并把第二层返回。此时可以为第二层添加一个新的顶层。
>>> f = open('test.txt', 'w')
>>> f
<_io.TextIOWrapper name='test.txt' mode='w' encoding='cp936'>
>>> b = f.detach()
>>> b
<_io.BufferedWriter name='test.txt'>
>>> f.write('ok!')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: underlying buffer has been detached
>>> f = io.TextIOWrapper(b, encoding='latin-1')
>>> f
<_io.TextIOWrapper name='test.txt' encoding='latin-1'>
5.17 将字节写入文本文件
文本文件通过在一个有缓冲区的二进制模式文件上增加一个 Unicode 编码解码层来创建。文件的 buffer 属性指向对应额底层文件,直接访问该属性就能绕过编码解码层。
>>> f
<_io.TextIOWrapper name='test.txt' mode='w' encoding='cp936'>
>>> f.write(b'123')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: write() argument must be str, not bytes
>>> f.buffer.write(b'123')
3
5.18 将文件描述符包装成文件对象
有一个对应操作系统上已打开的 I/O 通道的整型文件描述符,想将其包装为文件对象。
文件描述符是由操作系统指定的整数,用来代指 I/O 通道。可以使用 open() 函数将其包装成一个 python 文件对象。
import os
# os.open():打开底层文件,返回文件描述符
fd = os.open('test.txt', os.O_WRONLY)
f = open(fd, 'w')
f.write('Ok!!!')
f.close()
高层的文件关闭时,底层的文件描述符也会被关闭。设置 open() 函数的可选参数 closefd=False 可以避免这样。
???
5.19 创建临时文件和文件夹
tempfile 模块。
from tempfile import TemporaryFile
with TemporaryFile('w+t') as f:
f.write('Ok!\n')
f.write("Let's go!")
f.seek(0)
data = f.read()
TemporaryFile() 第一个参数是模式,默认是 w+b,文本模式使用 w+t。模式同时支持读和写。此外也能接收和 open() 函数一样的若干参数。临时文件在关闭时会自动销毁,如果不想这样,可以在创建时指定关键字参数 delete=False。
Unix 系统中,TemporaryFile 创建的临时文件都是匿名的,若需要使用文件的名字,可以使用该模块的 NamedTemporaryFile,创建的文件的 name 属性就有该临时文件的文件名。
使用 tempfile.TemporaryDirectory() 能创建临时文件夹。
上述的三个函数会自动处理创建和清理的步骤。创建的临时文件位置通常是在系统默认位置,可以通过函数 tempfile.gettempdir() 来获取文件的真实位置。创建时,能使用关键字参数(prefix,suffix,dir)指定目录和命名规则。
5.20 与串行端口的数据通信
与硬件设备打交道。
使用 pyserial 包。
5.21 序列化 Python 对象
pickle 模块。
序列化和反序列化。
由于 pickle 在加载时会自动加载相应模块并构造对象,处于安全考虑,pickle 只适合在相互之间可以认证的解析器之间使用。
有些依赖外部系统状态的对象是不能被序列化的,打开的文件、网络连接、线程、进程、栈帧等。自定义类可以通过定义 __getstate__() 和 __setstate__() 方法来绕过这些限制。pickle.dump() 时会调用 __getstate__() 获取序列化的对象,反序列化时调用 __setstate__()。
需要长期存储的数据不应该使用 pickle,因为 pickle 依附于 python 源码,源码变动时,存储的数据可能会变得不可读取。存档时最好使用标准的编码格式如 XML,CSV 或 JSON。这些格式更标准且能适应不同语言,也支持源码变更。