欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Python系列专栏】第二十八篇 Python中的StringIO和BytesIO

程序员文章站 2022-03-09 15:24:26
...

StringIO和BytesIO

StringIO

很多时候,数据读写不一定是对文件进行的,我们也可以在内存中进行读写操作。

StringIO 顾名思义就是在内存中读写 str

要把 str 写入 StringIO,我们需要先创建一个 StringIO 对象,然后,像文件一样写入即可:

>>> from io import StringIO
>>> f = StringIO()
>>> f.write('hello')
5
>>> f.write(' ')
1
>>> f.write('world!')
6
>>> print(f.getvalue())
hello world!

getvalue() 方法用于获得IO流中的全部内容

读取 StringIO 的方法也和读文件类似:

>>> from io import StringIO
>>> f = StringIO('Hello!\nHi!\nGoodbye!') # 用一个str初始化StringIO
>>> while True:
...     s = f.readline()
...     if s == '': # 读取完毕,跳出循环
...         break
...     print(s.strip()) # 去掉当前行首尾的空格再打印
...
Hello!
Hi!
Goodbye!

BytesIO

StringIO 操作的只能是 str,如果要操作二进制数据,就需要使用 BytesIO

BytesIO 实现了在内存中读写 bytes,我们创建一个 BytesIO,然后写入一些 bytes

>>> from io import BytesIO
>>> f = BytesIO()
>>> f.write('中文'.encode('utf-8'))
6
>>> print(f.getvalue())
b'\xe4\xb8\xad\xe6\x96\x87'

请注意,写入的不是 str,而是经过UTF-8编码的 bytes

StringIO 类似,读取 BytesIO

>>> from io import BytesIO
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87') # 用一个bytes初始化BytesIO
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'

为什么使用StringIO和BytesIO

这个问题在*上有回答,为什么我们不直接使用 strbytes,而要这么别扭地在内存中使用 StringIOBytesIO 呢?其实呀,主要是因为 StringIOBytesIO 都是 file-like Object,可以像文件对象那样使用,当某些库的函数支持文件对象时,我们可以传入 StringIOBytesIO,也能使用,这一点 strbytes 是没法做到的。


读写IO需要注意的地方

StringIO 为例,前面我们将到读取 StringIO 时,是先使用字符串进行初始化,然后再读取

>>> f = StringIO('Hello!\nHi!\nGoodbye!') # 用一个str初始化StringIO
>>> f.readlines()
['Hello!\n', 'Hi!\n', 'Goodbye!']

但如果我们没有进行初始化,而是对一个空的 StringIO 进行写入,然后再读取呢?这时就会像下面这样:

>>> f = StringIO()
>>> f.write('Hello!\n')
7
>>> f.write('Hi!\n')
4
>>> f.write('Goodbye!')
8
>>> f.readlines()
[]

我们发现此时读取不到写入的字符串了,这是为什么呢?其实呀,这时因为当前所处流的位置(Stream Position)在末尾,所以读取不到东西了。那怎么知道当前处在流的什么位置呢?我们可以使用 tell() 方法。对比一下:

使用字符串初始化 StringIO

>>> f = StringIO('Hello!\nHi!\nGoodbye!')
>>> f.tell()
0

写入 StringIO

>>> f = StringIO()
>>> f.write('Hello!\n')
7
>>> f.write('Hi!\n')
4
>>> f.write('Goodbye!')
8
>>> f.tell()
19

可以发现,使用字符串初始化时,位置会保持在流的开头,而使用 write() 方法对流进行写入操作后,位置会移动到写入结束的地方。那有没有办法在写入以后进行读取呢?有!可以使用前面提到的 getvalue() 方法读取IO流中的全部内容,另外也可以使用 seek() 方法回到前面的某一位置,然后读取该位置后的内容:

>>> f.tell()
19
>>> f.seek(0) # 回到流的开头位置
0
>>> f.tell()
0

有时候呀,我们可能会在初始化一个 StringIO 之后,想要对其进行写入操作,这时会发生一个问题:

>>> f = StringIO('Hello!')
>>> f.getvalue()
'Hello!'
>>> f.write('Hi!')
3
>>> f.getvalue()
'Hi!lo!'

可以看到初始化的内容被写入的内容覆盖了,这显然不是我们所希望的。为什么会这样呢?其实呀,跟前面说的问题是一样的,举一反三,都是因为Stream Position引起的。初始化一个 StringIO 后,位置在流的开头,此时写入就会从流的开头写入,而不是像我们所希望的那样从流的末尾写入,稍微改动一下就好了:

>>> f = StringIO('Hello!')
>>> f.seek(0, 2) # 移动到流的末尾
6
>>> f.write('Hi!')
3
>>> f.getvalue()
'Hello!Hi!'

除了移动到流的末尾,也能移动到某个位置,看看 seek() 方法的描述:

Help on built-in function seek:

seek(pos, whence=0, /) method of _io.StringIO instance
    Change stream position.

    Seek to character offset pos relative to position indicated by whence:
        0  Start of stream (the default).  pos should be >= 0;
        1  Current position - pos must be 0;
        2  End of stream - pos must be 0.
    Returns the new absolute position.

可以看到 seek() 方法有必选参数 pos 和 可选参数 whence,前者是移动多少,后者是从哪里开始移动。whence 默认为0,也即默认从流的开头移动 pos 个位置。


小结

StringIOBytesIO 是在内存中操作 strbytes 的方法,和读写文件具有一致的接口。