如何在Python中模拟以文本模式打开的文件

10

我正在研究一些用于测试处理文件的代码的方法,但我希望编写一些仅依赖源文件中特定字符串而不是在文件系统中具有特定文件的测试。

我知道可以通过io.StringIO提供类似于file的流接口来操作字符串。

问题是操作不遵循相同的语义。例如,使用file.seek()file.read()的组合将根据文件对象是来自open()还是来自io.StringIO以及包含非ASCII字符的字符串而产生不同的结果:

import io

#      'abgdezhjiklmnxoprstufqyw'
text = 'αβγδεζηθικλμνξoπρστυφχψω'


with open('test.txt', 'w') as file_obj:
    file_obj.write(text)


with open('test.txt', 'r') as file_obj:
    file_obj.seek(8)
    print(file_obj.read(8))
# εζηθικλμ


with io.StringIO(text) as file_obj:
    file_obj.seek(8)
    print(file_obj.read(8))
# ικλμνξoπ

对于仅包含ASCII字符的字符串,不存在此问题:

import io

text = 'abgdezhjiklmnxoprstufqyw'


with open('test.txt', 'w') as file_obj:
    file_obj.write(text)


with open('test.txt', 'r') as file_obj:
    file_obj.seek(8)
    print(file_obj.read(8))
# iklmnxop


with io.StringIO(text) as file_obj:
    file_obj.seek(8)
    print(file_obj.read(8))
# iklmnxop

显然,这是由于在使用 open() 打开的文件中,.seek() 遵循了 bytes 语义的 offset 参数,而对于 io.StringIO,它遵循了 str 语义。

我确实明白出于性能方面的考虑,即使以文本模式打开文件,也不实用按照 str 语义来进行 seek()

因此,我的问题是:如何获得一个等效的 io.StringIO(),并且具有按照 bytes 语义进行 seek 的方法?我需要自己覆盖 io.StringIO 吗,还是有更好的方法?


1
在 TextIO 文件上执行 seek(8) 会导致未定义的行为。请参阅 https://docs.python.org/3/library/io.html#io.TextIOBase.seek 所以你真的不应该这样做。 - PM 2Ring
1
我认为,如果你想测试你的代码,你可以写入/tmp/目录。不需要寻找复杂的解决方案。 - Alexandr Zayets
3个回答

7
你可以使用 BytesIOTextIOWrapper 来模拟真实文件的行为:
text = 'αβγδεζηθικλμνξoπρστυφχψω'

with io.BytesIO(text.encode('utf8')) as binary_file:
    with io.TextIOWrapper(binary_file, encoding='utf8') as file_obj:
        file_obj.seek(8)
        print(file_obj.read(8))
        # εζηθικλμ

谢谢!这正是我在寻找的。请注意,您也可以使用单个 with 块来实现此功能。值得一提的是,encoding 参数是可选的,就像 open() 一样。 - norok2
1
编码不是可选的,因为 str.encode 默认使用的编码与 TextIOWrapper 不同('utf-8' vs locale.getpreferredencoding(False))。 - Aran-Fey

3
您可以使用 BytesIO 代替:

import io

s = "αβγδεζηθικλμνξoπρστυφχψω"
with io.BytesIO(s.encode("utf-8")) as f:
    f.seek(8)
    print(f.read(8).decode("utf-8"))

1
这将输出εζηθ而不是εζηθικλμ - RomanPerekhrest
@RomanPerekhrestm,是的,它应该打印εζηθ,因为它是8个字节。 - Olvin Roght
2
根据实际数据,这可能效果不佳,因为UTF-8使用1到4个字节对字符进行编码。 - PM 2Ring
@PM2Ring,标准的TextIO可以很好地读取UTF-8字符串,但据我所知,作者需要不同的东西。 - Olvin Roght

0

对于字节缓冲区,有相应的io.BytesIO作为流实现。

但是,像这样在text模式下操作带有Unicode文本的文件

with open('test.txt', 'r') as file_obj:
    file_obj.seek(8)
    print(file_obj.read(8))  # εζηθικλμ

将读取8个字符而不是字节。 要从字节缓冲区中提取相同的文本片段,您需要预先提供一个字节序列的长度:

with open(dir_path + 'test.txt', 'r') as file_obj:
    file_obj.seek(8)
    text_part = file_obj.read(8)
    byte_len = len(text_part.encode('utf8'))
    print(byte_len)   # 16
    print(text_part)  # εζηθικλμ 


with io.BytesIO(bytes(text.encode('utf8'))) as file_obj:
    file_obj.seek(8)
    print(file_obj.read(byte_len).decode(encoding='utf8'))  # εζηθικλμ

否则,您可能会得到一个“意外”的文本片段:
with io.BytesIO(bytes(text.encode('utf8'))) as file_obj:
    file_obj.seek(8)
    print(file_obj.read(8).decode(encoding='utf8'))  # εζηθ

请查看我在问题评论中提供的文档。除非参数是由“tell”返回的值,否则在TextIO文件上调用“seek”将会导致未定义的行为。 - PM 2Ring

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接