I/O
在计算机中I/O是Input/Output
的简写,表示输入和输出。由于程序和运行时的数据是在内存中驻留,并由CPU计算核心来执行,涉及到数据交换的地方比如磁盘、网络等时,就需要I/O接口。
通常程序完成I/O操作会有Input
和Output
两个数据流,当然也有只用一个的情况,比如从磁盘读取文件到内存,就只会使用Input
操作。相反将数据从内存写入到磁盘文件时也只有一个Output
操作。
输入输出是相对的,需要考虑具体的对象是什么。一般而言,当编写的程序需要读取磁盘文件时,相当于将磁盘的数据输入到程序中,对于程序而言读取的数据就属于Input
,对于磁盘而言则相当于将数据输出给程序,输出的数据是属于Output
。
Stream
I/O编程中,流Stream
是一个很重要的概念,可以把流想象成一根管道,数据就是水管中的水,但只能单向流动。Input Stream
输入流是数据从外部比如磁盘或网络流进内存,Output Stream
输出流则是数据从内存 流到外部。例如,对于浏览网页来说,浏览器和服务器之间至少需要建立了两条水管才能收发数据。
同步异步
由于CPU和内存的速度远高于外设的速度,所以I/O编程中存在速度验证不匹配的问题 。比如说要将100MB的数据写入磁盘,CPU输出100MB数据只需0.01秒,而磁盘接收这100MB数据可能需要10秒,那么这个时候应该怎么办呢?这里有两种解决的方案:
- 让CPU等着,也就是程序暂停执行后续代码,等100MB数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO。
- CPU不等待,只是告诉磁盘:“您老慢慢写,不着急,我接着干别的事去了。”,于是后续代码可以立即接着执行,这种模式称为异步IO。
同步和异步的区别在于是否等待IO执行的结果,很显然使用异步IO编写程序的性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。想想看,你得知道什么时候通知你IO执行结束,另外通知的方式也分为回调模式和轮询模式。总而言之,异步IO的复杂度远远高于同步IO。
IO操作
操作IO的能力都是由操作系统提供的,每种编程语言都会把操作系统提供的低级C语言接口封装起来以方便使用,Python同步也不例外。读写文件是最常见的IO操作,Python内置读写文件的函数,用法和C语言是兼容的。
在磁盘上读写文件的功能都是由操作系统提供,现代操作系统不允许普通的程序直接操作磁盘,所以读写文件是请求操作系统打开一个文件对象,通常被称之为文件描述符fd
,然后操作系统提供的接口从这个文件对象中读取数据(读文件),或将数据写入到文件对象(写文件)。
读文件
打开文件
Python中open()
函数用于打开一个文件,并创建file
对象(文件描述符fd
)。
fd = open(
filename,
mode = "r",
buffering = 1,
encoding = None,
errors = None,
newline = None,
closefd = True,
opener = None
)
参数说明
filename
表示一个包含所需访问的文件名称的字符串值,filename
包含文件所在的存储路径,存储路径可以是相对路径也可以是绝对路径。mode
表示决定打开文件的读写模式,也就是设定文件的打开权限,包含只读r
、写入w
、追加a
。模式参数是非强制的,默认文件访问模式为只读r
。buffering
用于指定打开文件所使用的缓冲方式,缓冲是指用于读取文件的缓冲区,缓冲区是一段内存区域,设置缓冲区的目的是先将文件内容读取到缓冲区,可以减少CPU读取磁盘的次数。当buffering为0时表示不缓冲,当buffering为1时表示只缓冲一行数据,当buffering为-1时表示使用系统默认缓冲机制,默认值为-1.任何大于1的值表示使用给定的值作为缓冲区的大小。encoding
用于指定文件的编码方式,默认采用UTF-8。编码方式主要用于指定文件中的字符编码,以避免乱码。
读写模式
r
表示以只读方式打开文件,文件的指针将会放在文件的开头,默认模式。
r
模式只能用于打开已存在的文件,当打开不存在的文件时,open
函数会抛出异常。
filename = "gbk.txt"
try:
fd = open(filename, "r")
except IOError:
print("%s 文件不存在"%filename)
当需要打开的文件名称不带路径时,open
函数会在Python程序运行的当前目录中寻找该文件,若当前目录下没有该文件则open
函数会抛出IOError
异常。
使用r
默认读取的是UTF-8编码的文本文件
rb
表示以二进制格式打开文件用于只读,文件指针将会放在文件开头,默认模式。
如果要读取二进制文件,比如图片、视频等,则需要使用rb
模式打开文件即可。
file = "pic.png"
with open(file, "rb") as fd:
print(fd.read())
需要注意的是读取的二进制文件,打印输出时是以十六进制表示的字符。
r+
表示打开文件用于读写,文件指针将放置在文件开头。rb+
表示以二进制格式打开一个文件用于读写,文件指针将会放置在文件开头。w
表示打开文件只用于写入,如果文件已存在则打开文件,并从头开始编辑,即原有内容会被删除。如果文件不存在则会创建新文件。wb
表示以二进制格式打开一个文件只用于写入,如果文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果文件不存在,则会创建新文件。w+
表示以二进制格式打开一个文件用于读写,如果文件已存在则打开文件,并头开头开始编辑,即原有内容会被删除,如果文件不存在则会创建新文件。wb+
表示以二进制格式打开一个文件用于读写,如果文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除,如果文件不存在则会创建新文件。a
表示打开一个文件用于追加,如果文件已存在则文件指针会放置到文件结尾,也就是说,新的内容将会被写入到已有内容之后。ab
表示以二进制格式打开一个文件用于追加,如果文件已存在则文件指针将会放置到文件结尾,也就是说,新的内容将会被写入到已有内容之后,如果文件不存在则会创建新文件并进行写入。a+
表示打开一个文件用于读写,如果文件已存在则文件指针会被放置到文件结尾,文件打开时会是追加模式,如果文件不存在则会创建新文件用于读写。ab+
表示以二进制格式打开一个文件用于追加,如果文件已存在则文件指针会被放置到文件结尾,如果文件不存在则会创建新文件并用于读写。
例如:当需要以读文件模式打开一个文件对象时,可使用Python内置的open()
函数,传入文件名称和模式标示符。
fd = open("/home/test.py", "r")
fd.read()
fd.close()
标示符r
表示读read
,即以读文件模式打开文件对象。若文件不存在open()
函数会抛出一个IOError
的错误,并给出 错误码和详细信息。如果文件打开成功,接着可调用read()
函数一次性读取文件的全部内容。Python会将内存读取到内存并使用一个字符串str
对象表示。最后可以调用close()
函数关闭文件。需要在注意的是,文件使用完毕必须关闭,因为文件对象会占用操作系统的资源,另外操作系统同一时间能打开的文件数量也是有限的。
关闭文件
首先Python中的使用open
打开文件后会自动关闭文件,所以不显式地主动关闭也是可以被接受的。其次,Python自动关闭文件的时间是不确定的,所以推荐主动关闭文件。另外,最好的处理文件的方式是使用上下文管理器with
打开文件,这样可以确定文件关闭时间。最后使用fd.close()
显式地关闭文件时,只是不能读写文件,但文件对象依然是存在的。
由于文件读写时都由可能产生IOError
,一旦出错,后续代码close()
代码就不会被调用。因此,为了保证无论是否出错都能正确地关闭文件,可使用try...finally
来处理异常。
try:
fd = open("/home/test.py", "r")
print(fd.read())
finally:
if fd:
fd.close()
上面的代码繁琐,因此Python引入了with
语句来帮助自动调用close()
方法。使用with
语句和try...finally
效果是一样的,但是代码更加简洁,并且不必调用fd.close()
函数。
with open("/home/test.py", "r") as fd:
print(fd.read())
读取内容
全部去读
这里使用read()
函数会一次性读取文件的全部内容,如果文件较大,比如有10GB,那一次性读取文件时内存就会爆掉。
分块读取
为了保险起见,可以反复调用read(size)
函数,每次最多读取size
个字节的内容。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
file = "server.log"
size = 1024
with open(file, "r") as fd:
while True:
chunk = fd.read(size)
print(chunk)
if not chunk:
break
读取单行
调用readline()
函数可每次读取一行内容
读取多行
调用readlines()
函数可一次性读取所有内容并按行返回列表list
。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
file = "config.py"
with open(file, "r") as fd:
for line in fd.readlines():
print(line.strip())# strip将末尾\n去除
如果文件很小,read()
一次性读取最为方便,如果不能确定文件大小,反复调用read(size)
比较保险。如果是配置文件,调用readlines()
最为方便。
读取非UTF-8编码的文本文件
若需要读取非UTF-8编码的文本文件,则需要给open()
函数传入encoding
参数。
例如:读取GBK编码的文件
file = "gbk.txt"
with open(file, "r", encoding="gbk") as fd:
print(fd.read())
如果遇到有些编码不规范的文件,比如可能会遇到UnicodeDecodeError
错误,因为文本文件中可能夹杂了非法编码的字符。遇到这种情况open()
函数还可接收一个errors
参数表示如果遇到编码错误后如何处理,最简单的处理方式是选择直接忽略。
file = "gbk.txt"
with open(file, "r", encoding="gbk", errors="ignore") as fd:
print(fd.read())
写文件
写文件和读文件是一样的,唯一区别是调用open()
函数时,传入模式为w
或wb
表示写文本文件或二进制文件。
filename = "test.log"
fd = open(filename, "w")
content = "hello world"
fd.write(content)
fd.close()
可以反复使用write()
函数来写入文件,但务必需要调用fd.close()
函数来关闭文件。
当写文件时,操作系统往往不会立即将数据写入磁盘,而是放在内存中缓存起来,空闲的时候再慢慢写入。只有调用close()
函数时操作系统才保证将没有写入的数据全部写入磁盘。如果忘记调用close()
函数的后果是数据可能只写入了一部分到磁盘,剩下的丢失了。所以,还是使用上下文with
语句比较保险。
with open(filename, "w") as fd:
content = "hello world"
fd.write(content)
StringIO
- Python内置的IO包中的StringIO类用于在内存中读写字符串
- 当在StringIO对象上调用
close
函数后文本缓冲区将被清空 - 可以通过初始化函数来初始化一段内存空间,也可以通过
write
函数将字符串写入到内存。 - 使用
getvalue
函数返回内存缓冲区的所有内容 - 内存缓冲区的内容可以通过
readline
函数读取
很多时候,数据读写不一定都是文件,也可以在内存中读写,StringIO就是在内存中读写字符串。
Python中的StringIO
经常被用来作字符串的缓存,因为StringIO
的接口和文件操作是一致的,也就是说同样的代码,可以同时当成文件操作或StringIO
操作。
StringIO是io
模块中的类,在内存中开辟的一个文本模式的缓冲区buffer
,在其中可以像文件对象一样进行操作字符串。当使用close
函数调用时这个缓冲区会被释放掉。
StringIO的优点在于,磁盘的操作比内存操作要慢得多,内存足够的时候,常用的优化思路是少落地,即减少磁盘IO的过程,可以大大提高程序的运行速度。业务中单机情况可使用StringIO或BytesIO,多机则考虑使用Redis。
StringIO的缺点在于内存断电,缓存中的数据会丢失,所以不建议存储很重要的数据,但可以存储日志之类的丢失也没有太大影响的数据。
导入模块
from io import StringIO
如果需要将字符串写入StringIO,则需先创建一个StringIO,然后像文件一些写入即可。
from io import StringIO
fd = StringIO()
fd.write("hello world")
# 使用getvalue()函数获取写入后的字符串
print(fd.getvalue())
如果需要读取StringIO,可以使用一个字符串初始化StringIO,然后像读取文件一样读取即可。
from io import StringIO
fd = StringIO("hello world\nthank you\nyou are welcome\n")
while True:
line = fd.readline()
if line == "":
break
print(line.strip())
StringIO常用方法
stringio.read([size])
read
函数的参数size
用于限定读取的长度,类型为int
整形,默认会从当前位置读取对象中所有的数据,读取结束后位置被移动。
stringio.readline([length])
readline
函数的参数length
用于限定读取的结束位置,类型为int
整型,缺省值为None
,即表示从当前位置读取至下一个以\n
为结束符的行,读位置被移动。
stringio.readlines()
readlines
函数表示读取所有行
stringio.write(string)
write
函数用于从读写位置将参数string
写入到对象stringio
中,参数为字符串或Unicode类型,读写位置被移动。
stringio.writeline(list)
writeline
函数用于从读写位置将列表list
写入给对象stringio
,参数list
为一个列表,列表的成员为字符串str
或Unicode类型,读写位置被移动。
stringio.getvalue()
getvalue
函数将会返回对象stringio
中的所有数据
stringio.truncate([size])
truncate
表示从读写位置起切断数据,参数size
限定裁剪的长度,默认为None
。
stringio.tell()
tell
函数将会返回当前读写位置
stringio.seek(pos[, mode])
seek
函数用于移动当前读写位置至指定pos
位置处,可选参数mode
为0时表示将读写位置移动到pos
处,可选参数mode
为1时表示将读写位置从当前位置移动pos
个长度,可选参数mode
为2时表示读写位置置于末尾处再先后移动pos
个长度,默认值为0。
stringio.close()
close
函数用于释放缓冲区,执行close
函数后数据将被释放,不可再进行操作。
stringio.isatty()
isatty
函数总是会返回0,不存StringIO对象是否已经被关闭close
。
stringio.flush()
flush
函数用于刷新缓冲区
例如:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from io import StringIO
# 创建StringIO对象,当前缓冲区内容为123456789。
data = "123456789"
sfd = StringIO(data)
print(sfd.getvalue()) #123456789
# 从开头写入abc将覆盖123
sfd.write("abc")
print(sfd.getvalue()) #abc456789
# 每次使用read读取前必须先seek,seek(0)表示定位到开头
sfd.seek(0)
# 从当前位置读取到结束
print(sfd.read())#abc456789
# 定位到第4个位置
sfd.seek(3)
# 从第4个位置开始读取2个字符
print(sfd.read(2))#45
# 定位到第7个位置
sfd.seek(6)
# 从当前位置开始写入
sfd.write("xyz")
# 读取写入后的内容
print(sfd.read())#
# 获取当前值
print(sfd.getvalue()) #abc456xyz
例如:使用StringIO缓冲以及paramiko的RSAKey生成密钥对
SSH协议的开源实现是OpenSSH,Paramiko是Python中的一个库,实现了SSHv2协议,底层使用cryptography
。通过Paramiko可以在Python代码中直接使用SSH协议对远程服务器执行操作,而不是通过SSH命令对远程服务器进行操作。由于Paramiko属于第三方库,使用前需进行安装:
$ pip install paramiko
$ vim rsa.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
from io import StringIO
from paramiko import RSAKey
# 生成RSA公钥和私钥
def gen_rsa_keys(key=""):
output = StringIO()
sbuffer = StringIO()
content = {}
#判断私钥是否存在,若不存在则生成,并将其缓存到output之中。
if not key:
try:
key = RSAKey.generate(2048)
key.write_private_key(output)
private_key = output.getvalue()
except IOError:
raise IOError("generate keys: there was an error writing to the file")
except SSHException:
raise SSHException("generate keys: the key is invalid")
#若私钥存在则直接获取
else:
private_key = key
output.write(private_key)
print(output.getvalue())
try:
key = RSA.from_private_key(output)
except SSHException as e:
print(e)
# 利用私钥生成公钥
for data in [key.get_name(), " ", key.get_base64(), " %s@%s"%("yap", os.uname()[1])]:
sbuffer.write(data)
public_key = sbuffer.getvalue()
content["public_key"] = public_key
content["private_key"] = private_key
return content
# 测试
keys = gen_rsa_keys()
print(keys)
$ python rsa.py
StringIO只能操作字符串,如果需要操作二进制数据,则需要使用BytesIO。
BytesIO
BytesIO实现了在内存中读写字节bytes
。BytesIO是io
模块中的类,用于在内存中开辟一个二进制缓冲区,可以像文件对象一样进行操作,当使用close
函数关闭时,这个二进制缓冲区同时会被释放掉。
from io import BytesIO
bfd = BytesIO()
print(bfd.readable())#True
print(bfd.writable())#True
print(bfd.seekable())#True
byte = b"hello\nworld"
bfd.write(byte)
bfd.seek(0)
print(bfd.readline())#b"hello\n"
print(bfd.tell())#6
print(bfd.getvalue())#b"hello\nworld"
bfd.close()
创建一个BytesIO
并写入一些字节
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from io import BytesIO
bfd = BytesIO()
message = "中文".encode("utf-8")
bfd.write(message)
print(bfd.getvalue())
# b'\xe4\xb8\xad\xe6\x96\x87'
使用BytesIO的write
函数写入的并不是字符串而是经过UTF-8编码后的字节bytes
。
使用字节bytes
初始化BytesIO,然后可以像读写文件一样进行操作。
from io import BytesIO
byte = b"\xe4\xb8\xad\xe6\x96\x87"
bfd = BytesIO(byte)
print(bfd.read())
# b'\xe4\xb8\xad\xe6\x96\x87'
例如:使用BytesIO获取远程图片的大小格式
安装图片模块
$ pip install pillow
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from io import BytesIO
from PIL import Image
import requests
url = "http://p99.pstatp.com/large/pgc-image/95b9aa2664c1441199795f84e2812e39.jpg"
# 远程请求获取字节流
response = requests.get(url, stream=True)
# 将请求到的数据转换为字节流
bytes_stream = BytesIO(response.content)
#使用Image打开字节流数据
roi_img = Image.open(bytes_stream)
# 获取图片格式
print(roi_img.format)
# 获取图片大小
print(roi_img.size)
# 创建空字节IO对象保存图片字节流
img = BytesIO()
img = bytes_stream.getvalue()
print(img)
作者:JunChow520
链接:https://www.jianshu.com/p/cbfc890254bc
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
来源:oschina
链接:https://my.oschina.net/u/3406045/blog/3164692