【问题标题】:Reading Unicode file data with BOM chars in Python在 Python 中使用 BOM 字符读取 Unicode 文件数据
【发布时间】:2012-11-15 11:00:28
【问题描述】:

我正在使用 Python 读取一系列源代码文件并遇到 unicode BOM 错误。这是我的代码:

bytes = min(32, os.path.getsize(filename))
raw = open(filename, 'rb').read(bytes)
result = chardet.detect(raw)
encoding = result['encoding']

infile = open(filename, mode, encoding=encoding)
data = infile.read()
infile.close()

print(data)

如您所见,我正在使用chardet 检测编码,然后读取内存中的文件并尝试打印它。打印语句在包含 BOM 的 Unicode 文件上失败,并出现错误:

UnicodeEncodeError:“charmap”编解码器无法对位置 0-2 中的字符进行编码:
字符映射到

我猜它正在尝试使用默认字符集解码 BOM,但它失败了。如何从字符串中删除 BOM 以防止这种情况发生?

【问题讨论】:

  • 只是想知道,当数据以 UTF-8 BOM 开头时,chardet 作为编码返回什么?似乎这将是一个很大的暗示,编码是 UTF-8 :^)
  • @MarkTolonen: it was a bugfixed now

标签: python unicode


【解决方案1】:

没有理由检查 BOM 是否存在,utf-8-sig 会为您管理它,如果 BOM 不存在,其行为与 utf-8 完全相同:

# Standard UTF-8 without BOM
>>> b'hello'.decode('utf-8')
'hello'
>>> b'hello'.decode('utf-8-sig')
'hello'

# BOM encoded UTF-8
>>> b'\xef\xbb\xbfhello'.decode('utf-8')
'\ufeffhello'
>>> b'\xef\xbb\xbfhello'.decode('utf-8-sig')
'hello'

在上面的示例中,您可以看到utf-8-sig 正确解码给定字符串,而不管 BOM 是否存在。如果您认为您正在阅读的文件中可能存在 BOM 字符的可能性很小,请使用 utf-8-sig 而不必担心

【讨论】:

  • 直到现在我才遇到 utf-8-sig --- 谢谢!剥离 BOM 不是编码更明显价值的默认行为,有什么好的理由吗?我的意思是,真的有人想在从文本文件中读取的字符串中查看 BOM 吗?
  • @AdamF 我认为utf-8utf-8-sig之间的想法是不要有意外的行为/魔法。我很高兴 Python utf-8 按原样解码文件,BOM 是文件中的一个字符,因此保留它是有意义的。我也很高兴utf-8-sig 自动处理剥离它。虽然我不知道有人想要 BOM 的情况,但我确信存在用例。通过这两种编码,我们可以决定自己的预期行为。
【解决方案2】:

在解码 UTF-16 而非 UTF-8 时,BOM 字符应自动剥离,除非您明确使用 utf-8-sig 编码。你可以试试这样的:

import io
import chardet
import codecs

bytes = min(32, os.path.getsize(filename))
raw = open(filename, 'rb').read(bytes)

if raw.startswith(codecs.BOM_UTF8):
    encoding = 'utf-8-sig'
else:
    result = chardet.detect(raw)
    encoding = result['encoding']

infile = io.open(filename, mode, encoding=encoding)
data = infile.read()
infile.close()

print(data)

【讨论】:

  • 有趣的是chardet 不会自动执行此操作。
  • 好的,我这样做了,它似乎工作正常,但是 Python 抛出一个奇怪的错误,抱怨 \u2019(右单引号)无法使用 utf-8-sig 解码。我该如何处理呢?
  • 没关系,我知道是因为控制台不支持该字符。问题解决了。
  • +1 on @MarkRansom 评论:有人知道为什么 chardet 不自动执行此操作吗?
  • @StephenJ.Fuhry WDYM 它不会影响输入流吗?它是输入流的一部分。如果将输入流解析为 UTF-* 的应用程序不理解 BOM 标记,则会产生一个奇怪的字符。如果将输入流解析为 UTF-* 的应用程序理解 BOM 标记,那么您所描述的就会发生。在这种情况下,问题正是应用程序不理解 BOM。所以你描述的不会发生。这正是问题所在。
【解决方案3】:

我根据 Chewie 的回答编写了一个漂亮的基于 BOM 的检测器。在数据可以是已知的本地编码或带有 BOM 的 Unicode(这是文本编辑器通常产生的)的常见用例中就足够了。更重要的是,与chardet 不同的是,它不会做任何随机猜测,所以它给出了可预测的结果:

def detect_by_bom(path, default):
    with open(path, 'rb') as f:
        raw = f.read(4)    # will read less if the file is smaller
    # BOM_UTF32_LE's start is equal to BOM_UTF16_LE so need to try the former first
    for enc, boms in \
            ('utf-8-sig', (codecs.BOM_UTF8,)), \
            ('utf-32', (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE)), \
            ('utf-16', (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
        if any(raw.startswith(bom) for bom in boms):
            return enc
    return default

【讨论】:

【解决方案4】:

chardet detects BOM_UTF8 automatically2.3.0 version released on Oct 7, 2014:

#!/usr/bin/env python
import chardet # $ pip install chardet

# detect file encoding
with open(filename, 'rb') as file:
    raw = file.read(32) # at most 32 bytes are returned
    encoding = chardet.detect(raw)['encoding']

with open(filename, encoding=encoding) as file:
    text = file.read()
print(text)

注意:chardet 可能会返回将 BOM 留在文本中的 'UTF-XXLE''UTF-XXBE' 编码。 'LE', 'BE' 应该被剥离以避免它——尽管此时更容易自己检测 BOM,例如 @ivan_pozdeev's answer

为避免在将 Unicode 文本打印到 Windows 控制台时出现UnicodeEncodeError,请参阅Python, Unicode, and the Windows console

【讨论】:

  • 我错了还是open实际上没有encoding关键字参数?
  • @YanFoto:它在 Python 3 上。在旧版本上使用 io.open
【解决方案5】:

我发现其他答案过于复杂。有一种更简单的方法,不需要下降到二进制文件 I/O 的低级习语,不依赖不属于 Python 标准库的字符集启发式 (chardet),并且不'不需要在 UTF-16 家族中似乎没有类似物的罕见替代编码签名(utf-8-sig 与常见的utf-8)。

我发现的最简单的方法是处理 Unicode 中的 BOM 字符,并让编解码器完成繁重的工作。只有一个 Unicode byte order mark,因此一旦将数据转换为 Unicode 字符,确定它是否存在和/或添加/删除它很容易。读取具有可能 BOM 的文件:

BOM = '\ufeff'
with open(filepath, mode='r', encoding='utf-8') as f:
    text = f.read()
    if text.startswith(BOM):
        text = text[1:]

这适用于所有有趣的 UTF 编解码器(例如 utf-8utf-16leutf-16be、...),不需要额外的模块,也不需要下拉到二进制文件处理或特定的codec 常量。

要编写 BOM:

text_with_BOM = text if text.startswith(BOM) else BOM + text
with open(filepath, mode='w', encoding='utf-16be') as f:
    f.write(text_with_BOM)

这适用于任何编码。 UTF-16 大端只是一个例子。

顺便说一句,这不是解雇chardet。当您不知道文件使用什么编码时,它会有所帮助。只是不需要添加/删除 BOM。

【讨论】:

  • 当文本文件使用 utf-16 LE 时,这对我不起作用。读取文件时,我得到“UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte”。
  • @criddell 您是否明确使用了上面的代码?如果是这样,您可能已经尝试使用utf-8 编解码器读取utf-16be 编码的文件。上面的示例展示了用于显示广度的两种编码。实际上,如果您使用utf-16be 编码编写文件,则还必须使用相同的编码读取该文件。技术经过测试并且确实有效。 Example here
  • 我明白你现在在说什么了。您的第一段代码误导了我。在其中,您正在以 utf-8 格式读取文件,然后在开始时查找 utf-16 BOM。您链接到的示例代码不同。对于我的项目,我事先不知道编码,所以你的答案不适合我。
  • BOM 管理的解决方案与不知道您正在阅读什么编码无关(这本身就是一个棘手的问题)。但是,它们可以组合。参见例如this function
  • 此页面上最简单,因此也是最好的解决方案。
【解决方案6】:

@ivan_pozdeev 对字符串/异常(而不是文件)的回答的变体。我正在处理填充在 python 异常中的 unicode HTML 内容(请参阅http://bugs.python.org/issue2517

def detect_encoding(bytes_str):
  for enc, boms in \
      ('utf-8-sig',(codecs.BOM_UTF8,)),\
      ('utf-16',(codecs.BOM_UTF16_LE,codecs.BOM_UTF16_BE)),\
      ('utf-32',(codecs.BOM_UTF32_LE,codecs.BOM_UTF32_BE)):
    if (any(bytes_str.startswith(bom) for bom in boms): return enc
  return 'utf-8' # default

def safe_exc_to_str(exc):
  try:
    return str(exc)
  except UnicodeEncodeError:
    return unicode(exc).encode(detect_encoding(exc.content))

另外,这个更简单的代码可以轻松删除非 ascii 字符:

def just_ascii(str):
  return unicode(str).encode('ascii', 'ignore')

【讨论】:

  • 您不应该在内存中的字节串中看到 BOM(它应该在解码文件的代码中被剥离)。您的默认值 (utf-8) 可能会在解码期间引发异常。 BOM 不保证编码会成功。请改用errors='backslashreplace'。无关:(1)不要使用裸except:它捕获太多,甚至KeyboardInterrupt。 (2) 不要使用` and bracket instead for enc, boms in [...]:`
  • @J.F.Sebastian - 我切换到“异常除外”。我不确定我是否理解您的#2 反馈。 FWIW,我看到来自 HTML 的 BOM 字符通过网络传输,随后被填充到 python 异常中。
  • (1) 如果您希望输入字符汤,那么encode() 更有可能在异常处理程序中引发异常 (2) 不要丢失异常信息:使用 @ 987654329@ 而不是'ignore' 错误处理程序 (3) 我的意思是你可以使用[] 括号而不是反斜杠来将for-loop 中的表达式分成多行。
【解决方案7】:

如果您想编辑文件,您需要知道使用了哪个 BOM。此版本的@ivan_pozdeev 答案返回编码和可选 BOM:

def encoding_by_bom(path, default='utf-8') -> Tuple[str, Optional[bytes]]:
    """Adapted from https://stackoverflow.com/questions/13590749/reading-unicode-file-data-with-bom-chars-in-python/24370596#24370596 """

    with open(path, 'rb') as f:
        raw = f.read(4)    # will read less if the file is smaller
    # BOM_UTF32_LE's start is equal to BOM_UTF16_LE so need to try the former first
    for enc, boms in \
            ('utf-8-sig', (codecs.BOM_UTF8,)), \
            ('utf-32', (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE)), \
            ('utf-16', (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
        for bom in boms:
            if raw.startswith(bom):
                return enc, bom
    return default, None

【讨论】:

    猜你喜欢
    • 2014-02-17
    • 2017-09-07
    • 1970-01-01
    • 2023-03-20
    • 2011-08-22
    • 1970-01-01
    • 2015-09-25
    • 2012-02-04
    • 1970-01-01
    相关资源
    最近更新 更多