一、Python 文件读写操作
Python 对文件的处理,对文件的读写是我们日常中最用的操作了,不管你是分析日志还是要将生成的结果写到文件里,都需要用到读文件的读写操作。读写文件前,我们先必须了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。
读操作
在 Python 下对文件操作非常容易,我们使用它的内建函数 open()
就可以打开一个文件,如果你要打开个文件一般的用法是:
1 |
f = open(filename, mode, [buffer]) |
open 方法可以接收三个参数:文件路径、打开模式和缓冲区大小。Python 3 未缓冲的文本 I/O 被禁用,貌似有个 Bug:issue17404。
mode | 说明 | 注意 |
r | 只读方式打开文件 | 只能打开已存在的文件 |
w | 只写方式打开文件 | 文件不存在会自动创建,文件存在则清空 |
a | 追加方式打开 | 文件不存在会自动创建 |
a+ | 追加和读写方式打开 | |
rb、wb、ab | 二进制方式打开 |
在模式后面使用'+'
表示同时支持写入、输出操作,如 r+(如果以r+这种方式写入那么文件必须提前存在)、w+、a+。在模式后面附加 'b'
表示以二进制方式打开,如 rb+、wb+。这些模式基本覆盖了我们日常的对文件操作,当然除了这些还有一些其他模式,想深入了解的可以去自己学习下,如果是对运维来说这些基本够了。
如果文件打开成功,接下来,调用 read()
方法可以一次读取文件的全部内容(硬伤就是它会一次将读入文件的所有内容,所以读大文件,会把你内存吃满),Python 把内容读到内存,用一个 str 对象表示:
1 |
>>> f.read() |
最后一步是调用 close() 方法关闭文件。文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的:
1 |
>>> f.close() |
由于文件读写时都有可能产生 IOError,一旦出错,后面的 close()
就不会调用。所以,为了保证无论是否出错都能正确地关闭文件,我们可以使用 try ... finally
来实现:
1 2 3 4 5 6 |
try: f = open('/tmp/1.txt', 'r') print(f.read()) finally: if f: f.close() |
但是每次都这么写实在太繁琐,所以,Python 引入了 with 语句来自动帮我们调用 close() 方法:这和前面的 try … finally 是一样的,但是代码更佳简洁,并且不必调用 close() 方法。
1 2 |
with open('/tmp/1.txt', 'r') as f: print(f.read()) |
调用 read()
会一次性读取文件的全部内容,如果文件有 10G,内存就爆了,所以,要保险起见,可以反复调用 read(size) 方法,每次最多读取 size 个字节的内容。
也可以采用文件迭代的方式访问文件,这种方式应该是最常用的读取文件方式了,因为我们可以直接迭代文件对象,例子如下:
1 2 3 |
f = open('/tmp/1.txt','r') for line in f: print(line) |
另外,可以使用 readline() 方法,这个函数在某些场景有估计会用到,它的作用就是每次从文件中读取一行出来,我们看个例子:
1 2 3 4 |
>>> f = open('/tmp/1.txt','r') >>> f.readline() '111111\n' >>> f.close() |
这个是每次读取一行,所以需要用到 while 循环,用 if 来判断文件是否已经结束,如果结束就跳出,否则打印改行。
1 2 3 4 5 6 7 |
f = open('/tmp/1.txt','r') while True: line = f.readline() # line = 'context\n' if not line: break else: print(line.strip()) |
正常读取文件,每一行的内容都是'context\n'
,如果此行为空则为'\n'
。当文件读完之后,最后一行就会返回一个空字符串,而没有\n
换行符。
readlines()
这个方法我用的不多,因为它也有个硬伤就是它返回一个列表,所以当文件足够大的时候,返回的列表就会异常的大,就会非常慢,不过它的好处就是可以快速释放文件资源,我们了解下就可以,对付一般文件还是可以的,大文件就会 OOM,例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> f = open('/tmp/1.txt','r') >>> res = f.readlines() >>> f.close() >>> type(res) <type 'list'> >>> for line in res: ... print(line.strip()) ... 111111 222222 333333 444444 |
file-like object
像 open() 函数返回的这种有个 read() 方法的对象,在 Python 中统称为 file-like Object。除了 file 外,还可以是内存的字节流,网络流,自定义流等等。file-like Object 不要求从特定类继承,只要写个 read() 方法就行。
StringIO 就是在内存中创建的 file-like Object,常用作临时缓冲。
二进制
前面讲的默认都是读取文本文件,并且是 ASCII 编码的文本文件。要读取二进制文件,比如图片、视频等等,用 ‘rb’ 模式打开文件即可:
1 2 3 |
>>> f = open('/tmp/test.jpg', 'rb') >>> f.read() '\xff\xd8\xff\xe1\x00\x18Exif\x00\x00... #十六进制表示的字节; |
字符编码
字符编码要读取非 ASCII 编码的文本文件,就必须以二进制模式打开,再解码。比如 GBK 编码的文件(VIM 编辑器下通过 set fileencoding=gbk 可以创建一个 gbk 编码文件):
1 2 3 4 |
>>> f = open('/tmp/gbk.txt', 'rb') >>> u = f.read().decode('gbk') >>> print(u) 中国 |
如果每次都这么手动转换编码嫌麻烦(写程序怕麻烦是好事,不怕麻烦就会写出又长又难懂又没法维护的代码),Python 还提供了一个 codecs 模块帮我们在读文件时自动转换编码,直接读出 unicode:
1 2 3 4 5 |
>>> import codecs >>> with codecs.open('/tmp/gbk.txt', 'r', 'gbk') as f: ... print(f.read()) ... 中国 |
Python 3 开始,要读取非 UTF-8 编码的文本文件,需要给 open() 函数传入 encoding 参数,使用上更加方便了。例如,读取 GBK 编码的文件:
1 2 3 |
>>> f = open('/tmp/gbk.txt', 'r', encoding='gbk') >>> f.read() '中国' |
遇到有些编码不规范的文件,你可能会遇到 UnicodeDecodeError,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open() 函数还接收一个 errors 参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略:
1 |
>>> f = open('/tmp/gbk.txt', 'r', encoding='gbk', errors='ignore') |
写文件
写文件和读文件是一样的,唯一区别是调用 open() 函数时,传入标识符’ w’ 或者 ‘wb’ 表示写文本文件或写二进制文件:
1 2 3 |
>>> f = open('/tmp/test.txt', 'w') >>> f.write('Hello, world!\n') >>> f.close() |
你可以反复调用 write() 来写入文件,但是务必要调用 close() 来关闭文件。
当调用 write(str) 时,Python 解释器调用系统调用想把把内容写到磁盘,但是 Linux 内核有文件缓存机制,所以缓存到内核的缓存区,当调用 close() 或 flush() 时才会真正的把内容写到文件。忘记调用 close() 或 flush() 的后果是数据可能只写了一部分到磁盘,剩下的丢失了。所以,还是用 with 语句来得保险:
1 2 |
with open('/tmp/test.txt', 'w') as f: f.write('Hello, world!\n') |
二、操作文件常用方法和属性
读相关方法
next():迭代,一行一行地读取数据。
1 2 3 4 5 |
>>> f = open('/tmp/1.txt','r') >>> f.next() '111111\n' >>> f.next() '222222\n' |
readline(n)
读入若干行,n 代表读入的最长字节数。默认读取一行数据字节大小。
readlines()
返回文件中的所有行并生成列表,指针就会转向最后一个字节。
read(size)
一次读多少个字节数据。
seek(offset,whence=0)
用来操作文件移动指针的偏移位置。offset 表示开始的偏移量,也就是代表需要移动偏移的字节数。whence 表示给 offset 参数一个定义,表示要从哪个位置开始偏移;可选值,0 代表从文件开头开始算起,1 代表从当前位置开始算起,2 代表从文件末尾算起。
1 2 3 |
>>> f = open('/tmp/1.txt', 'r+') >>> f.seek(2,0) # 从文件头部偏移2个字节 >>> f.readline(2) # 从文件头部2个字节开始往后读取2个字节 |
tell()
返回当前指针在文件中的位置,单位字节。
1 2 3 4 5 6 7 8 9 10 11 |
def read(): seek = 0 while True: with open('/tmp/1.txt', 'r') as f: f.seek(seek) data = f.readline() if data: seek = f.tell() yield data else: return |
打开文件之后,首先从文件开头读(即 seek=0),第一行有数据,f.tell() 记住当前位置(即第一行最后的换行符),再次打开文件后从上一次 tell 的位置开始读数据,yield data 即输出第一行内容,这是一个生成器。
写相关方法
write(‘new line.’)
往文件中写入数据可以加\n换行,文件权限要为 W。
writelines()
往文件中写字符串序列。
1 2 3 4 5 |
>>> import os >>> f = open('/tmp/test.txt','w+') >>> f1 = os.listdir('/etc') >>> f.writelines(f1) >>> f.flush() |
close()
关闭文件同时会刷新内存数据到磁盘上。
flush()
刷新缓冲区将内存中的数据保存到磁盘上。
属性
closed
查看当前文件是否关闭。
name
查看文件名。
mode
查看当前文件打开方式。
1 2 3 4 5 6 7 |
>>> f = open('/tmp/test.txt','r') >>> f.closed False >>> f.name '/tmp/test.txt' >>> f.mode 'r' |
三、普通读写与二进制读写有何不同?
不同之处有两个地方:
第一,使用 ‘r’ 的时候如果碰到 ‘0x1A’,就会视为文件结束,这就是 EOF。使用 ‘rb’ 则不存在这个问题。即,如果你用二进制写入再用文本读出的话,如果其中存在 ‘0X1A’,就只会读出文件的一部分。使用 ‘rb’ 的时候会一直读到文件末尾。
第二,对于字符串 x=’abc\ndef’,我们可用 len(x) 得到它的长度为7,\n我们称之为换行符,实际上是 ‘0X0A’。当我们用 ‘w’ 即文本方式写的时候,在 Windows 平台上会自动将 ‘0X0A’ 变成两个字符 ‘0X0D’,’0X0A’,即文件长度实际上变成 8。当用 ‘r’ 文本方式读取时,又自动的转换成原来的换行符。如果换成 ‘wb’ 二进制方式来写的话,则会保持一个字符不变,读取时也是原样读取。所以如果用文本方式写入,用二进制方式读取的话,就要考虑这多出的一个字节了。’0X0D’ 又称回车符。Linux 下不会变。因为 Linux 只使用 ‘0X0A’ 来表示换行。