Python对文件的操作

Python的文件操作

什么是IO

在计算机中 I/O 是指 Input/Output,即 Stream (流)的输入和输出,输入和输出是相对于内存来说的。程序运行时数据都驻留在在内存当中,由 CPU 这个超快的计算机核心来执行,涉及到数据交换的地方,通常是磁盘、网络操作就需要 IO 接口。

IO 编程中可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream(输入流)是指数据从外(磁盘、网络)流进内存,Output Stream 是数据从内存流出到外面(磁盘、网络)。

由于 CPU 和内存的速度远远高于外设的速度,所以在 IO 编程中,就存在速度严重不匹配的问题。举个例子来说,比如要把 100M 的数据写入磁盘,CPU 输出 100M 的数据只需要 0.01 秒,可是磁盘要接收这 100M 数据可能需要 10 秒,怎么办呢?有两种办法:

第一种是 CPU 处于等待状态,也就是程序暂停执行后续代码,等到 100M 的数据在 10 秒后写入磁盘,再接着往下执行,这种模式称为同步IO;

第二种是 CPU 不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是后续代码可以立刻接着执行,这种模式称为异步IO;

同步和异步的区别就在于是否等待 IO 执行的结果,举个去肯德基吃汉堡的例子:

同步IO:你说“来个汉堡”,服务员告诉你,对不起,汉堡要现做,需要等5分钟,于是你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO。

异步IO:你说“来个汉堡”,服务员告诉你,汉堡需要等5分钟,你可以先去逛商场,等做好了,我们再通知你,这样你可以立刻去干别的事情(逛商场)

很明显,使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。想想看,你得知道什么时候通知你“汉堡做好了”,而通知你的方法也各不相同。如果是服务员跑过来找到你,这是回调模式,如果服务员发短信通知你,你就得不停地检查手机,这是轮询模式。总之,异步IO的复杂度远远高于同步IO。

操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。

以上内容转自 [廖雪峰的官方网站]

文件读写

读写文件是最常见的IO操作,Python 内置了读写文件的函数,用法和C是兼容的。

在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。

在 Python 中对文件操作的步骤:

  • 打开文件,得到文件对象(文件描述符)赋值给一个变量。

  • 通过文件描述符对文件进行操作。

  • 关闭文件。

文件打开模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
'r'       open for reading (default)
# 以只读模式打开文件(默认模式),并将文件指针指向文件头;如果文件不存在会报错

'w' open for writing, truncating the file first
# 以只写模式打开文件,并将文件指针指向文件头;如果文件存在则将其内容清空,如果文件不存在则创建

'x' create a new file and open it for writing
# 以只写模式新建文件,并将文件指针指向文件头;如果文件存在则抛出 FileExistsError

'a' open for writing, appending to the end of the file if it exists
# 以追加可写模式打开文件,并将文件指针指向文件尾部;如果文件不存在则创建

'b' binary mode
# 二进制模式,需要与上面几种模式搭配使用,如ab,wb, ab, ab+

't' text mode (default)
# 普通文本模式(默认模式)

'+' open a disk file for updating (reading and writing)
# 在原有操作基础上补充其他操作功能

'r+'
# 在 r 的基础上增加了可写功能
# 覆盖当前文件指针所在位置的字符,如原来文件内容是"Hello,World",打开文件后写入"hi"则文件内容会变成"hillo, World"

'w+'
# 在w的基础上增加了可读功能
# 'w+'与 r+ 的不同是,w+在打开文件时就会先将文件内容清空,和 w 基本相同

'a+'
# 在 a 的基础上增加了可读功能
# a+ 与 r+ 的不同是, a+ 不管指针在哪只能写到文件末尾(读指针和写指针不相同)

读取文件

例如现有文件 ‘poem’ 内容如下:

1
2
3
4
5
关关雎鸠,在河之洲。窈窕淑女,君子好逑。
参差荇菜,左右流之。窈窕淑女,寤寐求之。
求之不得,寤寐思服。悠哉悠哉,辗转反侧。
参差荇菜,左右采之。窈窕淑女,琴瑟友之。
参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。

使用内置 open() 函数,传入文件名和标示符,指定文件编码并且以 read 模式打开文件:

1
2
f = open('poem', 'rt', encoding='utf-8')
# 如果文件不存在,open() 函数会抛出一个 IOError 的异常,并且给出错误码和详细的信息告诉你文件不存在

调用 read() 方法一次读取文件全部内容到内存,用一个 str 对象表示,并使用 print() 将内容输出到屏幕

1
print(f.read())

文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的:

1
f.close()

注意: 打开文件时 open() 函数是通过操作系统打开的文件,如果不指定文件编码方式则默认使用操作系统自身文件编码方式打开,并且在内存中是 Unicode 形式。在 windows 系统中,’poem’ 文件是 utf-8 保存的,而 Windows 的默认编码是 gbk 编码,所以直接打开会乱码,需要 f=open('poem',encoding='utf8'),’poem’ 文件如果是 gbk 保存的则直接打开即可。

由于文件读写时都有可能产生 IOError ,一旦出错,后面的 f.close() 就不会调用。所以为了保证无论是否出错都能正确地关闭文件,我们可以使用 try ... finally 来实现:

1
2
3
4
5
6
try: # 执行代码,可能抛出异常
f = open('poem', 'r')
print(f.read())
finally: # 不管有无异常都执行
if f:
f.close()

但是每次都这么写实在太繁琐,所以 Python 引入了 with 语句来自动帮我们调用 close() 方法:

1
2
with open('poem', 'r') as f:
print(f.read())

这和前面的 try ... finally 是一样的,但是代码更加简洁并且不必调用 f.close() 方法 ,而且无论在这段代码的任何地方,如果发生异常,此时文件仍会被关闭。

读取文件常用方法

方法 描述
read() 一次读取文件所有内容,返回一个 str
read(size) 每次最多读取指定长度的内容,返回一个str;Python3 中 size 指定的是字符(非字节)长度
readlines() 一次读取文件所有内容,按行返回一个list
readline() 每次只读取一行内容
seek(n) 将文件指针移动到指定字节的位置
tell() 获取当前文件指针所在字节位置

我们已经知道,对文件的读取操作需要将文件中的数据加载到内存 ,调用 read() 会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以为了保险起见,可以反复调用 read(size) 方法,每次最多读取 size 个字符的内容。另外调用 readline() 可以每次读取一行内容,调用 readlines() 一次读取所有内容并按行返回 list。因此要根据需要决定怎么调用。

如果文件很小,read() 一次性读取最方便;如果不能确定文件大小,反复调用 read(size) 比较保险;如果是配置文件,调用 readlines() 最方便:

1
2
3
4
with open('/etc/resolv.conf', 'rt', encoding='utf-8') as fr:
for line in fr.readlines(): # 一次读取所有内容并返回列表(list)
if line.startswith('nameserver'):
print(line.strip())

如果文件比较大,则可以使用迭代器的方法,它在你每次你需要下一个值的时候给你返回,没调用的时候就处于休眠状态等待下一次调用

1
2
3
4
with open('/etc/resolv.conf', 'rt', encoding='utf-8') as fr:
for line in fr: # for 内部将文件对象 fr 做成一个迭代器,用一行取一行
if line.startswith('nameserver'):
print(line.strip())

需要注意的是,在 Python3 中使用 read() 时传入的参数是字符个数而不是字节,而 tell() 返回的是字节的位置,在 utf-8 中一个中文字符占三个字节:

1
2
3
4
with open('poem','r',encoding='utf-8') as f:
print(f.tell())
print(f.read(5))
print(f.tell())

返回结果:

1
2
3
0
关关雎鸠,
15

写入文件

写文件和读文件都需要调用 open() ,当以 'w' 模式写入文件时,如果文件已存在,会直接覆盖(相当于删掉后新写入一个文件)。以 'a' 模式写入文件时,会以追加(append)模式追加到文件末尾

1
2
3
f = open('test.txt', 'w')
f.write('Hello world')
f.close()

可以反复调用 write() 来写入文件,但是务必要调用 f.close() 来关闭文件。当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用 close() 方法时,操作系统才保证把没有写入的数据全部写入磁盘。忘记调用 close() 的后果是数据可能只写了一部分到磁盘,剩下的丢失了。所以还是用 with 语句来得保险:

1
2
with open('test.txt','w') as f:
f.write('Hello World')

其他方法

使用 flush() 可以刷新缓冲区数据,立刻将缓冲区的数据写入硬盘中的文件:

1
2
3
4
5
6
7
import sys,time
for i in range(30):
sys.stdout.write('*') # stdout 标准输出,也是一个文件
sys.stdout.flush()
# 实时将内存中的数据写入标准输出文件
# 如果不写此行则数据会保留着内存缓冲区,直到sleep完成并一次性显示所有符号
time.sleep(0.1)

内置的 print() 也有 flush 参数

1
2
3
4
import sys,time
for i in range(30):
print('*',end='',flush=True)
time.sleep(0.1)

使用 truncate() 可以裁切数据,如果不加任何参数,则裁切(删除)掉开头到结尾的内容。如果指定一个数字 N,则将第 N 个字符后的内容全部截掉,保留前 N 个字符:

1
2
3
with open('poem2','a',encoding='utf-8') as f:
# 文件打开模式应该是 append 而不是 write ,因为 write 会先清除所有内容
f.truncate(5)

isatty() 可以判断文件是否是终端文件

next() 将会返回文件下一行,这个方法也是 file 对象实例可以被当做迭代器使用的原因。

fileno() 返回一个整型的文件描述符,可以用于一些底层 IO 操作上(如 os 模块的 read 方法)。

操作练习

读取文件 ‘poem’ ,显示到屏幕同时为第四行添加注释 # 琴瑟友之:弹琴鼓瑟来亲近她 ,如果涉及到字符串拼接尽量使用 join() 方法代替 +

菜鸟版:一次性读入所有内容到内存然后循环打印,使用变量定位行号。

1
2
3
4
5
6
7
8
f = open('poem', mode='r', encoding='utf8')
line_number = 0
for i in f.readlines():
line_number += 1
if line_number == 4:
i = ''.join([i.strip(), '# 琴瑟友之:弹琴鼓瑟来亲近她'])
print(i.strip())
f.close()

菜鸟加强版:读入所有内容赋值给变量后立即关闭文件,对变量的元素循环和修改,使用索引定位

1
2
3
4
5
6
7
f = open('poem',mode='r',encoding='utf8')
data = f.readlines()
f.close()

data[3] = ''.join([data[3].strip(),'# 琴瑟友之:弹琴鼓瑟来亲近她'])
for line in data:
print(line.strip())

中级版:一次性读入所有内容,使用enumerate()try ... finally

1
2
3
4
5
6
7
8
9
try:
f = open('poem', 'r', encoding='utf-8')
for line_number, line in enumerate(f.readlines(), 1):
if line_number == 4:
line = ''.join([line.strip(), '# 琴瑟友之:弹琴鼓瑟来亲近她'])
print(line.strip())
finally:
if f:
f.close()

最佳实践:前三种方法中 read()readlines() 在文件比较大时都会消耗大量内存空间,容易造成内存溢出,因此结合 with 语句并使用迭代器用一行取一行,变量定位行号更合适:

1
2
3
4
5
6
7
line_number = 0
with open('poem','r',encoding='utf-8') as f:
for line in f:
line_number += 1
if line_number == 4:
line = ''.join([line.strip(), '# 琴瑟友之:弹琴鼓瑟来亲近她'])
print(line.strip())

在 Python 中由于内存的机制无法像 word 那样实时修改并保存文件,因此只能先读取源文件数据到内存空间中进行修改,然后写入到另一个文件,再把新文件替换掉源文件。

例如,要在文件 ‘poem’ 的第四行之后添加注释 # 琴瑟友之:弹琴鼓瑟来亲近她

1
2
3
4
5
6
7
8
9
10
11
12
13
# -*- coding:utf-8 -*-
f_read = open('poem','r',encoding='utf-8')
f_write = open('poem.bak','w',encoding='utf-8')

line_number = 0
for line in f_read:
line_number += 1
if line_number == 4:
line = line.replace('\n','# 琴瑟友之:弹琴鼓瑟来亲近她\n')
f_write.write(line)

f_read.close()
f_write.close()

使用 with 语句,可以同时管理多个文件对象

1
2
3
4
5
6
7
8
9
# -*- coding:utf-8 -*-
line_number = 0
with open('poem', 'r', encoding='utf-8') as fr, \
open('poem.bak', 'w', encoding='utf-8') as fw:
for line in fr:
line_number += 1
if line_number == 4:
line = line.replace('\n', '# 琴瑟友之:弹琴鼓瑟来亲近她\n')
fw.write(line)
有钱任性,请我吃包辣条
0%