Python IO

烈酒焚心 提交于 2020-02-28 00:45:37

I/O

在计算机中I/O是Input/Output的简写,表示输入和输出。由于程序和运行时的数据是在内存中驻留,并由CPU计算核心来执行,涉及到数据交换的地方比如磁盘、网络等时,就需要I/O接口。

通常程序完成I/O操作会有InputOutput两个数据流,当然也有只用一个的情况,比如从磁盘读取文件到内存,就只会使用Input操作。相反将数据从内存写入到磁盘文件时也只有一个Output操作。

输入输出是相对的,需要考虑具体的对象是什么。一般而言,当编写的程序需要读取磁盘文件时,相当于将磁盘的数据输入到程序中,对于程序而言读取的数据就属于Input,对于磁盘而言则相当于将数据输出给程序,输出的数据是属于Output

Stream

I/O编程中,流Stream是一个很重要的概念,可以把流想象成一根管道,数据就是水管中的水,但只能单向流动。Input Stream输入流是数据从外部比如磁盘或网络流进内存,Output Stream输出流则是数据从内存 流到外部。例如,对于浏览网页来说,浏览器和服务器之间至少需要建立了两条水管才能收发数据。

同步异步

由于CPU和内存的速度远高于外设的速度,所以I/O编程中存在速度验证不匹配的问题 。比如说要将100MB的数据写入磁盘,CPU输出100MB数据只需0.01秒,而磁盘接收这100MB数据可能需要10秒,那么这个时候应该怎么办呢?这里有两种解决的方案:

  1. 让CPU等着,也就是程序暂停执行后续代码,等100MB数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO。
  2. 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()函数时,传入模式为wwb表示写文本文件或二进制文件。

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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!