【问题标题】:UTF-8 HTML and CSS files with BOM (and how to remove the BOM with Python)带有 BOM 的 UTF-8 HTML 和 CSS 文件(以及如何使用 Python 删除 BOM)
【发布时间】:2011-01-28 05:19:06
【问题描述】:

首先,一些背景知识:我正在使用 Python 开发一个 Web 应用程序。我的所有(文本)文件当前都以 UTF-8 格式存储在 BOM 中。这包括我所有的 HTML 模板和 CSS 文件。这些资源作为二进制数据(BOM 和所有)存储在我的数据库中。

当我从数据库中检索模板时,我使用template.decode('utf-8') 对它们进行解码。当 HTML 到达浏览器时,BOM 出现在 HTTP 响应正文的开头。这会在 Chrome 中产生一个非常有趣的错误:

Extra <html> encountered. Migrating attributes back to the original <html> element and ignoring the tag.

Chrome 似乎在看到 BOM 并将其误认为内容时会自动生成 <html> 标签,从而使真正的 <html> 标签出错。

那么,使用 Python,从我的 UTF-8 编码模板中删除 BOM 的最佳方法是什么(如果存在 - 我不能保证将来会这样做)?

对于其他基于文本的文件(如 CSS),主流浏览器会正确解释(或忽略)BOM 吗?它们是作为没有.decode('utf-8') 的纯二进制数据发送的。

注意:我使用的是 Python 2.5。

谢谢!

【问题讨论】:

    标签: python file utf-8 byte-order-mark


    【解决方案1】:

    既然你说:

    我所有的(文本)文件目前都在 与 BOM 一起以 UTF-8 存储

    然后使用“utf-8-sig”编解码器对其进行解码:

    >>> s = u'Hello, world!'.encode('utf-8-sig')
    >>> s
    '\xef\xbb\xbfHello, world!'
    >>> s.decode('utf-8-sig')
    u'Hello, world!'
    

    它会自动删除预期的 BOM,如果 BOM 不存在也能正常工作。

    【讨论】:

    • 哦!很不错!我会尽快尝试。
    • 运行良好(尽管 Chrome 无论如何都神秘地停止给出错误,即使使用我的旧(错误)代码 - 这就是我一次进行一大堆更改的结果)。
    【解决方案2】:

    解码后检查第一个字符是否为BOM:

    if u.startswith(u'\ufeff'):
      u = u[1:]
    

    【讨论】:

    • u'\ufffe' 是否会出现在非 UTF-8 文件的开头?在我的情况下(UTF-8),BOM 不会占用两个“字符”(读取:字节)吗?
    • u'\ufffe' 可以在任何 UTF 或 UCS 编码文件的开头找到。 BOM 是 UTF-8 中的三个字节,但它仍然是一个 Unicode 代码点。
    • 好的,所以为了弄清楚这一点,我需要先使用u = contents.decode('utf-8') 解码文件的字节内容,然后我才能使用你的方法,因为 BOM 是单个代码点。对吗?
    • @John:把数字混在一起说“完全错误”有点夸张,你不觉得吗?
    • @Ignacio:我仍然认为这个答案最适合我的情况,但是我建议您编辑答案以改用 u'\ufeff' 。这似乎是正确的顺序(当使用 Unicode 代码点时——实际编码字节的顺序取决于,这是 BOM 的全部点)。
    【解决方案3】:

    之前接受的答案是错误的。

    u'\ufffe' 不是字符。如果你把它放在一个 unicode 字符串中,那么有人已经把它塞满了。

    BOM(又名零宽度无间断空间)是 u'\ufeff'

    >>> UNICODE_BOM = u'\N{ZERO WIDTH NO-BREAK SPACE}'
    >>> UNICODE_BOM
    u'\ufeff'
    >>>
    

    读取this(Ctrl-F 搜索 BOM)和thisthis(Ctrl-F 搜索 BOM)。

    这是一个正确且拼写错误/抗脑力的答案:

    将您的输入解码为unicode_str。然后这样做:

    # If I mistype the following, it's very likely to cause a SyntaxError.
    UNICODE_BOM = u'\N{ZERO WIDTH NO-BREAK SPACE}'
    if unicode_str and unicode_str[0] == UNICODE_BOM:
        unicode_str = unicode_str[1:]
    

    奖励:与看似任意的十六进制文字集合相比,使用命名常量可以让您的读者更多地了解正在发生的事情。

    更新不幸的是,标准 Python 库中似乎没有合适的命名常量。

    唉,编解码器模块只提供“一个圈套和一个妄想”:

    >>> import pprint, codecs
    >>> pprint.pprint([(k, getattr(codecs, k)) for k in dir(codecs) if k.startswith('BOM')])
    [('BOM', '\xff\xfe'),   #### aarrgghh!! ####
     ('BOM32_BE', '\xfe\xff'),
     ('BOM32_LE', '\xff\xfe'),
     ('BOM64_BE', '\x00\x00\xfe\xff'),
     ('BOM64_LE', '\xff\xfe\x00\x00'),
     ('BOM_BE', '\xfe\xff'),
     ('BOM_LE', '\xff\xfe'),
     ('BOM_UTF16', '\xff\xfe'),
     ('BOM_UTF16_BE', '\xfe\xff'),
     ('BOM_UTF16_LE', '\xff\xfe'),
     ('BOM_UTF32', '\xff\xfe\x00\x00'),
     ('BOM_UTF32_BE', '\x00\x00\xfe\xff'),
     ('BOM_UTF32_LE', '\xff\xfe\x00\x00'),
     ('BOM_UTF8', '\xef\xbb\xbf')]
    >>>
    

    更新 2 如果您尚未对输入进行解码,并希望检查它的 BOM,您需要检查 两个 不同的 UTF-16 和 BOM UTF-32 至少有 两个 不同的 BOM。如果每种方法只有一种,那么您就不需要 BOM,对吗?

    这里逐字逐句地从我自己的代码中得到我的解决方案:

    def check_for_bom(s):
        bom_info = (
            ('\xFF\xFE\x00\x00', 4, 'UTF-32LE'),
            ('\x00\x00\xFE\xFF', 4, 'UTF-32BE'),
            ('\xEF\xBB\xBF',     3, 'UTF-8'),
            ('\xFF\xFE',         2, 'UTF-16LE'),
            ('\xFE\xFF',         2, 'UTF-16BE'),
            )
        for sig, siglen, enc in bom_info:
            if s.startswith(sig):
                return enc, siglen
        return None, 0
    

    输入 s 至少应该是输入的前 4 个字节。它返回可用于解码输入的后 BOM 部分的编码,加上 BOM 的长度(如果有)。

    如果您偏执,您可以允许另外 2 个(非标准)UTF-32 排序,但 Python 不为它们提供编码,我从未听说过实际发生,所以我不知道麻烦。

    【讨论】:

    • 我看不到这里使用的“零宽度无间隔空间”是如何比 u“\uFEFF”更清晰的,因为它也是 BOM(双关语)。它们都需要了解 BOM 的先验知识。
    • @Cameron:易读性来自于给你使用名称的任何常量,例如UNICODE_BOM。
    • @Cameron:我对 BOM 一无所知,但我知道什么是“零宽度不间断空间”,不知道什么是 u"\uFEFF"。后者也更难确定我是否输入正确,因为它的 8 个字符长度仅包含 3 个字母数字字符,其中两个非常相似。
    • @Vickie:在这种情况下,“零宽度不间断空间”根本没有用于表示零宽度不间断空间(它的目的完全不同 - 查找 BOM如果你很好奇),这就是为什么我发现按名称而不是按代码点使用它同样无济于事。 @John:你说得对,直接使用符号名称(如常量)而不是代码点是个好主意。
    • @Cameron:对 ZWNBSP 使用 \N 常量的意义在于,如果您不小心“弄乱了顺序”,您将立即收到 SyntaxError。 ZWNBSP 最初的用途现已弃用; “使用 U+2060 WORD JOINER 来表达单词连接语义比 ZWNBSP 更受欢迎,因为它不能与 BOM 混淆”。不幸的是,unicodedata 文件不包含允许u"\N{BYTE ORDER MARK}" 之类的映射。
    【解决方案4】:

    您可以使用类似的东西来删除 BOM:

    import os, codecs
    def remove_bom_from_file(filename, newfilename):
        if os.path.isfile(filename):
            # open file
            f = open(filename,'rb')
    
            # read first 4 bytes
            header = f.read(4)
    
            # check if we have BOM...
            bom_len = 0
            encodings = [ ( codecs.BOM_UTF32, 4 ),
                ( codecs.BOM_UTF16, 2 ),
                ( codecs.BOM_UTF8, 3 ) ]
    
            # ... and remove appropriate number of bytes    
            for h, l in encodings:
                if header.startswith(h):
                    bom_len = l
                    break
            f.seek(0)
            f.read(bom_len)
    
            # copy the rest of file
            contents = f.read() 
            nf = open(newfilename)
            nf.write(contents)
            nf.close()
    

    【讨论】:

    • 嗯,您不必在读取前 4 个字节后和测试 BOM 之前回退文件吗? f.seek(0).
    • @Konrad 我错过了,感谢您指出。无论如何,这不是生产代码:]。
    • 对我来说看起来不错(使用 seek(0) 修复),但是当我尝试切割 BOM 时,我已经将整个文件保存在内存中——内容的效率如何[2: ] (例如)在 Python 中?它会创建整个字符串的副本吗?
    • 如果我在读取文件时剥离 BOM,我会使用此方法,但我将在内存中已存在文件的情况下剥离 BOM。不过感谢您的回复!
    • 这个答案也有问题 读取文件时,您需要检查五个(至少)可能的 BOM,而不是三个。看我的回答。
    猜你喜欢
    • 2021-12-21
    • 1970-01-01
    • 2012-02-12
    • 2017-12-27
    • 2012-10-20
    • 2012-05-04
    相关资源
    最近更新 更多