爬虫拿到 HTTP 返回的 HTML 源码后,不能直接进入解析阶段。原始页面可能带 UTF-8 BOM、空白、注释、转义字符和不规范标签,直接解析或提取文本容易出现乱码、匹配失败和 DOM 异常。HTML 预处理的目标不是压缩,而是在正式解析前修复脏数据、消除干扰项,为 XPath、CSS 选择器和文本提取提供干净输入。
预处理顺序与主要工作
一套标准流程的顺序很关键:二进制预处理 -> 编码识别与解码 -> 文本清洗 -> 标签规范化 -> 可选过滤。具体包括:剔除 UTF-8 BOM、截断超长响应、过滤无效二进制头部;把 bytes 转为字符串并解决乱码;移除注释、多余换行和空格;补全残缺标签、处理转义字符;按需移除 script/style/noscript 等无用标签,降低解析压力。重点提醒:应先做 bytes 处理,再解码,最后做文本清洗。不要先解码再处理 BOM,否则会引入 \ufeff 字符。
第一步:原始二进制处理,清除 UTF-8 BOM
Windows 服务器或记事本生成的网页,开头可能带 b'\xef\xbb\xbf'。它不属于 HTML 正文,会干扰魔数判断和前缀匹配,lstrip() 也无法自动移除。可以在解码前写一个函数:- def strip_bom_and_prefix_whitespace(raw_bytes: bytes) -> bytes:
- if raw_bytes.startswith(b'\xef\xbb\xbf'):
- raw_bytes = raw_bytes[3:]
- raw_bytes = raw_bytes.lstrip(b' \r\n\t')
- return raw_bytes
复制代码 这一步适合放在 detect_file_type 之前执行,在解码前处理性能更高,也能规避解码异常。
第二步:编码识别,bytes 转文本
很多网页 Content-Type 里写的编码和页面 meta 声明不一致,直接用 resp.content.decode('utf-8') 大概率会乱码。可以使用 cchardet 或 chardet 自动探测编码:- import cchardet
- def decode_html(raw_bytes: bytes) -> str:
- detect_result = cchardet.detect(raw_bytes)
- encoding = detect_result['encoding'] or 'utf-8'
- try:
- return raw_bytes.decode(encoding)
- except Exception:
- return raw_bytes.decode('utf-8', errors='replace')
复制代码 更稳妥的做法是优先从 response headers、html meta 标签中提取 charset,其优先级高于自动探测,准确率通常更高;自动探测作为兜底。
第三步:文本清洗,去除注释与多余空白
文本清洗要区分两种场景:一是保留页面结构,只删除注释、合并多余空白,适合解析;二是强压缩,删除标签间空白,适合存储和文本比对,不建议用于解析。不要用简单正则全局删除空白,因为 pre、textarea、script、style 内部的空白不能随意清除。例如:- import re
- import htmlmin
- def clean_html_text(html: str, remove_comments=True) -> str:
- if remove_comments:
- html = re.sub(r'<!--[\s\S]*?-->', '', html)
- html = htmlmin.minify(html, remove_comments=False, remove_all_empty_space=False)
- return html
复制代码 如果只需要提取正文文本,不需要 JS 和 CSS,可以删除 script、style 标签,减少解析开销:- from bs4 import BeautifulSoup
- def remove_useless_tags(html: str) -> str:
- soup = BeautifulSoup(html, 'html.parser')
- for tag in soup(['script', 'style', 'noscript']):
- tag.decompose()
- return str(soup)
复制代码 注意:移除 script/style 会改变页面 DOM 结构。如果业务需要执行 JS 或提取内嵌 CSS,不能使用这一步。
完整预处理流水线示例
把上述步骤组合到 requests 爬虫中,可以形成可复用的预处理函数。下面示例使用 requests、cchardet、htmlmin 和 BeautifulSoup,并需要导入 re:- import re
- import requests
- import cchardet
- import htmlmin
- from bs4 import BeautifulSoup
- def preprocess_html(resp: requests.Response) -> str:
- raw_bytes = resp.content
- if raw_bytes.startswith(b'\xef\xbb\xbf'):
- raw_bytes = raw_bytes[3:]
- raw_bytes = raw_bytes.lstrip(b' \r\n\t')
- detect_result = cchardet.detect(raw_bytes)
- encoding = detect_result['encoding'] or 'utf-8'
- try:
- html_str = raw_bytes.decode(encoding)
- except Exception:
- html_str = raw_bytes.decode('utf-8', errors='replace')
- html_str = re.sub(r'<!--[\s\S]*?-->', '', html_str)
- soup = BeautifulSoup(html_str, 'html.parser')
- for tag in soup(['script', 'style']):
- tag.decompose()
- html_str = str(soup)
- html_str = htmlmin.minify(html_str, remove_comments=False, remove_all_empty_space=False)
- return html_str
- if __name__ == '__main__':
- r = requests.get('https://example.com')
- clean_html = preprocess_html(r)
- print(clean_html)
复制代码
常见踩坑点
BOM 处理时机:BOM 必须在 bytes 阶段处理,解码后会变成 \ufeff,容易干扰文本匹配。编码冲突:HTTP Header 的 charset、meta 标签 charset、页面实际编码三者可能不一致,自动探测只能兜底,优先解析 meta 标签。正则不能万能:不要手写正则删除标签,HTML 不是规则文本,正则容易破坏 DOM,标签删除交给 BeautifulSoup。预处理不等于压缩:做 XPath/CSS 选择器解析时不要使用强压缩模式,体积虽小但调试困难,且极少提升解析速度。转义字符:<、& 这类实体一般保留原样,只有提取纯文本展示时才调用 html.unescape()。异常页面:部分网页存在不闭合标签,BS4 会自动修复,这也是预处理的一部分。
不同业务场景的预处理策略
网页文件类型判断:只做二进制 BOM 清洗,不解码,直接用原始 bytes 做魔数判断。DOM 解析、字段提取:BOM 清洗 -> 解码 -> 删除注释 -> 移除 script/style -> 轻量空白规整。网页归档存储:在上述基础上开启 htmlmin 强压缩,减小存储体积。网页正文相似度比对:全部预处理完成后,再提取纯文本,消除换行空格带来的干扰。
小结
HTML 预处理不是简单删空格,而是一套分层清洗流水线:二进制预处理优先,然后处理编码,再做 DOM 清洗。根据业务选择清洗力度,类型检测尽量不解码;DOM 解析用 BS4 安全移除无用标签;归档场景再开启压缩。在爬虫项目中,把 BOM 清洗、编码探测、注释清理、无用标签移除封装成独立函数,可以减少后续解析环节的诡异 bug。 |