目录
5.1 读写文本数据
open
(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)¶
打开 file 并返回对应的 file object。如果该文件不能打开,则触发 OSError
。
file 是一个 path-like object,表示将要打开的文件的路径(绝对路径或者当前工作目录的相对路径),也可以是要被封装的整数类型文件描述符。(如果是文件描述符,它会随着返回的 I/O 对象关闭而关闭,除非 closefd 被设为 False
。)
mode 是一个可选字符串,用于指定打开文件的模式。
buffering 是一个可选的整数,用于设置缓冲策略。缓冲是指用于读取文件的缓冲区,缓冲区就是一段内存区域。设置缓冲区的目的是先把文件内容读取到缓冲区,可以减少CPU读取磁盘的次数。Buffering为0时表示不缓冲,为1时表示只缓冲一行数据,为-1时表示使用系统默认缓冲机制,默认为-1。任何大于1的值表示使用给定的的值作为缓冲区大小。一般情况下使用函数默认值即可。
encoding 是用于指定解码或编码文件的编码方式,默认采用utf-8。编码方式主要是指文件中的字符编码。我们经常会碰到这样的情况,当打开一个文件时,内容全部是乱码,这是因为创建文件时采用的编码方式,和打开文件时的编码方式不一样,就会造成字符显示错误,看上去就是乱码。
errors 是一个可选的字符串参数,用于指定如何处理编码和解码错误 - 这不能在二进制模式下使用。
https://docs.python.org/zh-cn/3.7/library/functions.html?highlight=open#open
模式 | 描述 |
---|---|
r | 以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。 |
rb | 以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。 |
r+ | 打开一个文件用于读写。文件指针将会放在文件的开头。 |
rb+ | 以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。 |
w | 打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。 |
wb | 以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。 |
w+ | 打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。 |
wb+ | 以二进制格式打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。 |
a | 打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。 |
ab | 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。 |
a+ | 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。 |
ab+ | 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。 |
5.2 打印输出至文件中
open
(file,‘wt’)
打开并写入文本 'w'
,同 'wt'
5.3 使用其它分隔符或行终止符打印
怎么在使用 print()
函数输出数据时,改变默认的分隔符或者行尾符?
print
(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
将 objects 打印到 file 指定的文本流,以 sep 分隔并在末尾加上 end。 sep, end, file 和 flush 如果存在,它们必须以关键字参数的形式给出。所有非关键字参数都会被转换为字符串,就像是执行了 str()
一样,并会被写入到流,以 sep 且在末尾加上 end。 sep 和 end 都必须为字符串;它们也可以为 None
,这意味着使用默认值。
>>> for i in range(5):
print(i)
0
1
2
3
4
>>> for i in range(5):
print(i,sep=',',end='.')
0.1.2.3.4.
或者也可以用 str.join(),但 str,join() 仅仅适用于字符串,再有其他格式时就需要先执行转换,所以还是在 print()
函数中使用 sep
关键字参数更好。
5.4 读写二进制数据
怎么读取或写入二进制文件,比如图片,声音文件等?
使用模式为 rb
或 wb
的 open()
函数。在读取二进制数据时,所有返回的数据都是字节字符串格式的,而不是文本字符串。 在写入的时候,必须保证参数是以字节形式对外暴露数据的对象(比如字节字符串,字节数组对象等)。
从二进制模式的文件中读取或者写入文本数据,必须要进行解码或者编码操作 ,如下:
>>> with open(gyf,'rb') as f:
data = f.read(16)
text = data.decode('UTF-8')
转换存储在文件中或是从网络连接等其他来源获取的二进制数据为 python值,可以用 struct
二进制I/O还有一个鲜为人知的特性就是数组和C结构体类型能直接被写入,而不需要中间转换为自己对象,示例:
'''数组方法 class array.array(typecode[, initializer])'''
>>> import array
>>> nums = array.array('i',[x for x in range(5)])
>>> with open(gyf,'wb') as f:
f.write(nums)
20
这个适用于任何实现了被称之为”缓冲接口”的对象,这种对象会直接暴露其底层的内存缓冲区给能处理它的操作。 二进制数据的写入就是这类操作之一。
需要注意的是,在读取二进制数据,索引和迭代动作返回的是字节的值而不是字节字符串。
>>> with open(gyf,'rb') as f:
print(f.readline())
b'\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00'
'''迭代返回了字节值'''
>>> with open(gyf,'rb') as f:
for i in f.readline():
print(i,sep=',',end='.')
0.0.0.0.1.0.0.0.2.0.0.0.3.0.0.0.4.0.0.0.
5.5 文件不存在时才能写入
怎么避免在写文件时不小心覆盖一个已存在的文件?
使用 x
模式来代替 w
模式的 open() 函数,如果文件是二进制的,使用 xb
来代替 xt。示例:
'''对已存在的文件写入失败
'''
>>> gyf
'C:\\Users\\Administrator\\Desktop\\gyf.py'
>>> with open(gyf,'xt') as f:
f.write('fall to write')
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
with open(gyf,'xt') as f:
FileExistsError: [Errno 17] File exists: 'C:\\Users\\Administrator\\Desktop\\gyf.py'
'''对不存在的才可写入成功
'''
>>> with open(r'C:\Users\Administrator\Desktop\gyf2.py','xt') as f:
f.write('fall to write')
13
也可以用 os.path.exists()判断文件是否存在,在进行写入,但显然 x 模式的open()更简单。
5.6 字符串的IO操作
怎么实现使用操作类文件对象的程序来操作文本或二进制字符串?
使用 io.StringIO() 和 io.BytesIO() 类来创建类文件对象,io.StringIO() 类用于操作字符串数据,io.BytesIO 类用于操作二进制数据
当你想模拟一个普通的文件的时候 StringIO 和 BytesIO 类是很有用的。 比如,在单元测试中,你可以使用 StringIO 来创建一个包含测试数据的类文件对象, 这个对象可以被传给某个参数为普通文件对象的函数。需要注意的是, StringIO 和 BytesIO 实例并没有正确的整数类型的文件描述符。 因此,它们不能在那些需要使用真实的系统级文件如文件,管道或者是套接字的程序中使用。
https://docs.python.org/zh-cn/3.7/library/io.html?highlight=stringio#io.StringIO
5.7 读写压缩文件
gzip
和 bz2
是 linux 中常见压缩文件格式,怎么读写一个gzip或bz2格式的压缩文件?
gzip
和 bz2
模块可以很容易的处理这些文件。 两个模块都为 open()
函数提供了另外的实现来解决这个问题。
https://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p07_read_write_compressed_datafiles.html
5.8 固定大小记录的文件迭代
对于文本文件,一行一行的读取(默认的迭代行为)更普遍点。对于二进制模式打开的文件,读取固定大小的记录更普遍。怎么读取固定大小的记录?
使用 iter
和 functools.partial()
函数
iter()
函数传递一个可调用对象和一个标记值,会创建一个迭代器。 这个迭代器会一直调用传入的可调用对象直到它返回标记值为止,这时候迭代终止。
functools.partial
用来创建一个每次被调用时从文件中读取固定数目字节的可调用对象。
'''gyf文件的内容
'''
>>> data
b'\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00'
'''固定长度记录的文件迭代
'''
>>> from functools import partial
>>> size = 5
>>> with open(gyf,'rb') as f:
items = iter(partial(f.read,size),b'')
for r in items:
print(r,end=',')
b'\x00\x00\x00\x00\x01',b'\x00\x00\x00\x02\x00',b'\x00\x00\x03\x00\x00',b'\x00\x04\x00\x00\x00',
标记值 b''
就是当到达文件结尾时的返回值。
5.9 读取二进制数据到可变缓冲区中
怎么直接读取二进制数据到一个可变缓冲区中,而不需要做任何的中间复制操作? 或者原地修改数据并将它写回到一个文件中去?
readinto()
5.10 内存映射的二进制文件
怎么内存映射一个二进制文件到一个可变字节数组中? 目的可能是为了随机访问它的内容或者是原地做些修改
https://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p10_memory_mapping_binary_files.html
5.11 文件路径名的操作
对于任何的文件名的操作,你都应该使用 os.path 模块,而不是使用标准字符串操作来构造自己的代码。 特别是为了可移植性考虑的时候更应如此, 因为 os.path 模块知道Unix和Windows系统之间的差异并且能够可靠地处理类似 Data/data.csv 和 Data\data.csv 这样的文件名。 其次,你真的不应该浪费时间去重复造轮子。通常最好是直接使用已经为你准备好的功能。
>>> import os
>>> gyf = r'C:\Users\Administrator\Desktop\gyf.py'
'''获得路径的最后部分'''
>>> os.path.basename(gyf)
'gyf.py'
>>> os.path.basename(os.path.dirname(gyf))
'Desktop'
'''获取目录名'''
>>> os.path.dirname(gyf)
'C:\\Users\\Administrator\\Desktop'
'''连接路径'''
>>> gyf2 = os.path.join(os.path.dirname(gyf),'gyf2')
>>> gyf2
'C:\\Users\\Administrator\\Desktop\\gyf2'
'''展开用户的主目录 '''
>>> gyf = r'~\Desktop\gyf.py'
>>> os.path.expanduser(gyf)
'C:\\Users\\Administrator\\Desktop\\gyf.py'
'''分割文件扩展名'''
>>> os.path.splitext(gyf)
('~\\Desktop\\gyf', '.py')
要注意的是 os.path 还有更多的功能在这里并没有列举出来。 可以查阅官方文档来获取更多与文件测试,符号链接等相关的函数说明。
5.12 测试文件是否存在
'''测试一个文件或目录是否存在'''
>>> os.path.exists(gyf)
True
'''测试文件类型,比如普通文件、目录'''
>>> os.path.isfile(gyf)
True
>>> os.path.isdir(gyf)
False
'''获取元数据,比如文件大小'''
>>> os.path.getsize(gyf)
20
使用 os.path
来进行文件测试是很简单的。 在写这些脚本时,可能唯一需要注意的就是你需要考虑文件权限的问题,特别是在获取元数据时候。
5.13 获取文件夹中的文件列表
>>> path = 'C:\\Users\\Administrator\\Desktop'
>>> import os
>>> os.listdir(path)
#返回结果是桌面上所有文件的列表,包括所有文件,子目录,符号链接等
'''可用列表推导进行过滤'''
>>> [i for i in os.listdir(path) if os.path.isfile(os.path.join(path,i))]
#返回结果是过滤掉不是普通文件的文件列表
'''字符串的 startswith() 和 endswith() 方法对于过滤一个目录的内容也是很有用的'''
>>> [i for i in os.listdir(path) if i.endswith('.pdf')]
['PythonTricksuffetfwesomeythoneatures.pdf', 'Python源码剖析.pdf', '流畅的python.pdf']
'''对于文件名的匹配可以使用 glob 或 fnmatch 模块'''
>>> [i for i in os.listdir(path) if fnmatch(i,'*.py')]
['gyf.py', 'gyf2.py']
得到文件列表后,可以使用 os.path
模块中的函数或者 os.stat()
函数获取其他的元信息,比如文件大小,修改时间等等, 示例:
'''pdf 是得到的文件列表'''
>>> pdf = [i for i in os.listdir(path) if i.endswith('.pdf')]
>>> path
'C:\\Users\\Administrator\\Desktop'
>>> pdf_path = [os.path.join(path,i) for i in pdf]
>>> pdf_path
['C:\\Users\\Administrator\\Desktop\\Python源码剖析.pdf', 'C:\\Users\\Administrator\\Desktop\\流畅的python.pdf']
'''文件信息列表'''
>>> pdf_info = [(i,os.path.getsize(i),os.path.getmtime(i)) for i in pdf_path]
>>> pdf_info
[('C:\\Users\\Administrator\\Desktop\\Python源码剖析.pdf', 30441873, 1574845534.5135152), ('C:\\Users\\Administrator\\Desktop\\流畅的python.pdf', 12237035, 1545278665.7902768)]
最后还有一点要注意的就是,有时候在处理文件名编码问题时候可能会出现一些问题。 通常来讲,函数 os.listdir()
返回的实体列表会根据系统默认的文件名编码来解码。 但是有时候也会碰到一些不能正常解码的文件名。 关于文件名的处理问题,在5.14和5.15小节有更详细的讲解。
5.14 忽略文件名编码
通常来讲,你不需要担心文件名的编码和解码,普通的文件名操作应该就没问题了。 但是,有些操作系统允许用户通过偶然或恶意方式去创建名字不符合默认编码的文件。 这些文件名可能会神秘地中断那些需要处理大量文件的Python程序。
https://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p14_bypassing_filename_encoding.html
5.15 打印不合法的文件名
这一小节主题可能会被大部分读者所忽略。但是如果你在编写依赖文件名和文件系统的关键任务程序时, 就必须得考虑到这个。否则你可能会在某个周末被叫到办公室去调试一些令人费解的错误。
https://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p15_printing_bad_filenames.html
5.16 增加或改变已打开文件的编码
怎么在不关闭一个已打开的文件前提下增加或改变它的Unicode编码?
5.17 将字节写入文本文件
I/O系统以层级结构的形式构建而成,可以通过下面操作一个文本文件的示例来查看这种层次:
>>> gyf = r'C:\Users\Administrator\Desktop\gyf.py'
>>> f = open(gyf,'w')
>>> f
<_io.TextIOWrapper name='C:\\Users\\Administrator\\Desktop\\gyf.py' mode='w' encoding='cp936'>
>>> f.buffer
<_io.BufferedWriter name='C:\\Users\\Administrator\\Desktop\\gyf.py'>
>>> f.buffer.raw
<_io.FileIO name='C:\\Users\\Administrator\\Desktop\\gyf.py' mode='wb' closefd=True>
在这个例子中,io.TextIOWrapper
是一个编码和解码Unicode的文本处理层, io.BufferedWriter
是一个处理二进制数据的带缓冲的I/O层, io.FileIO
是一个表示操作系统底层文件描述符的原始文件。 增加或改变文本编码会涉及增加或改变最上面的 io.TextIOWrapper
层。
文本文件是通过在一个拥有缓冲的二进制模式文件上增加一个Unicode编码/解码层来创建。 buffer
属性指向对应的底层文件。如果你直接访问它的话就会绕过文本编码/解码层。
怎么在文本模式打开的文件中写入原始的字节数据?
https://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p17_write_bytes_to_text_file.html
5.18 将文件描述符包装成文件对象
现在有一个对应于操作系统上一个已打开的I/O通道(比如文件、管道、套接字等)的整型文件描述符, 怎么将它包装成一个更高层的Python文件对象?
一个文件描述符和一个打开的普通文件是不一样的。 文件描述符仅仅是一个由操作系统指定的整数,用来指代某个系统的I/O通道。这是一个文件描述符:
>>> import os
#os.O_WRONLY , os.O_CREAT 常量是 open() 函数 flags 参数的选项,用按位或运算符 | 组合
>>> fd = os.open(gyf,os.O_WRONLY | os.O_CREAT)
可以通过使用 open()
函数来将其包装为一个Python的文件对象,仅需要使用这个整数值的文件描述符作为第一个参数来代替文件名即可:
#变成一个合适的文件
>>> f = open(fd,'wt')
......
5.19 创建临时文件和文件夹
怎么在程序执行时创建一个临时文件或目录,并希望使用完之后可以自动销毁掉?
tempfile模块
--- 生成临时文件和目录
tempfile.
TemporaryFile
(mode='w+b', buffering=None, encoding=None, newline=None, suffix=None, prefix=None, dir=None)
返回一个 file-like object 作为临时存储区域。创建该文件使用了与 mkstemp()
相同的安全规则。它将在关闭后立即销毁(包括垃圾回收机制关闭该对象时)。在 Unix 下,该文件在目录中的条目根本不创建,或者创建文件后立即就被删除了,其他平台不支持此功能。您的代码不应依赖使用此功能创建的临时文件名称,因为它在文件系统中的名称可能是可见的,也可能是不可见的。
生成的对象可以用作上下文管理器(参见 例子)。完成文件对象的上下文或销毁后,临时文件将从文件系统中删除,因此创建的文件可以读取或写入而不用关闭。
mode 参数默认值为 'w+b'
。
因为是使用二进制模式,所以它在所有平台上的行为都保持一致而不用关心所存储的是什么数据。 buffering, encoding 和 newline 的解读方式与 open()
相同。
示例
:
>>> from tempfile import TemporaryFile
>>> with TemporaryFile('w+t') as f:
'''对匿名临时文件写入'''
f.write('tempfile test')
f.write('test two')
'''设置文件读取指针到开头'''
f.seek(0)
'''读取数据'''
data = f.read()
'''该匿名临时文件已自动销毁'''
13
8
0
>>> data
'tempfile testtest two'
创建匿名临时文件用TemporaryFile()
,类似的创建不匿名临时文件和临时文件夹分别用 NamedTemporaryFile()
和 TemporaryDirectory()
函数
https://docs.python.org/zh-cn/3.7/library/tempfile.html?highlight=tempfile#tempfile.TemporaryFile
5.20 与串行端口的数据通信
怎么通过串行端口读写数据,典型场景就是和一些硬件设备打交道(比如一个机器人或传感器)?
可以通过使用Python内置的I/O模块来完成这个任务,但对于串行通信最好的选择是使用第三方包 pySerial包 。
https://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p20_communicating_with_serial_ports.html
5.21 序列化Python对象
怎么将一个Python对象序列化为一个字节流,以便将它保存到一个文件、存储到数据库或者通过网络传输它?
对于序列化最普遍的做法就是使用 pickle
模块。
pickle.
dump
(obj, file, protocol=None, *, fix_imports=True)
系列化。将打包好的对象 obj 写入已打开的 file object file。参数 file 必须有一个 write() 方法,该 write() 方法要能接收字节作为其唯一参数。因此,它可以是一个打开的磁盘文件(用于写入二进制内容),也可以是一个 io.BytesIO
实例,也可以是满足这一接口的其他任何自定义对象。
pickle.
load
(file, *, fix_imports=True, encoding="ASCII", errors="strict")¶
反序列化。从已打开的 file object 文件 中读取打包后的对象,重建其中特定对象的层次结构并返回。Pickle 协议版本是自动检测出来的,所以不需要参数来指定协议。打包对象以外的其他字节将被忽略。
参数 file 必须有两个方法,其中 read() 方法接受一个整数参数,而 readline() 方法不需要参数。 两个方法都应返回字节串。 因此 file 可以是一个打开用于二进制读取的磁盘文件、一个 io.BytesIO
对象,或者任何满足此接口要求的其他自定义对象。
>>> import pickle
>>> data = 'something about data'
>>> gyf = 'C:\\Users\\Administrator\\Desktop\\gyf.py'
'''把Python对象序列化为一个字节流存入文件gyf'''
>>> with open(gyf,'wb') as f:
pickle.dump(data,f)
'''读取数据并把字节流反序列化为Python对象'''
>>> with open(gyf,'rb') as f:
data_get = pickle.load(f)
>>> data_get
'something about data'
pickle.
dumps
(obj, protocol=None, *, fix_imports=True)
将 obj 打包以后的对象作为 bytes
类型直接返回,而不是将其写入到文件。
pickle.
loads
(bytes_object, *, fix_imports=True, encoding="ASCII", errors="strict")
对于打包生成的对象 bytes_object,还原出原对象的结构并返回。
对于大多数应用程序来讲,dump()
和 load()
函数的使用就是你有效使用 pickle
模块所需的全部了。 它可适用于绝大部分Python数据类型和用户自定义类的对象实例。 如果你碰到某个库可以让你在数据库中保存/恢复Python对象或者是通过网络传输对象的话, 那么很有可能这个库的底层就使用了 pickle
模块。
https://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p21_serializing_python_objects.html