16 - 文件操作-StringIO-BytesIO
1 文件操作
读写文件是最常见的IO操作(一般说IO操作,指的是文件IO,如果是网络,一般都会直接说网络IO),在磁盘上读写文件的功能都是由操作系统提供的,操作系统不允许普通的程序直接操作磁盘(大部分程序都需要间接的通过操作系统来完成对硬件的操作),所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。在操作系统中,文件常用的操作有:
功能 | 介绍 |
---|---|
open | 打开 |
read | 读取 |
write | 写 |
close | 关闭 |
readline | 行读取 |
readlines | 多行读取 |
seek | 文件指针操作 |
tell | 指针操作 |
1.1 open函数介绍
在python中,我们使用open函数来打开一个文件,然后返回一个文件对象(流对象)和文件描述符。打开文件失败,则返回异常。
第三方模块codecs包中提供的open函数,具备内置函数open的所有功能,并且在内部它对目标文件进行了编码的与判断处理,当你不知道目标文件的编码类型时,使用codecs.open来说更可靠。
1.2 打开操作
想要读取文件,那么就需要先行打开文件。下面来看一下open函数的用法
open(['file', "mode='r'", 'buffering=-1', 'encoding=None', 'errors=None', 'newline=None', 'closefd=True', 'opener=None'],)
file
:要打开的文件名(默认为当前路径,否则要指定绝对路径)。mode
:表示打开文件的格式。buffering
: 设置缓冲区的大小(二进制模式和文本模式不同)encoding
:打开文件的编码格式(文本模式)errors
: 转码错误时是否提示异常newline
: 文本模式中对新行的处理方式closefd
: 删除描述符时,文件对象是否释放
Python 2.x中包含file和open两个操作文件的函数,Python 3.x 中只有open,操作方法相同
f = file('/etc/passwd','r')
1.2.1 mode模式
文件打开的方式有如下几种:
描述字符 | 意义 | 备注 |
---|---|---|
r | 只读打开 | 文件的默认打开方式,文件不存在会报异常 |
w | 只写打开 | 文件不存在会创建,存在的话,会覆盖源文件(非追加) |
x | 创建并只写一个新文件 | 文件已存在则报错 |
a | 只追加写模式 | 在文件末尾追加,文件不存在,新建并追加 |
b | 二进制模式 | 字节流 ,将文件就按照字节理解,与字符编码无关。二进制模式操作时,字节操作使用bytes类型 |
t | 文本模式 | 字符流 ,将文件的字节按照某种字符编码理解,按照字符操作,缺省模式 |
+ | 读写打开一个文件 | 给原来只读、只写方式打开提供缺失的读或者写能力 |
默认使用文本只读模式打开,特殊文件需要用文本模式传输的话,建议使用b
+符
,为r、w、a、x提供缺失的读或者写的功能,但是,获取文件对象依旧按照r、w、a、x自己的特征。但+不能单独使用,可以认为它是为前面的模式字符做的增强功能。
描述字符 | 意义 |
---|---|
r+ | 读模式附加写功能 |
w+ | 写模式附加读功能 |
a+ | 追加写模式附加读功能 |
x+ | 打开一个新文件可读可写 |
以上为文本模式,二进制模式时只需要添加b即可.即r+b,或者rb+都可
In [2]: f = open('hello.txt','r')
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
<ipython-input-2-27a5c78b3969> in <module>
----> 1 f = open('hello.txt','r')
FileNotFoundError: [Errno 2] No such file or directory: 'hello.txt'
In [3]: f = open('123.txt')
In [4]: f.read()
Out[4]: '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00abc'
In [5]: f.close()
In [6]: f = open('123.txt','w')
In [7]: f.read()
---------------------------------------------------------------------------
UnsupportedOperation Traceback (most recent call last)
<ipython-input-7-571e9fb02258> in <module>
----> 1 f.read()
UnsupportedOperation: not readable
In [8]:
注意:
- 我们把这个 f 成为文件句柄(用来标识一个文件的内存对象),包含文件名,字符集,大小,硬盘上的起始位置等等。
- f 在这里就变成了一个文件迭代器,我们可以通过使用for line in f ,去循环的读取文件的每一行内容
# for 循环遍历文件,打印文件的每一行
In [11]: f = open('123.txt')
In [12]: for i in f:
...: print(i,end='')
...:
hello
world
my
name
is
daxin
thanks
In [13]:
注意:这里for line in fd,其实可以从fd.readlines()中读取,但是如果文件很大,那么就会一次性读取到内存中,非常占内存,而这里fd存储的是对象,只有我们读取一行,它才会把这行读取到内存中,建议使用这种方法。
1.2.2 文件指针
为什么文件只能读一次,第二次读就是空的了?是否可以重复读取呢?在对文件操作中,会存在一个指针用于指定当前光标的位置
,当我们读完一次后, 文件指针就指向了文件的末尾了,那么如果想要重复读取,那么只需要对指针的位置进行调整即可。
- mode = r , 指针起始在0
- mode = a , 指针其实在EOF(end of file:文件末尾)
f.tell() # 显示当前文件指针的位置
f.seek(offset, whence=0, /) # 移动文件指针的位置
- offset:偏移多少字节
- whence:从哪里开始
在文本模式下:
- whence
0
:缺省值,表示从头开始,offset只能正整数 - whence
1
:表示从当前位置开始,offset只能设置为0 - whence
2
:表示从EOF位置开始,offset只能设置为0
文本模式支持从开头向后偏移的方式。whence为1时,只支持偏移0(
seek(0,0)
),相当于原地不同,没什么卵用。whence为2时,表示从EOF开始,但也只支持偏移0(seek(0,2)
),相当于将文件指针移动到EOF。
二进制模式下:
- whence
0
: 缺省值,表示从头开始,offset只能正整数 - whence
1
: 表示从当前位置,offset可正可负 - whence
2
: 表示从EOF开始,offset可正可负
二进制模式支持任意起点的偏移,从头、从尾部、从中间位置开始,向后seek可以超界,但是向前seek的时候,不能超界,否则抛异常
1.2.3 缓冲区
open函数的第三个参数为buffering,叫作缓冲区,缓冲区是内存中的一小部分空间,用于存放准备写入磁盘的数据,操作系统在进行磁盘写入的时候,由于磁盘速度要远远慢于CPU以及内存,如果每次写入数据时,都要直接写到磁盘上的话,那么效率会非常的低,所以在操作系统层面会对写入操作进行优化,比如缓冲区默认大小为4K或者4K的倍数(为什么是4K,和磁盘扇区有关,这里就不详述了),当写入的数据小于4K,会先存在缓冲区,当缓冲区满了,或者超过一定时间没有写入,操作系统会将数据一次性的刷入(flush)磁盘。
buffering的默认值为-1,表示使用缺省大小的buffer,如果是二进制模式,由io.DEFAULT_BUFFER_SIZE
决定,默认是4096
或者8192
,如果是文本模式,设备类型为终端设备时,使用行缓存方式
,如果不是,则使用二进制模式的策略。使用其他值时,有如下含义:
0
:只在二进制模式使用,表示关闭buffer1
:只在文本模式使用,表示使用行缓冲。(见到换行符就flush)大于1
:用于指定buffer的大小
二进制场景:
In [3]: import io
In [4]: f = open('123.txt','w+b')
In [5]: print(io.DEFAULT_BUFFER_SIZE)
8192
In [6]: f.write("daxin".encode())
Out[6]: 5
In [7]: f.seek(0)
Out[7]: 0
In [8]: f.read()
Out[8]: b'daxin'
In [9]: f.flush() # 默认情况下缓冲区大小为8192字节,所以我们写入的不会被立即刷入到磁盘上,如果我们要手动刷入可以手动执行flush命令
In [10]: f.close() # close会触发flush操作
In [11]: f = open('123.txt','w+b',4) # 缓冲区为4个字节
In [12]: f.write('dax'.encode()) # 3个字节不会刷入磁盘
Out[12]: 3
In [13]: f.write('in'.encode()) # 超过缓冲区大小会直接连同本次写入的数据一起刷入磁盘
Out[13]: 2
In [14]: f.seek(0)
Out[14]: 0
In [15]: f.read()
Out[15]: b'daxin'
In [16]: f.flush()
In [17]: f.close()
文本字符串场景:
In [18]: f = open('123.txt','w+') # 没有指定缓冲区的大小,采用默认值-1(由于不是终端设备,所以采用二进制的策略 由io.DEFAULT_BUFFER_SIZE控制
In [19]: !cat 123.txt
In [20]: f.write('daxin')
Out[20]: 5
In [21]: !cat 123.txt
In [22]: f.write('\n') # 遇到换行符的时候并没有刷入磁盘
Out[22]: 1
In [23]: !cat 123.txt
In [29]: f = open('123.txt','w+',1) # 指定缓冲区的大小为1(行)
In [30]: f.write('daxin')
Out[30]: 5
In [31]: f.write(' hello')
Out[31]: 6
In [33]: !cat 123.txt # 没有遇到换行符,所以不会刷入磁盘
In [34]: f.write('\n') # 遇到换行符,然后刷入磁盘
Out[34]: 1
In [35]: !cat 123.txt
daxin hello
In [36]:
buffering | 说明 |
---|---|
buffering=1 | 文本和二进制模式时,都是io.DEFAULT_BUFFER_SIZE控制 |
buffering=0 | 文本模式时不支持,二进制模式时表示关闭缓冲区 |
buffering=1 | 文本模式表示行换成,二进制模式表示一个字节 |
buffering>1 | 文本模式时,是io.DEFAULT_BUFFER_SIZE字节,flush后把当前字符串也写入磁盘。二进制模式时,表示缓冲区的大小,直到超过这个值时,才会刷入磁盘 |
一般来说,文本模式一般都用默认缓冲区大小,二进制模式可以指定buffer的大小。除非我们明确知道,否则选择默认缓冲区大小是一个比较好的选择,另外在日常编程中,明确知道需要写磁盘了,都会手动调用一次flush,而不是等到自动flush或者close的时候。
1.2.4 encoding编码
当我们操作二进制文件的时候,数据都是十六进制的,并没有什么编码的一说。而在使用文本模式时,就需要指定编码类型了,常用的中文编码比如,GBK、UTF-8、Unicode等,文件在内存中都是Unicode格式的,而存储在磁盘上就是按照本地编码的格式存储了,windows上默认的是GBK(ANSI,cp936),当我们存储的文本数据不是默认的格式时,在打开时就会产生乱码,这时可以使用指定编码格式打开文件。
encoding=None,默认值时,表示使用操作系统默认编码类型
- windows: GBK(0xB0A1)
- Linux: UTF-8(0xE5 95 8A)
1.2.5 其他参数
errors
: 什么样的编码错误将被捕获。
- None/strict:有编码错误将抛出ValueError异常
- ignore:表示忽略
newline
: 文本模式中,换行的转换。可以为None、''(空串)、'\r'、'\n'、'\r\n'。
- 读时,None表示'\r'、'\n'、'\r\n'都被转换为'\n';''(空串)表示不会自动转换通用换行符;其他合法字符表示换行符就是指定字符,就会按照指定字符分行
- 写时,None表示'\n'都会被替换为系统指定缺省分隔符(os.linesep);'\n'表示'\n'不替换;其他的合法字符表示'\n'会被替换为指定的字符
closefd
: 关闭文件描述符.
- True: 表示关闭它
- False: 会在文件关闭后保持这个文件描述符
fileobj.fileno()查看
1.3 读写操作
read(size=-1)
: 表示读取的多少个字符或字节;负数或者None表示读取到EOF(end of file),中文文本时,表示size个字符。
>>> f = open('123.txt','r+')
>>> f.read(5)
'daxin'
>>> f.read(5)
' hell'
>>> f.read(1)
'o'
>>> f.read(1)
'\n'
>>> f.read(1)
'你'
readline(size=-1)
: 一行行读取文件内容。size设置一次能读取行内几个字符或者字节。负数或者None表示读取到EOF(end of file)
>>> f = open('123.txt','r+')
>>> f.readline(3)
'a d'
>>> f.readline()
'axin hello world\n'
>>> f.readline(-10)
'c\n'
readlines(hint=-1)
: 读取所有行并返回列表。当指定hint为负数时,则会读取所有行,当指定为正数时,表示读取hint个字符所在的行
。比如第一行有6个字符,第二行有3个字符,当我们使用readlines(7)时,就会返回两行,而如果使用readlines(5)时就会只返回第一行。
In [58]: f.readlines()
Out[58]: ['1\n', '2\n', '3\n', '4\n', '5\n', '6\n', '7\n', '8\n', '9']
In [94]: f = open('123.txt','r+')
In [95]: f.readlines(9)
Out[95]: ['你好啊我了个去\n', '为什么\n']
In [98]: f.readlines(3)
Out[98]: ['你好\n', '在']
In [99]:
write(s)
: 把字符串写入到文件中并返回写入字符串的个数。
writelines(lines)
: 将字符串列表写入文件中。
In [60]: f = open('123.txt','r+')
In [65]: f.seek(0,2)
Out[65]: 25
In [67]: f.write('10')
Out[67]: 2
In [68]: f.seek(25)
Out[68]: 25
In [69]: f.read()
Out[69]: '10'
In [71]: f.writelines(['11','12'])
In [72]: f.seek(25)
Out[72]: 25
In [73]: f.read()
Out[73]: '101112'
1.4 关闭操作
当我们打开一个文件时,那么在系统中就会存在一个文件描述符对应这个文件,并且在文件系统中,一个程序可以使用的文件描述符的数量是有限的,如果我们使用完毕后不进行释放,那么很有可能就耗光文件描述符的数量(Linux中默认的文件描述符为1024,使用ulimit -a查看),产生异常,所以在使用完毕后,我们就需要手动的进行关闭
f.close()
关闭文件的操作,会同时调用flush(),把缓冲区的数据刷入磁盘。当文件已经关闭,再次关闭没有任何效果
1.5 上下文管理
上面说我们每次打开一个文件都需要手动的进行关闭,那么如果忘记了,这个文件会一直占用着对应的文件描述符,如果多次打开一个文件不关闭,那么每次都会占用新的文件秒师傅,直到文件描述符用完。上下文管理就是为了解决这种问题的,它有如下特点:
- 使用 with .. as 关键字。
- with语句块执行完毕的时候,会自动关闭文件对象
- 不会开启新的作用域(不会产生诸如函数的内部变量等)
2.6以上版本支持with读取,with open('/tmp/hello.txt') as fd:
,然后所有打开文件的操作都需要缩进,包含在with下才,这样不需要明确的指定close,当新的代码没有缩进的时候,文件会自动关闭。
In [103]: with open('123.txt','r+') as f:
...: f.read()
...:
In [105]: f.closed
Out[105]: True
或者:
In [100]: f = open('123.txt','r+')
In [101]: with f:
...: f.read()
...:
In [102]: f.closed
Out[102]: True
1.6 文件对象的其他方法
文件对象的内置方法还有很多种,如下所示:
fd.closed(): 判断文件是否被关闭,若被打开提示False,没有的话提示True
fd.flush(): 把修改的内容,强制刷新到文件中去
fd.isatty: 判断是否是一个终端文件
fd.mode: 查看文件的打开模式
fd.name: 查看文件的名称
fd.next: 迭代的方法,和readline很像,区别是,next读到末尾会报错,readline会继续返回空
fd.read: 一次性读取所有内容,以字符串的方式存取
fd.readable(): 判断文件是否可读
fd.readlines: 一次性读取所有内容,以列表的方式存取(适合操作小文件)
fd.readline(): 每次读取一行内容
fd.seek(0): 调整文件读取的指针位置
fd.seekable(): 判断文件是否可以调整指针位置(tty,磁盘等文件是不能被seek的),可以被seek则返回真,否则返回假
fd.tell(): 查询文件目前读取位置(以字符为单位)
fd.truncate(): 截取文件,从开头开始截取,不指定指针位置的话,那么会清空文件
fd.write: 把一个字符串写入到文件中去
fd.writelines():把字符串列表写入文件中
fd.xreadlines():读一行打印一行,针对大文件非常适用 -----> Python 2.x 中适用,3.x中已经取消
fd.encoding: 查看文件的编码
fd.writeable(): 判断文件是否可以写
fd.fileno(): 返回文件在操作系统上的文件描述符(一个进程的运行默认会打开三个特殊的文件描述符:0表示 stdin、1表示 stdout,2表示stderr)
fd.name: 文件名称
2 StringIO模块
用于在内存空开辟一个文本模式的buffer,可以像文件对象一样操作它,当close方法被调用时,这个buffer会被释放.
使用前需要先进行导入, from io import StringIO
In [106]: from io import StringIO
In [107]: s = StringIO() # 实例化一个StringIO对象用于读写,相当于 f = open('123.txt','r+')
In [108]: s.write('123')
Out[108]: 3
In [109]: s.readline()
Out[109]: ''
In [110]: s.seek(0)
Out[110]: 0
In [111]: s.readline()
Out[111]: '123'
getvalue()
: 获取全部内容,根文件指针没有关系.
In [106]: from io import StringIO
In [107]: s = StringIO()
In [108]: s.write('123')
Out[108]: 3
In [109]: s.readline()
Out[109]: ''
In [110]: s.seek(0)
Out[110]: 0
In [111]: s.readline() # 指针已经切换到文件末尾
Out[111]: '123'
In [112]: s.getvalue() # 无视指针,可以读取所有
Out[112]: '123'
一般来说,磁盘的操作比内存的操作要慢得多,内存足够的情况下,一般的优化思路是少落地,减少磁盘IO的过程,可以大大提高程序的运行效率.
3 BytesIO模块
用于在内存中开辟一个二进制模式的buffer,可以像文件对象一样操作它,当close方法被调用时,这个buffer会被释放.
调用前需要先导入: from io import BytesIO
In [114]: from io import BytesIO
In [115]: b = BytesIO() # 实例化一个BytesIO对象用于存储二进制数据
In [116]: b.write(b'abc')
Out[116]: 3
In [117]: b.seek(0)
Out[117]: 0
In [118]: b.read()
Out[118]: b'abc'
In [119]: b.getvalue() # 同样无视指针的存在
Out[119]: b'abc'
针对StringIO和BytesIO来说,还有如下方法:
- readable(): 可读吗?
- writeable(): 可写吗?
- seekable(): 可以调整指针吗?
4 file-like对象
类文件对象,可以向文件对象一个操作,比如socket对象,输入输出对象(stdin,stdout)等都是类文件对象.
In [120]: from sys import stdin,stdout
In [122]: stdout.write('123')
123
In [123]: type(stdout)
Out[123]: colorama.ansitowin32.StreamWrapper
In [124]:
每个程序初始化运行时,都会打开3个文件:
- stdin: 标准输入
- stdout: 标准输出
- stderr: 标准错误输出**