一、IO编程概述
- 在计算机中,IO指的是
input(输入)
和output(输出)
。 - 计算机中程序和运行时产生的数据会在内存中驻留,然后由CPU来执行,其中涉及到数据交换的地方,例如磁盘、网络等,就需要IO接口,下面来看一些日常生活、工作中的案例,来帮助更好的理解IO:
在我们打开浏览器时,访问百度首页,这时浏览器这个程序就需要通过网络IO获取百度的首页,浏览器会先发送数据给百度服务器,告诉服务器想要的HTML页面,这个动作是往外发数据,叫做output,随后百度服务器把网页发过来,这个动作是从外面接收数据,叫做input。
通常来说,程序完成IO操作会有input和output两个数据流,但是也有使用一个的情况,例如:
从磁盘读取文件到内存,只有input操作,从内存把数据写到磁盘,同样只有output操作
在IO编程中,Stream(流)是一个很重要的概念,流就像是一个水管,而数据就是水管里的水,水管里的水只能单向流动,其中inputstream就像是水从B端流向A端,即数据从磁盘读进内存,反之,outputstream就像是水从A端流向B端,即内存把数据写到磁盘,对于浏览网页来说,浏览器和服务器之间就需要至少建立两条水管,来实现输入和输出的目的
由于CPU和内存的速度远远高于外设的速度,所以在IO编程中,就会出现速度严重不匹配的问题,例如:
当我们想把一个100MB的数据写入磁盘时,如果是CPU的话,可能只需要0.01秒,而磁盘接收这个数据,可能就要10秒,这样的问题有两种解决方法:
同步IO:在程序遇到需要把数据读入磁盘时,程序暂停执行后续代码,等待数据读入磁盘后,再执行剩余代码,在数据读入途中CPU进行等待
异步IO:在遇到上面的情况时,CPU不进行等待,后续代码继续执行
上面的两种模式的区别在于,CPU是否等待IO执行的结果,下面来看一下案例,帮助更好的理解两种模式:
背景:小明想去逛商场,但是逛商场之前想吃汉堡
同步IO:小明去点汉堡之后,服务员说,汉堡现做,需要等待5分钟,小明点餐之后,在收银台前等了5分钟,拿到汉堡之后,再去逛商场,这就是同步IO
异步IO:小明去点汉堡之后,服务员说,汉堡现做,需要5分钟,汉堡做好之后通知小明,在这期间,小明可以先去逛商场,这就是异步IO
看过上面的例子之后,很明显的看出,异步IO比同步IO速度快,使用异步IO来编写程序的性能远远高于同步IO,但是异步IO的缺点就是编程模型复杂,根据上面的案例中,我们需要定义在汉堡做好之后,服务员要通过什么方式通知小明,如果是服务员跑过来找到小明说的话,这是回调模式,如果服务员发短信通知小明,小明需要不定期的查看手机短信,这就是轮询模式
操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也一样,下面只说同步IO模式
二、文件读写
- 读写文件是最常见的IO操作,Python内置了读写文件的函数,用法和C语言是兼容的
- 在读写文件之前,我们需要了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象,通常把这个文件对象叫做文件描述符,然后通过操作系统提供的接口从这个文件对象中读取数据,即读取文件,或者把数据写入这个文件对象,即写入文件
- 读取文件
要以读取文件的模式打开一个文件对象,可以使用Python内置的函数open(),例如:
#下面以centos系统为例 [root@centos-1 ~]# echo "aaaaa" > test.txt #先创建一个文件 [root@centos-1 ~]# python Python 3.9.9 (main, May 10 2022, 15:32:30) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> f = open("/root/test.txt","r") >>> - open函数的第一个参数是文件路径,第二个参数为标识符,'r'为读 #如果打开的文件对象不存在,则会抛出一个IOError的错误,并且给出错误码和详细的信息,说明文件不存在 >>> f = open("/root/aaa.txt","r") Traceback (most recent call last): File "<stdin>", line 1, in <module> FileNotFoundError: [Errno 2] No such file or directory: '/root/aaa.txt' - 文件成功打开后,调用'read()'方法可以一次性读取文件的全部内容,Python会把内容读取到内存,使用'str'字符串对象表示 >>> f = open("/root/test.txt","r") >>> f.read() 'aaaaa\n' #\n表示换行符 - 在文件使用完毕之后,我们必须使用'close()'方法来关闭文件,因为文件对象会占用操作系统资源,并且操作系统同一时间能打开的文件数量也是有限的 >>> f.close() - 由于文件读写时可能会产生'IOError'错误,一旦出错后,后面的'f.close()'方法就不会调用,所以,为了保证无论是否出错都能正确的关闭文件,我们可以使用'try...finally'来达到目的 [root@centos-1 ~]# cat test.py #/usr/bin/enc python3 # -*- coding: utf-8 -*- try: f = open('/root/test.txt','r') print(f.read()) finally: if f: f.close() [root@centos-1 ~]# python3 test.py #执行脚本 aaaaa - 但是每次都像上面这么写的话,实在是太麻烦了,Python引入了'with'语句来自动帮我们调用'close()'方法 [root@centos-1 ~]# cat test.py #/usr/bin/enc python3 # -*- coding: utf-8 -*- with open('/root/test.txt','r') as f: print(f.read()) [root@centos-1 ~]# python3 test.py aaaaa
调用read()方法会一次性读取文件的所有内容,如果文件太大的话,内存就爆了,所以,保险起见,我们可以反复调用read(size)方法,每次最多读取指定size大小的内容
除了read(size),调用readline()可以每次读取一行内容,调用readlines()一次性读取所有内容并按行返回list列表,根据需求决定如何调用
如果文件很小,直接使用read()一次性读取是最方便的,如果不能确定文件大小,我们可以反复调用read(size),如果是配置文件,调用readlines()最方便
[root@centos-1 ~]# cat test.py #/usr/bin/enc python3 # -*- coding: utf-8 -*- f = open('test.txt','r') n = 1 for line in f.readlines(): print('第%s行为:' % n,line.strip()) #line.strip()会去掉每行的空格、换行符、制表符等 n = n + 1 f.close() [root@centos-1 ~]# python3 test.py 第1行为: aaaaa 第2行为: bbbbb 第3行为: ccccc
- file-like Object
像open()函数返回的这种有个read()方法的对象,在Python中统称为file-like Object对象,除了file之外,还可以是内存的字节流、网络流、自定义流等
file-like Object不要求从特定类继承,只要写个read()方法就行
StringIO就是在内存中创建的file-like Object,常用作临时缓冲
如果要想在内存中对数据进行读写,可以使用StringIO和BytesIO,前者是对字符串数据的读写,后者是对二进制数据的读写
- 二进制文件
- 如果读取的是二进制文件,我们可以使用
rb
模式打开文件,例如:
>>> f = open('/root/test.txt', 'rb') #使用rb模式 >>> f.read() b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六进制表示的字节
- 字符编码
- 想要读取非
utf-8
编码的文本文件,需要给open()
函数传入encoding
参数,例如:
- 读取GBK编码的文件 >>> f = open('/root/test.txt', 'r', encoding='gbk') >>> f.read() '测试'
遇到有些编码不规范的文件,可能会遇到报错UnicodeDecodeError,这是因为在文本文件中可能夹杂了一些非法编码的字符,遇到这种情况,open()函数还可以接收一个errors参数,表示如果遇到编码错误后如何处理,最简单的方法就是直接忽略,例如:
>>> f = open('/root/test.txt', 'r', encoding='gbk', errors='ignore')
- 写入文件
- 写文件和读文件是一样的,区别是在调用
open()
函数时,传入标识符w
或wb
表示写入文本文件或者二进制文件
[root@centos-1 ~]# python Python 3.9.9 (main, May 10 2022, 15:32:30) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> f = open('/root/test.txt','w') >>> f.write('aaaaaaaa') 8 >>> f.read() #因为标识符是w,写入,所以无法读取文件 Traceback (most recent call last): File "<stdin>", line 1, in <module> io.UnsupportedOperation: not readable >>> f.close() >>> f = open('/root/test.txt','r') >>> f.read() 'aaaaaaaa'
可以反复调用write()方法来写入文件,但是和读取文件一样,在写入之后必须要使用close()方法来关闭文件,当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再去写入,只有调用close()方法时,操作系统才会保证把没有写入的数据全部写入磁盘
忘记调用close()方法的后果,就是写入的数据可能只有一点到了磁盘,其余数据全部丢失,所以,通常来说,为了防止忘记使用close(),我们可以直接使用with语句
[root@centos-1 ~]# cat test.txt aaaaaaaa [root@centos-1 ~]# python Python 3.9.9 (main, May 10 2022, 15:32:30) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> with open('/root/test.txt','w') as f: ... f.write('Hello World!!!') ... 14 >>> with open('/root/test.txt','r') as f: ... print(f.read()) ... Hello World!!!
要写入特定编码的文本文件,需要给open()函数传入encoding参数,将字符串自动转换成指定编码
从上面的案例可以看到,test.txt文件原本的内容是aaaaaaaa,在以w模式写入文件时,新的Hello World!!!的内容覆盖了原本的内容,如果我们想要追加到文件末尾的话,可以使用a模式,以追加模式写入
[root@centos-1 ~]# cat test.txt Hello World!!![root@centos-1 ~]# python Python 3.9.9 (main, May 10 2022, 15:32:30) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> with open('/root/test.txt','a') as f: ... f.write('aaaaaaa') ... 7 >>> with open('/root/test.txt','r') as f: ... print(f.read()) ... Hello World!!!aaaaaaa
三、StringIO和BytesIO
- StringIO
很多时候,数据读写不一定是文件,也可以在内存中读写,当并不想把数据写到本地磁盘时,就可以使用StringIO
StringIO顾名思义就是在内存中读写str
想要把str写入StringIO,首先需要创建一个StringIO,然后像文件一样写入即可,例如:
[root@centos-1 ~]# python Python 3.9.9 (main, May 10 2022, 15:32:30) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from io import StringIO >>> f = StringIO() >>> f.write('Hello') #直接进行写入 5 >>> f.write(' World!!!') 9 >>> print(f.getvalue()) #使用getvalue()方法获取写入后的str Hello World!!!
想要读取StringIO,可以使用一个str初始化StringIO,然后像文件一样读取,例如:
[root@centos-1 ~]# python Python 3.9.9 (main, May 10 2022, 15:32:30) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from io import StringIO >>> f = StringIO('Hello! \n World!!!') >>> while True: ... s = f.readline() ... if s == '': ... break ... print(s.strip()) ... Hello! World!!!
- BytesIO
- 上面的StringIO操作的只能是Str数据,如果要操作二进制数据,就需要使用
BytesIO
- BytesIO实现了在内存中读写bytes,下面来创建一个BytesIO,然后写入一些bytes
[root@centos-1 ~]# python3 Python 3.9.9 (main, May 10 2022, 15:32:30) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from io import BytesIO >>> f = BytesIO() >>> f.write('中文'.encode('utf-8')) 6 >>> print(f.getvalue()) b'\xe4\xb8\xad\xe6\x96\x87' - 注意上面写入的是经过UTF-8编码的bytes
和StringIO相似,可以使用一个bytes初始化BytesIO,然后像文件一样读取
[root@centos-1 ~]# python Python 3.9.9 (main, May 10 2022, 15:32:30) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from io import BytesIO >>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87') >>> f.read() b'\xe4\xb8\xad\xe6\x96\x87'
小结:StringIO和BytesIO是在内存中操作str和bytes的方法,从内存中读写string或者bytes与直接读写文件类似,有相同的接口