前言
在爬虫、文件解析、http响应处理场景里,经常会遇到一种诡异问题:肉眼看文本完全正常,但程序判断逻辑莫名失效。典型案例:html页面明明是标准文档,却因为开头多出3个字节,被误判为非html类型。罪魁祸首就是 utf-8 bom。
很多开发者有误区:utf-8本身不需要bom。utf-8 bom只是微软体系(windows记事本等)为了标记编码加入的约定,并不是utf-8标准强制要求。它的二进制为 b'\xef\xbb\xbf',占3字节,放在文件最前面。
什么是 utf-8 bom
- bom:byte order mark,字节序标记。原本是为utf-16、utf-32区分大小端设计。
- utf-8 不存在字节序问题,但 windows 记事本保存utf-8文件时,默认会写入
0xef 0xbb 0xbf前缀,也就是utf-8 bom。 - 这3个字节不是正文内容,只是编码标记。但在二进制读取时,会原样读到内存,干扰解析逻辑。
举个例子:
# 带bom的html原始二进制 data = b'\xef\xbb\xbf<!doctype html><html>...</html>' print(data.lstrip()) # 输出还是 b'\xef\xbb\xbf<!doctype html>...' # lstrip() 只能删除空白字符,不会移除bom字节!
这就是我在http文件类型检测函数踩的坑:content-type 返回text/html,但正文头部带bom,前缀匹配直接失败,识别成txt。
python 处理 utf-8 bom 的几种方案
方案1:二进制预处理(推荐,爬虫场景首选)
先判断是否以bom字节开头,如果是直接切片去掉前3字节。适合直接读取bytes的场景,比如requests响应原始内容。
def strip_utf8_bom(data: bytes) -> bytes:
if data.startswith(b'\xef\xbb\xbf'):
return data[3:]
return data
raw = b'\xef\xbb\xbfhello world'
res = strip_utf8_bom(raw)
print(res) # b'hello world'
适合我们之前写的detect_file_type文件类型判断逻辑,二进制阶段清除bom,不需要解码,性能更好,也避免解码异常。
方案2:使用utf-8-sig编码(文本读取首选)
python内置编码 utf-8-sig,会自动识别并丢弃utf-8 bom,不需要手动写切片逻辑。
# 读取本地文件
with open("test.html", "r", encoding="utf-8-sig") as f:
text = f.read()
# bytes转字符串时使用
raw_bytes = b'\xef\xbb\xbf<!doctype html>'
text = raw_bytes.decode("utf-8-sig")
print(text)
注意区分:
utf-8:不会自动移除bom,bom会变成\ufeff字符留在文本里;utf-8-sig:自动移除bom,得到干净文本。
方案3:字符串层面清除 \ufeff
如果已经解码成字符串,bom会变成unicode零宽字符 \ufeff,可以直接替换:
text_with_bom = "\ufeff<!doctype html>"
clean_text = text_with_bom.replace("\ufeff", "")
print(clean_text)
缺点:会扫描全文替换,只适合小文本;大文件/二进制场景不推荐。
实战场景:爬虫http响应中的bom
很多网站返回html时会带上utf-8 bom。用requests拿到response.content是原始bytes。
如果直接用原始字节做魔数、标签前缀判断,bom会干扰匹配。
就像前面文件类型检测代码,处理顺序建议:二进制先剔除bom,再去除空白,再做前缀匹配:
def strip_bom_and_whitespace(data: bytes) -> bytes:
# 第一步移除utf8 bom
if data.startswith(b'\xef\xbb\xbf'):
data = data[3:]
# 第二步去除前置空白:空格、换行、回车、tab
return data.lstrip(b' \r\n\t')
# 测试bom+换行场景
html_bin = b'\xef\xbb\xbf\n <html><body></body></html>'
clean_bin = strip_bom_and_whitespace(html_bin)
print(clean_bin.startswith(b'<html')) # true
常见踩坑点总结
bytes.lstrip()不能自动删除bom,bom不属于空白字符;utf-8和utf-8-sig是两个编码,打开文件选错就会残留\ufeff;- 不要盲目全局替换
\ufeff,极少数业务场景下这个字符是业务正文,直接替换会破坏内容; - 爬虫文件类型识别,优先在bytes阶段处理bom,不要等到解码后处理,避免不必要的解码开销;
- utf-8 bom在linux服务端极少出现,大多是windows生成的文件或者windows服务器输出。
什么时候保留bom,什么时候去掉?
- 需要移除:网页解析、json解析、xml解析、文件类型识别、文本对比。
- 保留bom:需要兼容windows记事本,输出文件要让记事本默认识别为utf-8时,可以手动写入bom。
写入带bom的utf8文件示例:
content = "测试文本"
with open("out.txt", "wb") as fp:
fp.write(b'\xef\xbb\xbf')
fp.write(content.encode("utf-8"))
小结
utf-8 bom只是历史遗留标记,不是utf-8标准一部分。在python里分两种场景处理:
- 操作bytes原始二进制:判断前缀
b'\xef\xbb\xbf'切片去除; - 读取文本字符串:直接使用
utf-8-sig编码自动处理。
在爬虫、文件检测这类底层二进制判断场景,推荐bytes预处理,可以避免解码带来的性能损耗与编码异常,也是我在http文件类型识别函数里最终采用的方案。
到此这篇关于python如何处理utf-8 bom编码标记的文章就介绍到这了,更多相关python处理utf-8 bom内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论