第五章:文件与 IO

5.1 读写文本数据

文本文件中的 回车 在不同操作系统中所用的字符表示有所不同。

  • Windows : \r\n
  • Linux/Unix : \n
  • Mac OS : \r

rt模式下,python在读取文本时会自动把\r\n转换成\n.

1
2
3
with open('somefile.txt', 'rt') as f:
data = f.read()
print(data)

其实python默认就会将这些文件的换行符全部统一成\n
如果不想使用默认,可以使用open函数的newline参数

1
2
with open('somefile.txt', 'rt', newline='') as f:
pass

print语句有一个file参数,用于重定向数据流

1
print(line1, file=f)
  • 当读取一个未知编码的文本时使用 latin-1 编码永远不会产生解码错误。
  • 使用latin-1 编码读取一个文件的时候也许不能产生完全正确的文本解码数据,但是它也能从中提取出足够多的有用数据。

5.2 打印输出至文件中

print() 函数的file 关键字参数

1
2
with open('d:/work/test.txt', 'wt') as f:
print('Hello World!', file=f)

5.3 使用其他分隔符或行终止符打印

print() 函数中使用 sep 和 end 关键字参数

5.4 读写字节数据

用模式为 rb 或 wb 的 open() 函数

5.5 文件不存在才能写入

你想像一个文件中写入数据,但是前提必须是这个文件在文件系统上不存在。也就是不允许覆盖已存在的文件内容。

open() 函数中使用 x 模式来代替 w 模式

1
2
3
4
5
with open('test.txt', 'xt') as f:
print('Hello World!', file=f)

# 如果存在,就会报FileExistsError
# FileExistsError: [Errno 17] File exists: 'test.txt'

当然也可以自己实现:

1
2
3
4
5
6
7
import os

if not os.path.exists('somefile'):
with open('somefile','w') as f:
f.write('hello\n')
else:
print('file already exists')

如果文件是二进制的,使用 xb

5.6 字符串的 I/O 操作

操作类文件对象的程序来操作文本或二进制字符串。

使用 io.StringIO() 和 io.BytesIO() 类来创建类文件对象操作字符串数据。

  • StringIO的行为与file对象非常像,但它不是磁盘上文件,而是一个内存里的“文件”,我们可以像操作磁盘文件那样来操作StringIO。
  • StringIO模块,主要用于在内存缓冲区中读写数据。
  • 数据读写不一定是文件,也可以在内存中进行。
  • StringIO 顾名思义就是在内存中以 io 流的方式读写 str
1
2
3
4
5
6
7
from io import StringIO
f = StringIO()
f.write('hello') # 返回 5,也即写入的字符数目
f.write(' ')
f.write('world!')

print(f.getvalue() ) # hello world!
  1. s=io.StringIO([buf])

    • 此实例类似于open方法,不同的是==它并不会在硬盘中生成文件,而只寄存在缓冲区==;

    • 可选参数buf是一个str或unicode类型。它将会与其他后续写入的数据存放在一起

      (注意,若要在初始化数据之后继续写入数据,则在写入数据之前,应先将读写位置移动到结尾,然后再写入,否则,初始化数据会被覆盖掉,因为读写位置默认是0)。

  2. s.read([n])

    • 参数n限定读取长度,int类型;
    • 缺省状态为从当前读写位置读取对象s中存储的所有数据。
    • 读取结束后,读写位置被移动
  3. s.readline([length])

    • 参数length限定读取的结束位置,int类型,
    • 缺省状态为None:从当前读写位置读取至下一个以“\n”为结束符的当前行。
    • 读写位置被移动。
  4. s.readlines([sizehint])

    • 参数sizehint为int类型,
    • 缺省状态为读取所有行并作为列表返回,如果不使用缺省值从当前读写位置读取至下一个以“\n”为结束符的当前行。
    • 读写位置被移动。
  5. s.write(s)

    • 从读写位置将参数s写入给对象s。
    • 参数s为str或unicode类型。读写位置被移动
  6. s.writelines(list)

    • 从读写位置将list写入给对象s。参数list为一个列表,列表的成员为str或unicode类型。
    • 读写位置被移动。
  7. s.getvalue()
    此函数没有参数,无论读写位置在哪里,都能够返回对象s中的所有数据。

  8. s.truncate([size])

    • 如果有size参数 :
      无论读写位置在哪里,都从起始位置开始,裁剪size字节的数据。
    • 不带size参数
      将当前读写位置之前的数据,裁剪下来。
  9. s.tell()
    返回当前读写位置。

  10. s.seek(pos[,mode])

    • 移动当前读写位置至pos处,
    • 可选参数mode为0时将读写位置移动至pos处,
      为1时将读写位置从当前位置起向前或向后移动|pos|个长度,
      为2时将读写位置置于末尾处再向前或向后移动|pos|个长度;
      mode的默认值为0。
  11. s.close()
    释放缓冲区,执行此函数后,数据将被释放,也不可再进行操作。

  12. s.isatty()
    此函数总是返回0。

  13. s.flush()
    刷新内部缓冲区。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
from io import StringIO

s = StringIO('python')
s.seek(0,2) #将读写位置移动到结尾

s.write("aaaa")
lines = ['xxxxx', 'bbbbbbb']
s.writelines(lines)
s.write("ttttttttt")

print(s.getvalue())
# pythonaaaaxxxxxbbbbbbbttttttttt

io.StringIO 只能用于文本。如果你要操作二进制数据,要使用 io.BytesIO 类

  • 当你想模拟一个普通的文件的时候 StringIO 和 BytesIO 类是很有用的。
  • 比如,在单元测试中,你可以使用 StringIO 来创建一个包含测试数据的类文件对象,这个对象可以被传给某个参数为普通文件对象的函数。

5.7 读写压缩文件

读写一个 gzip 或 bz2 格式的压缩文件。

使用gzip 和 bz2 模块

1
2
3
4
5
6
7
8
9
10
import gzip
with gzip.open('somfile.gz','rt') as f:
text = f.read()
f.write('xxxxxx')


import bz2
with bz2.open('somfile.bz2','rt') as f:
text = f.read()
f.write('xxxxxx')

gzip.open() 和 bz2.open() 接受跟内置的open() 函数一样的参数,包括 encoding,errors,newline 等等。

当写入压缩数据时,可以使用 compresslevel 这个可选的关键字参数来指定一个压缩级别。

1
2
with gzip.open('somefile.gz', 'wt', compresslevel=5) as f:
f.write(text)

默认的等级是 9,也是最高的压缩等级。等级越低性能越好,但是数据压缩程度也越低。

5.8 固定大小记录的文件迭代

在一个固定长度记录或者数据块的集合上迭代,而不是在一个文件中一行一行的迭代。

使用 iter 和 functools.partial() 函数:

1
2
3
4
5
6
7
8
from functools import partial

RECORD_SIZE = 32

with open('somefile.data','rb') as f:
records = iter(partial(f.read,RECORD_SIZE),b'')
for r in records:
pass

records 对象是一个可迭代对象,它会不断的产生固定大小的数据块,直到文件末尾。

注意:
如果总记录大小不是块大小的整数倍的话,最后一个返回元素的字节数会比期望值少。

iter() 函数有一个鲜为人知的特性就是,如果你给它传递一个可调用对象和一个
标记值,它会创建一个迭代器。这个迭代器会一直调用传入的可调用对象直到它返回标记值为止,这时候迭代终止。

固定大小记录的文件迭代二进制模式更常见

5.9 读取二进制数据到可变缓冲区中

为了读取数据到一个可变数组中,使用文件对象的 readinto() 方法。

file.readinto(buf,size) : 读取size个字节到文件缓冲器中

  • 和普通 read() 方法不同的是,==readinto() 填充已存在的缓冲区而不是为新对象重新分配内存再返回它们。==
  • 因此,你可以使用它来避免大量的内存分配操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

buf = read_into_buffer('sample.bin')
print(buf) # bytearray(b'Hello World')

buf[0:5] = b'XXXXX'
pirnt(buf) # bytearray(b'XXXXX World')

另外有一个有趣特性就是 memoryview ,它可以通过零复制的方式对已存在的缓冲区执行切片操作,甚至还能修改它的内容。

1
2
3
4
5
6
7
8
print(buf)  # bytearray(b'Hello World')
buf[0:5] = b'XXXXX'
pirnt(buf) # bytearray(b'XXXXX World')

m1 = memoryview(buf)
m2 = m1[-5:]
print(m2) # <memory at 0x100681390>
print(buf) # bytearray(b'Hello World')

5.10 内存映射的二进制文件

内存映射一个二进制文件到一个可变字节数组中,目的可能是为了随机访问它的内容或者是原地做些修改。

使用 mmap 模块来内存映射文件。

mmap : memory-map打开一个文件并以一种便捷方式内存映射这个文件。

  • mmap是一种虚拟内存映射文件的方法,即可以将一个文件或者其它对象映射到进程的地址空间,实现==文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。==

  • 普通文件被映射到虚拟地址空间后,程序可以像操作内存一样操作文件,可以提高访问效率,适合处理超大文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import mmap

    # 预先准备一个hello.txt文件
    with open("hello.txt", "wb") as f:
    f.write("Hello Python!\n")


    with open("hello.txt", "r+b") as f:
    # 内存映射文件,0表示整个文件
    mm = mmap.mmap(f.fileno(), 0)
    # 通过标准文件方法读取内容
    print(mm.readline()) # Hello Python!
    # 通过切片读取内容
    print(mm[:5]) # Hello
    # 使用切片更新内容
    # 注意,新内容的大小必须相同
    mm[6:] = " world!\n"
    # 使用标准文件方法重新读取
    mm.seek(0)
    print(mm.readline()) # Hello world!
    # 关闭
    mm.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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('data', 'wb') as f:
f.seek(size-1)
f.write(b'\x00')


m = memory_map('data')

print(len(m)) # 1000000
print(m[0:10]) # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
  • 需要强调的一点是,内存映射一个文件并不会导致整个文件被读取到内存中
  • 也就是说,文件并没有被复制到内存缓存或数组中。相反,操作系统仅仅为文件内容保留了一段虚拟内存。
  • 当你访问文件的不同区域时,这些区域的内容才根据需要被读取并映射到内存区域中。而那些从没被访问到的部分还是留在磁盘上。所有这些过程是透明的,在幕后完成!

5.11 文件路径名的操作

使用路径名来获取文件名,目录名,绝对路径等等。

使用 os.path 模块中的函数来操作路径名

1
2
3
4
5
6
7
8
9
10
11
12
13
import os

path = '/Users/beazley/Data/data.csv'

print(os.path.basename(path)) # data.csv
print(os.path.dirname(path)) # /Users/beazley/Data

print(os.path.join('tmp','data','A.csv')) # tmp\data\A.csv
print(os.path.join('tmp','data','.csv')) # tmp\data\.csv

path = '~/Data/data.csv'
print(os.path.expanduser(path)) # C:\Users\lenovo/Data/data.csv
print(os.path.splitext(path)) # ('~/Data/data', '.csv')

os.path 模块知道 Unix 和 Windows 系统之间的差异并且能够可靠地处理类似 Data/data.csv 和Data\data.csv 这样的文件名。

5.12 测试文件是否存在

  • os.path.exists()
  • os.path.isdir()
  • os.path.isfile()
  • os.path.isllink()
  • os.path.realpath() : 对于link,找出链接的真正地址

注意:
os.path.isfile(),os.path.isdir(),os.path.isllink(),是能否找到这个文件/目录/链接.不是判断传入的参数是不是文件/目录/链接

1
2
3
4
import os

print(os.path.exists('hello.txt'))# True
print(os.path.isdir(r'D:\note\Python\没有这个目录'))# False

获取文件/目录的一些数据

  • os.path.getsize : 获取大小
  • os.path.getmtime : 获取修改时间
1
2
3
4
5
6
import os.path

# 获取文件大小
print(os.path.getsize('training1.py')) # 1429
# 获取目录大小
print(os.path.getsize(r'D:\note\Python')) # 4096

5.13 获取文件夹中的文件列表

获取文件系统中某个目录下的所有文件/目录列表。

os.listdir() 函数

1
2
3
4
import os

print(os.listdir(r'D:\note\Python\书籍__python-cookbook\trainingfile'))
# ['data', 'hello.txt', 'setup.py', 'somefile.txt', 'test.txt', 'tests', 'training1.py', 'training2.py', 'training3.py', 'training4.py', 'training5.py', 'training6.py']

注意:
如果trainingfile目录下的,test目录下还有文件.那么这些文件并不能出现在os.listdir里面
当然,我们可以自己写一个:

1
2
3
4
5
6
7
8
9
10
11
12
import os

def listdirs(path):
for f_or_d in os.listdir(path):
if os.path.isdir(f_or_d):
yield from listdirs(f_or_d)
else:
yield f_or_d

x = list(listdirs(r'D:\note\Python\书籍__python-cookbook\trainingfile'))
print(x)
# ['data', 'hello.txt', 'setup.py', 'somefile.txt', 'test.txt', 'test_func.py', '__pycache__', 'training1.py', 'training2.py', 'training3.py', 'training4.py', 'training5.py', 'training6.py']
1
2
3
4
5
import os
# 只获取文件
names = [name for name in os.listdir('somedir') if os.path.isfile(os.path.join('somedir', name))]
# 只获取目录
dirnames = [name for name in os.listdir('somedir') if os.path.isdir(os.path.join('somedir', name))]

也可以使用字符串的 startswith() 和 endswith() 方法过滤目录内容:

1
2
import os
pyfiles = [name for name in os.listdir('somedir') if name.endswith('.py')]

使用os.stat() 函数来收集文件数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os

pyfiles = [name for name in os.listdir(r'D:\note\Python\书籍__python-cookbook\trainingfile') if name.endswith('.py')]

file_metadata = [(name,os.stat(name)) for name in pyfiles]

for name, meta in file_metadata:
print(name, meta.st_size, meta.st_mtime)
# training1.py 1429 1564105519.7120438
# training2.py 433 1564241227.0905564
# training3.py 278 1564241650.4067366
# training4.py 120 1564240604.8063886
# training5.py 159 1564240853.097712
# training6.py 256 1564240927.5994468

5.14 忽略文件名编码

sys.getfilesystemencoding() 返回的文本编码来编码或解码。

1
2
import sys
print(sys.getfilesystemencoding()) # utf-8

5.15 打印不合法的文件名

使用下面的方法可以避免这样的错误:

1
2
3
4
5
6
7
def bad_filename(filename):
return repr(filename)[1:-1]

try:
print(filename)
except UnicodeEncodeError:
print(bad_filename(filename))

5.16 增加或改变已打开文件的编码

在不关闭一个已打开的文件前提下增加或改变它的 Unicode 编码。

给一个以二进制模式打开的文件添加 Unicode 编码/解码方式,可以使用io.TextIOWrapper() 对象包装它。

1
2
3
f = open('hello.txt','w')
print(f)
# <_io.TextIOWrapper name='hello.txt' mode='w' encoding='cp936'>
  • io.TextIOWrapper 是一个编码和解码 Unicode 的文本处理层,
  • io.BufferedWriter 是一个处理二进制数据的带缓冲的 I/O 层,
  • io.FileIO 是一个表示操作系统底层文件描述符的原始文件。
  • 增加或改变文本编码会涉及增加或改变最上面的io.TextIOWrapper 层。
1
2
3
4
5
6
7
8
import io
import urllib.request

u = urllib.request.urlopen('http://www.python.org')
f = io.TextIOWrapper(u,encoding='utf-8')

text = f.read()
print(text)

如果你想修改一个已经打开的文本模式的文件的编码方式,可以先使用 detach()方法移除掉已存在的文本编码层,并使用新的编码方式代替。

1
2
3
4
5
6
7
import sys
import io

print(sys.stdout.encoding) # cp936
sys.stdout = io.TextIOWrapper(sys.stdout.detach(),encoding='latin-1')
print(sys.stdout.encoding) # latin-1
sys.stdout = io.TextIOWrapper(sys.stdout.detach(),encoding='latin-1')

5.17 将字节写入文本文件

在文本模式打开的文件中写入原始的字节数据。

将字节数据直接写入文件的缓冲区

1
2
import sys
sys.stdout.buffer.write(b'Hello\n')

类似的,能够通过读取文本文件的 buffer 属性来读取二进制数据

5.18 将文件描述符包装成文件对象

你有一个对应于操作系统上一个已打开的 I/O 通道 (比如文件、管道、套接字等)的整型文件描述符,你想将它包装成一个更高层的 Python 文件对象。

(看不懂,略)

5.19 创建临时文件和文件夹

创建一个临时文件或目录,并希望使用完之后可以自动销毁掉

使用tempfile模块

tempfile.TemporaryFile :

  • 创建一个匿名的临时文件
  • TemporaryFile() 支持跟内置的 open() 函数一样的参数
1
2
3
4
5
6
7
from tempfile import TemporaryFile

with TemporaryFile('w+t') as f:
f.write('AAAAAA')

f.seek(0)
data = f.read()

如果要创建一个具名的临时文件,使用NamedTemporaryFile()

1
2
3
4
5
from tempfile import NamedTemporaryFile

with NamedTemporaryFile('w+t') as f:
print('filename is',f.name)
# filename is C:\Users\lenovo\AppData\Local\Temp\tmpx0ofhhmn

临时文件用完即删,但是也可以不删除,使用delete参数

1
2
with NamedTemporaryFile('w+t', delete=False) as f:
print('filename is:', f.name)

同理还有临时目录:

1
2
3
from tempfile import TemporaryDirectory
with TemporaryDirectory() as dirname:
print('dirname is:', dirname)

5.20 与串行端口的数据通信

对于串行通信最好的选择是使用 pySerial 包 。

1
2
3
4
5
6
7
8
9
import serial
ser = serial.Serial('/dev/tty.usbmodem641', # Device name varies
baudrate=9600,
bytesize=8,
parity='N',
stopbits=1)

ser.write(b'G1 X50 Y50\r\n')
resp = ser.readline()

5.21 序列化 Python 对象

使用 pickle 模块

1
2
3
4
5
6
7
8
9
10
11
import pickle

data = 'there is some python object'
f = open('somefile','wb')
pickle.dumps(data,f)

# Restore from a file
f = open('somefile', 'rb')
data = pickle.load(f)
# Restore from a string
data = pickle.loads(s)
1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle

f = open('somefile','wb')

pickle.dumps([1,2,3],f)
pickle.dumps('hello',f)
pickle.dumps({'name':'hyl'},f)

f.close()

print(pickle.load(f)) # [1,2,3]
print(pickle.load(f)) # hello
print(pickle.load(f)) # {'name':'hyl'}

**千万不要对不信任的数据使用 pickle.load()**。
pickle 在加载时有一个副作用就是它会自动加载相应模块并构造实例对象。
但是某个坏人如果知道 pickle 的工作原理,
他就可以创建一个恶意的数据导致 Python 执行随意指定的系统命令。
因此,一定要保证 pickle 只在相互之间可以认证对方的解析器的内部使用。

  • 有些类型的对象是不能被序列化的。

  • 这些通常是那些依赖外部系统状态的对象,比如打开的文件,网络连接,线程,进程,栈帧等等。

  • 用户自定义类可以通过提供__getstate__() __setstate__() 方法来绕过这些限制。

  • 如果定义了这两个方法,pickle.dump() 就会调用 __getstate__() 获取序列化的对象。类似的,__setstate__()在反序列化时被调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # countdown.py
    import time
    import threading

    class Countdown:
    def __init__(self, n):
    self.n = n
    self.thr = threading.Thread(target=self.run)
    self.thr.daemon = True
    # 实例化就启动线程
    self.thr.start()

    def run(self):
    while self.n > 0:
    print('T-minus', self.n)
    self.n -= 1
    time.sleep(5)
    # 序列化调用
    def __getstate__(self):
    return self.n
    # 反序列化调用
    def __setstate__(self, n):
    self.__init__(n)
  • 就像前面说的:
    ==pickle.load()会自动加载相应模块并构造实例对象==,对于Countdown类来说,创建实例对象就触发了self.thr.start(),也就启动了线程

  • 注意这里__getstate__存储的是self.n,而self.n是线程结束的关键.
    也就是说,存储self.n能存储线程的状态,使用pickle.load()之后能继续执行线程