note-PythonCookbook-第五章 文件与IO

第五章 文件与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。这些格式更标准且能适应不同语言,也支持源码变更。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值