【Python系列专栏】第二十八篇 Python中的StringIO和BytesIO
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
这个问题在*上有回答,为什么我们不直接使用 str
和 bytes
,而要这么别扭地在内存中使用 StringIO
和 BytesIO
呢?其实呀,主要是因为 StringIO
和 BytesIO
都是 file-like Object
,可以像文件对象那样使用,当某些库的函数支持文件对象时,我们可以传入 StringIO
和 BytesIO
,也能使用,这一点 str
和 bytes
是没法做到的。
读写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
个位置。
小结
StringIO
和 BytesIO
是在内存中操作 str
和 bytes
的方法,和读写文件具有一致的接口。