【问题标题】:Reading a multibyte text file in Windows - how does it detect newlines? (Python 2)在 Windows 中读取多字节文本文件 - 它如何检测换行符? (Python 2)
【发布时间】:2016-07-15 17:50:30
【问题描述】:

我认为这是 Unicode 世界的警告 -> 在不知道编码是什么的情况下,您无法正确将字节流处理为写入。如果您采用编码,那么您可能会显示有效但不正确的字符。

这是一个测试 - 一个带有文字的文件:

hi1
hi2

以 2 字节 Unicode 编码存储在磁盘上:

Windows 换行符以\r\n 存储为四字节序列0D 00 0A 00。在 Python 2 中使用默认编码打开它,我认为它需要 ASCII 1-byte-per-character(或只是一个字节流),它显示:

>>> open('d:/t/hi2.txt').readlines()
['\xff\xfeh\x00i\x001\x00\r\x00\n', 
 '\x00h\x00i\x002\x00']

它不是将两个字节解码为一个字符,但是四个字节的行结束序列被检测为两个字符,文件已正确拆分为两行 .

Windows 以“文本模式”打开文件,如下所述:Difference between files writen in binary and text mode

并将这些行提供给 Python。但是,根据问题顶部的警告,Windows 是如何知道文件是多字节编码的,并在不被告知的情况下查找四字节的换行符?

  • Windows 是否猜测,具有启发式 - 因此可能是错误的?
  • Unicode 的设计是否更巧妙,使 Windows 换行符模式在各种编码中明确无误?
  • 我的理解是否错误,是否有正确的方法来处理任何文本文件而无需事先告知编码?

【问题讨论】:

  • Windows 没有“文本模式”的概念。您说的是 C 运行时,但默认为 ANSI 文本模式,它不会检测到 UTF-16 \r\x00\n\x00 序列。您实际看到的是 Python 2 中 file.readlines 方法的实现。请参见第 1717 行、p = (char *)memchr(buffer+nfilled, '\n', nread),然后是第 1749 行、line = PyString_FromStringAndSize(q, p-q)。它天真地消耗了一个 \n 字符,这就是实际的 UTF-16LE \n\x00 被拆分的原因。
  • Windows 没有“文本模式”的概念 - fopen() at MSDN - “在文本模式下,回车-换行组合在输入时被转换为单个换行,和换行字符在输出时转换为回车换行组合。当 Unicode 流 I/O 函数在文本模式(默认)下运行时,源或目标流被假定为多字节字符序列。”。但是,Unicode 换行符四边形被 Python 破坏是有道理的,这可能是我的答案。
  • 微软的_openfopen_readfread等都是C运行时函数。它们本质上不是 Windows 文件 I/O 的一部分,即CreateFileReadFile 等。使用 Microsoft 的 CRT 是可选的。您可以使用没有“文本模式”的不同 CRT,或者您可以直接使用 Windows API。碰巧 Windows Python 是使用 MSVC 构建的,并使用 CRT 的低 I/O 和标准 I/O 运行时函数来促进与 Linux 和 OS X 等 POSIX 操作系统的跨平台兼容性。
  • @eryksun 然后,如果您将指向 Python 源的链接放在完整答案而不是评论中,我会将其标记为已接受。谢谢。

标签: python windows unicode


【解决方案1】:

这种情况下的结果与 Windows 或 Microsoft 的 C 运行时的标准 I/O 实现无关。如果您在 Linux 系统上使用 Python 2 进行测试,您将看到相同的结果。这就是file.readlines(2.7.12 源链接)在 Python 2 中的工作方式。参见第 1717 行、p = (char *)memchr(buffer+nfilled, '\n', nread),然后是第 1749 行、line = PyString_FromStringAndSize(q, p-q)。它天真地消耗了一个 \n 字符,这就是实际的 UTF-16LE \n\x00 序列被拆分的原因。

如果您使用 Python 2 的通用换行符模式打开文件,例如open('d:/t/hi2.txt', 'U')\r\x00 序列会天真地转换为 \n\x00readlines 的结果将改为 ['\xff\xfeh\x00i\x001\x00\n, \x00\n', '\x00h\x00i\x002\x00']

因此,您最初的假设是正确的。您需要知道编码,或者至少知道在文件开头查找 Unicode BOM(字节顺序标记),例如 \xff\xfe,表示 UTF-16LE(小端序)。为此,我建议在 Python 2.7 中使用 io 模块,因为它可以正确处理换行符转换。另一方面,codecs.open 要求包装文件采用二进制模式,并忽略通用换行模式:

>>> codecs.open('test.txt', 'U', encoding='utf-16').readlines()
[u'hi1\r\n', u'hi2']

io.open 返回一个内置支持通用换行符的TextIOWrapper

>>> io.open('test.txt', encoding='utf-16').readlines()
[u'hi1\n', u'hi2']

关于微软的 CRT,它默认为 ANSI 文本模式。 Microsoft 的 ANSI 代码页是 ASCII 的超集,因此 CRT 的换行转换将适用于使用 ASCII 兼容编码(如 UTF-8)编码的文件。另一方面,ANSI 文本模式不适用于 UTF-16 编码文件,即它不会删除 UTF-16LE BOM (\xff\xfe) 并且不会翻译换行符:

>>> open('test.txt').read()
'\xff\xfeh\x00i\x001\x00\r\x00\n\x00h\x00i\x002\x00' 

因此,对 UTF-16 编码文件使用标准 I/O 文本模式需要非标准 ccs 标志,例如fopen("d:/t/hi2.txt", "rt, ccs=UNICODE")。 Python 不支持此 Microsoft 对开放 mode 的扩展,但它确实使 CRT 的低 I/O (POSIX) _open_read 函数在 os 模块中可用。虽然这可能会让 POSIX 程序员感到惊讶,但微软的低 I/O API 也支持文本模式,包括 Unicode。例如:

>>> O_WTEXT = 0x10000
>>> fd = os.open('test.txt', os.O_RDONLY | O_WTEXT)
>>> os.read(fd, 100)
'h\x00i\x001\x00\n\x00h\x00i\x002\x00'
>>> os.close(fd)

O_WTEXT 常量不能直接在 Windows Python 中使用,因为使用 os.fdopen 以 Python file 的形式打开具有此模式的文件描述符是不安全的。 CRT 期望所有宽字符缓冲区是 wchar_t 大小的倍数,即 2 的倍数。否则,它会调用无效的参数处理程序来终止进程。例如(使用 cdb 调试器):

>>> fd = os.open('test.txt', os.O_RDONLY | O_WTEXT)
>>> os.read(fd, 7)
ntdll!NtTerminateProcess+0x14:
00007ff8`d9cd5664 c3              ret
0:000> k8
Child-SP          RetAddr           Call Site
00000000`005ef338 00007ff8`d646e219 ntdll!NtTerminateProcess+0x14
00000000`005ef340 00000000`62db5200 KERNELBASE!TerminateProcess+0x29
00000000`005ef370 00000000`62db52d4 MSVCR90!_invoke_watson+0x11c
00000000`005ef960 00000000`62db0cff MSVCR90!_invalid_parameter+0x70
00000000`005ef9a0 00000000`62db0e29 MSVCR90!_read_nolock+0x76b
00000000`005efa40 00000000`1e056e8a MSVCR90!_read+0x10d
00000000`005efaa0 00000000`1e0c3d49 python27!Py_Main+0x12a8a
00000000`005efae0 00000000`1e1146d4 python27!PyCFunction_Call+0x69

这同样适用于_O_UTF8_O_UTF16

【讨论】:

  • 那是全面的,结论性的。我应该注意到\n\x00 被分成两半并怀疑那里有什么东西。 (现在我想知道,如果 Windows 没有文本模式的概念,那么 Windows 怎么会有一个固定的文本文件行结束序列 - 它只是使用 \r\n 的约定吗?或者是你的声明“微软的低 I/O API 也支持文本模式”自从你发表评论后发生了变化?)
  • 微软的 CRLF 约定是从 CP/M 继承的,CP/M 是从 TOPS-10 继承的。 Microsoft 的语言库,无论是原生 C/C++ 还是基于 .NET,通常以 hard code CRLF 作为行尾,但 .NET TextWriter 确实允许设置 NewLine。但是,此 Windows 约定并未在操作系统中硬编码。 Windows 本身的 I/O API 是二进制的。它没有文本“行”的概念。许多程序在 Windows 上使用 Unix LF 约定。
【解决方案2】:

首先要做的事:以文本形式打开文件,指明正确的编码,并以显式文本模式打开。

如果您仍在使用 Python 2.7,请使用 codecs.open 而不是 open。在 Python 3.x 中,只需使用 open:

import codecs
myfile = codecs.open('d:/t/hi2.txt', 'rt', encoding='utf-16')

而且你应该能够处理它。

其次,likley 发生了什么:由于您没有指定以二进制模式打开文件,Windows 以“文本”模式打开它 - Windows 确实知道编码,因此可以找到 @987654324 @ 行中的序列 - 它分别读取行,执行行尾翻译 - 使用 utf-16 - 并将这些 utf-16 字节传递给 Python。

在 Python 端,您可以使用这些值,只需将它们解码为文本:

[line.decode("utf-16" for line in open('d:/t/hi2.txt')]

而不是

 open('d:/t/hi2.txt').readlines()

【讨论】:

  • 这是个好建议,但这与我的问题无关,我不是在问如何在 Python 2 中处理 Unicode 文件,我是在问即使使用一种使“不可能”的编码。 - Windows does know about the encoding - 怎么样?它怎么可能知道?如果它知道,为什么 Python 不知道?
  • 我已经在“第二...”开头的段落中解决了这个问题
猜你喜欢
  • 2010-10-23
  • 1970-01-01
  • 2015-11-23
  • 1970-01-01
  • 2010-09-14
  • 1970-01-01
  • 2011-09-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多