Python爬虫新手入门之初学lxml库 前言新手学爬虫第一个真正的坎不是怎么发请求而是怎么从一坨 HTML 里把想要的数据抠出来。标准库自带的xml.etree.ElementTree能做 XML 解析但它对网页这种容错要求高、结构不规范的 HTML 支持有限也不支持完整的 XPath。lxml 就是在这个位置上补上短板的第三方库。lxml 的定位可以一句话概括它是 ElementTree 的增强版——API 尽量向后兼容 ElementTree同时带来三样东西基于 C 库 libxml2 的解析实现、完整的 XPath 1.0 支持、以及对残缺 HTML 的容错解析。必须说清两件事第一lxml 是第三方库本机没有安装本文代码无法运行验证只能逐行人工推演。具体 API 与参数以 lxml 官方文档为准。第二爬虫要合规。遵守目标站点的 robots.txt、使用可识别的真实 User-Agent、严格限速、只取公开页面、不绕过任何登录墙或反爬机制。本文只讲解析不讲任何规避手段。一、先分清解析 XML 和解析 HTML 是两件事这是新手最容易踩的认知坑lxml 的 XML 解析器和 HTML 解析器是两套入口。XML 是严格格式标签必须闭合、大小写敏感、必须有且仅有一个根节点。用lxml.etree解析 XML 时格式不对就直接抛异常。HTML 是能容忍的杂乱格式标签可以不闭合比如单独的换行标签、图片标签、可以嵌套错乱、可以有重复属性。用 XML 解析器去啃网页最常见的症状是明明浏览器里能看程序却报解析错误。所以规则是XML 用lxml.etreeHTML 用lxml.html。官方文档里也明确写了lxml.html 是基于 lxml 的 HTML 解析器专门提供面向 HTML 元素的 API 和常用处理工具。对比项lxml.etreelxml.html目标格式严格 XML容错 HTML格式错误时直接报错尽量修复并建树典型入口fromstring/parsedocument_fromstring/fromstring是否处理命名空间是核心议题通常不涉及常见用途配置文件、接口返回、RSS网页抓取后的抽取二、XML 侧三个必须记住的入口fromstring从字符串解析返回根 Element。parse从文件或类文件对象解析返回的是ElementTree 对象不是 Element。这个区别新手经常搞错拿到 ElementTree 后要再取.getroot()才是根节点。官方文档特意提示了这一点。XML行为类似fromstring。# 适用于 Python 3.8需要第三方库 lxml本机未安装无法运行from lxml import etreexml_text catalogbook id1titleA/title/book/catalogroot etree.fromstring(xml_text) # 返回 Elementprint(root.tag, root[0].tag)tree etree.parse(catalog.xml) # 返回 ElementTreeroot2 tree.getroot() # 再取根节点命名空间namespace是 XML 侧最大的门槛。现实里的 XML 常常带命名空间写成xmlnsurn:example:library或xmlns:x...。带命名空间后root.find(title)会找不到任何东西——因为标签的真实名字其实是那段命名空间 URI 加上title这个组合。两种处理方式方式一在 XPath 里声明前缀映射。lxml 的 XPath 支持namespaces关键字参数传一个前缀 → URI的字典。官方文档强调了一个反直觉的点你在这里用的前缀和文档里写的前缀毫无关系。文档可以随意定义自己的前缀甚至用默认前缀都不影响你的映射。方式二用local-name()绕开前缀。//*[local-name() title]只匹配标签的本地名忽略命名空间。但要注意XPath 没有默认命名空间的概念所以空前缀无法用于前缀映射如果你不确定目标文档的命名空间 URIlocal-name()是更省事的写法代价是失去了命名空间的区分能力不同命名空间下同名标签会被一网打尽。# 适用于 Python 3.8需要第三方库 lxml本机未安装无法运行from lxml import etreexml_text (root xmlns:nurn:example:libraryn:itemhello/n:item/root)root etree.fromstring(xml_text.encode(utf-8))# 方式一声明前缀映射前缀名任取与文档里的 n 无关found root.xpath(//p:item/text(), namespaces{p: urn:example:library})# 方式二用 local-name() 忽略命名空间found2 root.xpath(//*[local-name() item]/text())print(found, found2)另外要留意 lxml 与 ElementTree 在元素名字上的表示差异ElementTree 系含 lxml 的 ElementTree 兼容层用 Clark 记法{命名空间}标签名来表示带命名空间的元素而 XPath 用前缀。两者混着读代码时容易懵。三、HTML 侧容错解析才是重点HTML 用lxml.html。官方文档列出的入口函数有document_fromstring(string)把字符串解析成一个完整的 HTML 文档根节点是 html 元素fragment_fromstring(string, create_parentFalse)解析一个 HTML片段片段需要有单个外层元素否则可以用create_parent指定一个容器标签包起来fragments_fromstring(string)解析出多个片段fromstring(string)按内容自动在document_fromstring和fragment_fromstring之间选择。# 适用于 Python 3.8需要第三方库 lxml本机未安装无法运行from lxml import htmlpage htmlbodydiv classitema href/p/1标题一/a/divdiv classitema href/p/2标题二/a/div/body/htmldoc html.document_fromstring(page)titles doc.xpath(//div[classitem]/a/text())links doc.xpath(//div[classitem]/a/href)print(titles, links)classitem这种写法有一个现实陷阱class 属性可以包含多个类名classitem active精确相等匹配就会漏掉。稳妥写法是用contains(concat( , normalize-space(class), ), item )这类表达式或者改用 CSS 选择器接口lxml 提供了基于 cssselect 的接口属于另一套 API。四、大数据量与合规姿势解析大文件用iterparse。官方文档给出的做法是etree.iterparse(source, events(start, end))逐事件产出(event, element)元组处理完一个元素就调用element.clear(keep_tailTrue)把它从树上摘掉从而把内存压住。注意文档里的关键提示只有end事件才保证该元素已经被完整解析所以在start事件里读text是不安全的。对爬虫场景更重要的是网络侧的纪律设置超时、请求之间 sleep 限速、失败指数退避、遵守 robots.txt、被拒绝就退避而不是换身份硬闯。解析再快越过合规红线也是不能做的事。# 适用于 Python 3.8需要第三方库 lxml本机未安装无法运行from lxml import etree# 流式解析大 XML避免整棵树驻留内存context etree.iterparse(big.xml, events(end,), tagrecord)for event, element in context:value element.findtext(name)print(value)element.clear(keep_tailTrue) # 处理完即释放注意上面的 tag 参数与 events 过滤细节请以 lxml 官方文档为准。如果记不准宁可采用全部事件都收、在循环里判断 element.tag的写法。常见坑点用lxml.etree去解析网页 HTML。❌ 把抓回来的 HTML 直接丢给etree.fromstring遇到未闭合标签就抛异常。 ✅ HTML 一律用lxml.html它基于容错的 HTML 解析器能处理残缺标签。把etree.parse的返回值当根节点。❌root etree.parse(f)后直接root.tag得到的是 ElementTree 而非 Element。 ✅ 记得调用.getroot()而fromstring返回的才是根 Element。忽略命名空间导致 XPath 查不到东西。❌ 文档带xmlns时仍写//title结果返回空列表。 ✅ 要么用namespaces传前缀映射要么用//*[local-name() title]。以为 XPath 里的前缀要跟文档里一致。❌ 因为文档用的是n:就把映射字典的键硬写成n以为必须对应。 ✅ 前缀名是自己取的与文档定义无关但空前缀不能用于映射因为 XPath 没有默认命名空间的概念。用classitem精确匹配多类名。❌ 页面写的是classitem active精确匹配全部漏掉。 ✅ 用contains(concat( , normalize-space(class), ), item )或改用 CSS 选择器。fromstring与document_fromstring用错。❌ 解析一个没有外层包裹的 HTML 片段时直接用document_fromstring。 ✅ 片段用fragment_fromstring必要时配合create_parent或fragments_fromstring。大文件整树加载导致内存爆掉。❌ 用etree.parse读几百 MB 的 XML然后遍历。 ✅ 用etree.iterparse处理完的元素调用clear(keep_tailTrue)及时释放。在start事件里读元素文本。❌ 认为收到start事件时元素的文本已经就绪。 ✅ 只有end事件保证元素解析完整取值应在end分支里做。总结场景用哪个入口关键提醒严格 XML 字符串etree.fromstring返回根 ElementXML 文件etree.parse返回 ElementTree要.getroot()大 XMLetree.iterparse只在end事件取值及时clear网页 HTMLlxml.html.document_fromstring不要用etree解 HTMLHTML 片段fragment_fromstring/fragments_fromstring注意create_parent的作用带命名空间namespaces或local-name()XPath 没有默认命名空间入门 lxml 只要抓住一句话XML 和 HTML 分开处理命名空间是 XML 侧最大的坑。剩下的是 XPath 表达式的熟练度以及——别忘了解析之外抓取行为本身必须合规。所有具体的函数签名与参数请以 lxml 官方文档的当前版本为准。