带BOM的UTF-8 HTML和CSS文件(以及如何用Python去除BOM)
首先,给大家一些背景信息:我正在用Python开发一个网页应用程序。我的所有(文本)文件现在都是用带有BOM的UTF-8格式存储的。这包括我所有的HTML模板和CSS文件。这些资源作为二进制数据(包括BOM)存储在我的数据库里。
当我从数据库中取出模板时,我用 template.decode('utf-8')
来解码它们。当HTML内容到达浏览器时,HTTP响应的开头会有BOM。这在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。
谢谢!
4 个回答
之前被接受的答案是错的。
u'\ufffe'
不是一个有效的字符。如果你在一个unicode字符串中看到了它,那说明有人搞错了。
BOM(也叫零宽不换行空格)是 u'\ufeff'
。
>>> UNICODE_BOM = u'\N{ZERO WIDTH NO-BREAK SPACE}'
>>> UNICODE_BOM
u'\ufeff'
>>>
可以阅读 这个(按 Ctrl-F 搜索 BOM),还有 这个 和 这个(同样按 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库中似乎没有合适的命名常量。
可惜的是,codecs模块只提供了“一个陷阱和一个幻觉”:
>>> 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,你需要检查两种不同的BOM用于UTF-16,至少两种不同的BOM用于UTF-32。如果每种编码只有一种方式,那就不需要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的长度(如果有的话)。
如果你比较谨慎,可以考虑另外两种(非标准的)UTF-32编码顺序,但Python并没有提供它们的编码,我也从未听说过实际出现过,所以我就不管了。
解码后,检查第一个字符是否是字节顺序标记(BOM):
if u.startswith(u'\ufeff'):
u = u[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标记,如果文件里没有这个标记,它也能正常工作。