将 io.StringIO 转换为 io.BytesIO

Posted

技术标签:

【中文标题】将 io.StringIO 转换为 io.BytesIO【英文标题】:convert io.StringIO to io.BytesIO 【发布时间】:2019-09-17 06:12:05 【问题描述】:

原始问题:我有一个StringIO 对象,如何将其转换为BytesIO?

更新:更普遍的问题是,如何在python3中将二进制(编码的)file-like对象转换为解码的file-like对象?

我得到的幼稚方法是:

import io
sio = io.StringIO('wello horld')
bio = io.BytesIO(sio.read().encode('utf8'))
print(bio.read())  # prints b'wello horld'

有没有更有效和优雅的方式来做到这一点?上面的代码只是将所有内容读入内存,对其进行编码,而不是分块流式传输数据。

例如,对于反向问题 (BytesIO -> StringIO),存在一个类 - io.TextIOWrapper 正是这样做的(参见 answer)

【问题讨论】:

“更优雅”是否包括在没有这样的批量复制的情况下自己实现它? 我希望有更好的东西,如果不是这样应该比天真的方法更好,所以是的。 请注意,在您要求 BytesIO -> StringIO 的原始问题和更新 StringIO -> BytesIO 中。示例继续 BytesIO -> StringIO。 【参考方案1】:

有趣的是,虽然这个问题看起来很合理,但要找出我需要将StringIO 转换为BytesIO 的实际原因并不容易。两者基本上都是缓冲区,您通常只需要其中一个就可以对字节或文本进行一些额外的操作。

我可能错了,但我认为您的问题实际上是当您想要传递给它的某些代码需要一个文本文件时,如何使用 BytesIO 实例。

在这种情况下,这是一个常见问题,解决方案是codecs模块。

使用它的两种常见情况如下:

编写要读取的文件对象

In [16]: import codecs, io

In [17]: bio = io.BytesIO(b'qwe\nasd\n')

In [18]: StreamReader = codecs.getreader('utf-8')  # here you pass the encoding

In [19]: wrapper_file = StreamReader(bio)

In [20]: print(repr(wrapper_file.readline()))
'qwe\n'

In [21]: print(repr(wrapper_file.read()))
'asd\n'

In [26]: bio.seek(0)
Out[26]: 0

In [27]: for line in wrapper_file:
    ...:     print(repr(line))
    ...:
'qwe\n'
'asd\n'

编写要写入的文件对象

In [28]: bio = io.BytesIO()

In [29]: StreamWriter = codecs.getwriter('utf-8')  # here you pass the encoding

In [30]: wrapper_file = StreamWriter(bio)

In [31]: print('жаба', 'цап', file=wrapper_file)

In [32]: bio.getvalue()
Out[32]: b'\xd0\xb6\xd0\xb0\xd0\xb1\xd0\xb0 \xd1\x86\xd0\xb0\xd0\xbf\n'

In [33]: repr(bio.getvalue().decode('utf-8'))
Out[33]: "'жаба цап\\n'"

【讨论】:

您需要BytesIO 而不是StringIO 的一个原因可能是使用upload_fileobj 将内存中的文件上传到S3 存储桶。更多信息here OutputStreamWriter 相当于Java 中请求的包装器。截至 2021 年初,Github 搜索产生了 100 万次使用。那是为了它的“实用性”。 非常有趣的串'Жаба цап гадюку' =)【参考方案2】:

@foobarna answer可以通过继承一些io基类来改进

import io
sio = io.StringIO('wello horld')


class BytesIOWrapper(io.BufferedReader):
    """Wrap a buffered bytes stream over TextIOBase string stream."""

    def __init__(self, text_io_buffer, encoding=None, errors=None, **kwargs):
        super(BytesIOWrapper, self).__init__(text_io_buffer, **kwargs)
        self.encoding = encoding or text_io_buffer.encoding or 'utf-8'
        self.errors = errors or text_io_buffer.errors or 'strict'

    def _encoding_call(self, method_name, *args, **kwargs):
        raw_method = getattr(self.raw, method_name)
        val = raw_method(*args, **kwargs)
        return val.encode(self.encoding, errors=self.errors)

    def read(self, size=-1):
        return self._encoding_call('read', size)

    def read1(self, size=-1):
        return self._encoding_call('read1', size)

    def peek(self, size=-1):
        return self._encoding_call('peek', size)


bio = BytesIOWrapper(sio)
print(bio.read())  # b'wello horld'

【讨论】:

UTF8 并不总是单字节。这是不正确的:BytesIOWrapper(io.StringIO('אבגד')).read(1) 返回两个字节:b'\xd7\x90' @ShmulikA,是的,它返回 1 个“字符”。要真正返回 1 字节的“中间”缓冲区,应该实现【参考方案3】:

将字符流转换为字节流可能是一个普遍有用的工具,所以这里是:

import io

class EncodeIO(io.BufferedIOBase):
  def __init__(self,s,e='utf-8'):
    self.stream=s               # not raw, since it isn't
    self.encoding=e
    self.buf=b""                # encoded but not yet returned
  def _read(self,s): return self.stream.read(s).encode(self.encoding)
  def read(self,size=-1):
    b=self.buf
    self.buf=b""
    if size is None or size<0: return b+self._read(None)
    ret=[]
    while True:
      n=len(b)
      if size<n:
        b,self.buf=b[:size],b[size:]
        n=size
      ret.append(b)
      size-=n
      if not size: break
      b=self._read(min((size+1024)//2,size))
      if not b: break
    return b"".join(ret)
  read1=read

显然write 可以对称地定义以解码输入并将其发送到底层流,但您必须处理仅对字符的一部分有足够的字节。

【讨论】:

@ShmulikA:永远循环,甚至;编辑。当我重写缓冲时(在发布之前),我忘记了break【参考方案4】:

正如一些人指出的,您需要自己进行编码/解码。

但是,您可以通过一种优雅的方式实现此目的 - 为 string =&gt; bytes 实现您自己的 TextIOWrapper

这是一个示例:

class BytesIOWrapper:
    def __init__(self, string_buffer, encoding='utf-8'):
        self.string_buffer = string_buffer
        self.encoding = encoding

    def __getattr__(self, attr):
        return getattr(self.string_buffer, attr)

    def read(self, size=-1):
        content = self.string_buffer.read(size)
        return content.encode(self.encoding)

    def write(self, b):
        content = b.decode(self.encoding)
        return self.string_buffer.write(content)

产生如下输出:

In [36]: bw = BytesIOWrapper(StringIO("some lengt˙˚hyÔstring in here"))

In [37]: bw.read(15)
Out[37]: b'some lengt\xcb\x99\xcb\x9ahy\xc3\x94'

In [38]: bw.tell()
Out[38]: 15

In [39]: bw.write(b'ME')
Out[39]: 2

In [40]: bw.seek(15)
Out[40]: 15

In [41]: bw.read()
Out[41]: b'MEring in here'

希望它能清除你的想法!

【讨论】:

read(size) 必须读取 size 字节。但是,len(bw.read(15))18 @FilipDimitrovski 确实如此。那是因为您说“读取 15 个字节”,而实际上它读取的是“15 个字符串字符”,其中一些恰好是 2 个字节长,因此是“18 个长度”。我没有说它是完美的,但至少它没有破坏编码(通过将有效的 utf-8 字符拆分为 2)。那是一个示例,可以通过添加更多检查或更多方法(readline、上下文管理器等)来改进【参考方案5】:

我有完全相同的需求,所以我在 nr.utils.io 包中创建了一个 EncodedStreamReader 类。它还解决了实际读取请求的字节数而不是包装流中的字符数的问题。

$ pip install 'nr.utils.io>=0.1.0,<1.0.0'

示例用法:

import io
from nr.utils.io.readers import EncodedStreamReader
fp = EncodedStreamReader(io.StringIO('ä'), 'utf-8')
assert fp.read(1) == b'\xc3'
assert fp.read(1) == b'\xa4'

【讨论】:

【参考方案6】:

您的示例中的bio_io.BytesIO 类对象。 您已经使用了 2 次 read() 函数。

我想出了bytes 转换和一个read() 方法:

sio = io.StringIO('wello horld')
b = bytes(sio.read(), encoding='utf-8')
print(b)

但第二种变体应该更快:

sio = io.StringIO('wello horld')
b = sio.read().encode()
print(b)

【讨论】:

以上是关于将 io.StringIO 转换为 io.BytesIO的主要内容,如果未能解决你的问题,请参考以下文章

Python 2.x到3.x转换的代码在IO.StringIO转换中失败

如何在Python 3中将QImage(QPixmap)转换为PIL图像?

如何将 io.StringIO() 与 csv 模块一起使用?

使用 Pandas Excelwriter 写入 StringIO 对象?

AttributeError: type object '_io.StringIO' has no attribute 'StringIO'

StringIO函数