Python处理UTF-8 BOM:爬虫文件类型识别失败解决
在爬虫和文件解析项目里,经常遇到一类“肉眼正常、程序判断异常”的问题:HTML 内容看起来是标准文档,文件类型识别却失败,被当成 txt 处理。排查后发现,正文最前面多了 3 个字节。这正是 UTF-8 BOM 造成的。一、UTF-8 BOM 是什么
BOM 全称 Byte Order Mark,字节序标记,最初用于 UTF-16、UTF-32 区分大小端。UTF-8 本身不存在字节序问题,标准也没有强制要求 BOM。但 Windows 记事本保存 UTF-8 文件时默认写入 0xEF 0xBB 0xBF,即 b'\xef\xbb\xbf',占 3 字节,位于文件最前面。
这 3 字节不是正文,只是编码标记。二进制读取时会被原样读入内存,干扰后续解析。例如:
data = b'\xef\xbb\xbf<!DOCTYPE html><html>...</html>'
print(data.lstrip())
# 输出还是 b'\xef\xbb\xbf<!DOCTYPE html>...'
# lstrip() 只能删除空白字符,不会移除BOM字节!
注意 lstrip() 只删除空白字符,BOM 不属于空白,所以不会被移除。在 HTTP 文件类型检测中,如果拿原始 bytes 做前缀匹配,就会直接失败,最终识别为 txt。
二、Python 处理 UTF-8 BOM 的三种方案
方案1:二进制预处理(推荐用于爬虫)
适合直接读取 bytes 的场景,比如 requests 的 response.content。先判断是否以 BOM 字节开头,是则切片去掉前 3 字节:
def strip_utf8_bom(data: bytes) -> bytes:
if data.startswith(b'\xef\xbb\xbf'):
return data
return data
raw = b'\xef\xbb\xbfhello world'
res = strip_utf8_bom(raw)
print(res)# b'hello world'
在文件类型判断逻辑里,二进制阶段清除 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()
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
# 第二步去除前置空白:空格、换行、回车、tab
return data.lstrip(b' \r\n\t')
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,什么时候去掉
需要移除 BOM 的场景:网页解析、JSON 解析、XML 解析、文件类型识别、文本对比。
需要保留 BOM 的场景:兼容 Windows 记事本,输出文件希望记事本默认识别为 UTF-8 时,可以手动写入 BOM:
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 预处理,可以避免解码性能损耗与编码异常,也能让文件类型识别逻辑稳定工作。
Re: Python处理UTF-8 BOM:爬虫文件类型识别失败解决
这个总结挺实用的,之前也踩过 bytes.lstrip() 去不掉 BOM 的坑,一直以为是空白字符的问题。按你给的顺序,先判断 b'\xef\xbb\xbf' 再切片,然后 lstrip 掉空格换行 tab,最后做前缀匹配,文件类型识别确实稳定很多。文本读取直接用 utf-8-sig 也很省事,能避开 \ufeff 残留。唯一要注意的就是别在全文里无脑 replace \ufeff,有些场景它可能真是正文。感谢分享,收藏了。Re: Python处理UTF-8 BOM:爬虫文件类型识别失败解决
这个总结很实用,UTF-8 BOM 确实很容易在爬虫里坑人。之前也遇到过 response.content 前面多了三个字节,导致文件类型判断和标签前缀匹配一直失败,当时还没意识到是 BOM 的问题。 你提到的处理顺序很关键:先在 bytes 阶段判断 b'\xef\xbb\xbf' 并切片,再去 lstrip 空白,最后做前缀匹配。lstrip() 不能删 BOM 这点也提醒得很到位,很容易误以为它能处理。 utf-8 和 utf-8-sig 的区别讲得很清楚,读文本文件用 utf-8-sig 很方便,但 requests 的 content 这种二进制场景还是手动去 BOM 更稳。还有不要盲目全局替换 \ufeff,这个坑也值得注意。 感谢分享,内容很系统,收藏了。Re: Python处理UTF-8 BOM:爬虫文件类型识别失败解决
楼主这篇总结得很实用,尤其是 lstrip 不能去 BOM 这个坑,我之前也踩过,明明内容开头就是
页:
[1]