使用Python BeautifulSoup处理非标准XML

5次阅读

beautifulsoup默认无法解析非标准xml,因其xml解析器严格遵循规范;需先用lxml.etree.XMLParser(recover=True)容错解析,再转字符串交由BeautifulSoup处理。

使用Python BeautifulSoup处理非标准XML

为什么 BeautifulSoup 默认无法正确解析非标准 XML

BeautifulSoup 的 xml 解析器(即 lxml-xmlxml)严格遵循 XML 规范,遇到常见非标准情况会直接报错或丢弃内容。比如:自闭合标签未写斜杠( 而非 )、属性值无引号()、缺失根节点、含非法字符实体(&foo;)、或混用 html 实体( )——这些在真实爬虫/遗留系统中极常见,但 lxml.etree.XMLParser(recover=True) 也不买账。

改用 lxml 的 recover parser + BeautifulSoup 的组合方案

核心思路是:不把原始文本直接喂给 BeautifulSoup(..., 'xml'),而是先用 lxml.etree.fromstring() 带容错解析,再把修复后的树转成字符串交给 BeautifulSoup 处理。这样既能保留 lxml 的恢复能力,又不放弃 BeautifulSoup 熟悉的 API。

  • lxml.etree.XMLParser(recover=True) 可容忍多数语法错误,如缺失引号、未闭合标签、乱码实体
  • 必须显式传入 parser 参数,否则 fromstring() 仍走严格模式
  • 解析后调用 etree.tostring(tree, encoding='unicode', method='xml') 转回字符串,避免字节/编码问题
  • 再用 BeautifulSoup(..., 'xml') 加载该字符串,后续操作完全不变
from lxml import etree from bs4 import BeautifulSoup 

broken_xml = 'text'

关键:启用 recover 模式

parser = etree.XMLParser(recover=True) tree = etree.fromstring(broken_xml.encode('utf-8'), parser) fixed_xml = etree.tostring(tree, encoding='unicode', method='xml')

soup = BeautifulSoup(fixed_xml, 'xml') print(soup.find('item')['id']) # 输出:123

遇到命名空间或 CDATA 时的特殊处理

非标准 XML 常混用命名空间前缀(如 )却不声明 xmlns:ns,或把 jsON/JS 代码塞进 却漏写右括号。BeautifulSoup 对 CDATA 默认当普通文本,而 lxml 的 recover parser 会把未闭合的 CDATA 当注释吞掉。

立即学习Python免费学习笔记(深入)”;

  • 若需保留 CDATA 内容原样,解析前先用正则临时替换:re.sub(r',解析完再还原
  • 命名空间报错(如 Namespace prefix ns not declared)可提前移除所有 xmlns: 属性和带冒号的标签名,用 re.sub(r'xmlns:[^=]+="[^"]*"', '', xml_str)
  • lxml 的 recover=True 开头但无结尾的情况会截断后续内容,务必检查输出字符串是否完整

比对 lxml.etree 和 BeautifulSoup 的实际行为差异

直接用 lxml.etree 能更细粒度控制容错,但丢失了 soup.select().find_next_siblings() 这类便利方法。而 BeautifulSoup 的 'xml' 解析器底层若用 lxml,本质仍是调用 etree.fromstring() —— 区别只在是否暴露 parser 控制权。

  • 不要用 BeautifulSoup(xml_str, 'lxml-xml'):它不接受自定义 parser,且内部仍走严格模式
  • 不要依赖 features='xml':该参数已被弃用,且无 recover 能力
  • 性能上,先 etree.fromstring() 再转字符串会有一次序列化开销,但对 MB 级以下数据影响可忽略
  • 若原始数据含大量非法 UTF-8 字节(如 GBK 混入),需先用 chardet 检测编码并 decode,再喂给 etree.fromstring()

真正麻烦的从来不是语法错误本身,而是错误位置不可预测:可能在第 1 行,也可能藏在嵌套 7 层深的某个属性值里。recover parser 能救活结构,但不会告诉你哪里被悄悄修正了——建议解析后用 soup.prettify() 快速扫一眼关键路径是否符合预期。

text=ZqhQzanResources