4、HTML解析基础:HTML结构、XPath语法、lxml库的使用

做数据采集,说白了就是跟网页打交道。你拿到一个页面,怎么把想要的数据摘出来?这就得靠解析了。我个人习惯把解析比作「拆快递」——你得先知道包裹长什么样,再找到下刀的位置,最后把东西取出来。

今天咱们就聊聊HTML解析这回事。我会从HTML结构讲起,再到XPath语法,最后上手lxml库。嗯,这三样东西,你玩明白了,采集数据就成功了一半。

4.1 HTML结构——你得先看懂网页的骨架

HTML是什么?超文本标记语言。说白了,就是用一堆标签把内容包起来,告诉浏览器「这是标题、这是段落、这是链接」。

一个典型的HTML文档长这样:

<!DOCTYPE html>
<html>
<head>
    <title>页面标题</title>
</head>
<body>
    <h1>一级标题</h1>
    <p>这是一个段落。</p>
    <a href="https://example.com">这是一个链接</a>
</body>
</html>

你想想看,这就像一棵树。最上面是<html>,下面分两个大枝干:<head><body><head>里放的是元数据,比如标题、编码;<body>里才是我们真正要抓的内容。

核心概念:DOM树

浏览器会把HTML解析成一棵文档对象模型(DOM)树。每个标签都是一个节点,节点之间有父子关系、兄弟关系。我们做数据采集,本质上就是在这棵树上找到目标节点。

我在项目中遇到过不少新手,上来就对着HTML字符串用正则表达式硬抠数据。结果页面一改版,正则就废了。我的建议是:别跟字符串较劲,用解析器操作DOM树,这才是正道。

4.2 XPath语法——定位数据的GPS

XPath,全称XML Path Language。它最初是为XML设计的,但HTML也是XML的亲戚,所以完全能用。

XPath的核心思想很简单:用路径表达式在DOM树里找节点。就像你用地址找房子一样——「北京市海淀区中关村大街xx号」这种写法,换成XPath就是/html/body/div[1]/p

4.2.1 基本语法

表达式 含义 示例
/ 从根节点选取 /html/body
// 从任意位置选取 //div(所有div)
. 当前节点 ./p
.. 父节点 ../div
@ 选取属性 //a/@href

举个例子。假设页面里有这么一段:

<div class="article">
    <h2>标题</h2>
    <p class="content">正文内容</p>
    <a href="detail.html">详情</a>
</div>

我想取<p>里的文本,XPath可以这么写:

//div[@class="article"]/p[@class="content"]/text()

拆开看://div找所有div,[@class="article"]筛选class为article的,/p取子节点p,[@class="content"]再筛选,最后/text()拿文本。

小技巧:我个人习惯先用浏览器的开发者工具(F12)验证XPath。在Elements面板选中元素,右键→Copy→Copy XPath,能快速拿到一个参考路径。但注意,浏览器生成的路径往往很啰嗦,我会手动精简一下。

4.2.2 常用函数和运算符

XPath还支持一些函数,让定位更灵活:

  • text():获取文本内容
  • contains(@class, "keyword"):class包含某个关键词
  • starts-with(@id, "prefix"):id以某前缀开头
  • position():节点位置,比如//li[position()=2]取第二个li
  • |:并集运算符,比如//h1 | //h2同时选h1和h2

我曾经遇到过一个坑:某个网站的class名里带了空格,比如class="item active"。直接用[@class="item active"]没问题,但如果你用contains(@class, "item"),就得小心了——它会把class="item-something"也匹配上。所以,精确匹配用等号,模糊匹配用contains,但要注意边界。

4.3 lxml库——Python里的解析利器

理论说完了,咱们上手干活。Python里解析HTML的库不少,BeautifulSoup、lxml、html.parser……我个人最常用的是lxml。为什么?因为它快,而且XPath支持得最好。

4.3.1 安装和基本使用

pip install lxml

装好之后,写个最简单的例子:

from lxml import etree

# 假设这是我们从网页抓到的HTML
html_str = """
<html>
    <body>
        <div class="content">
            <h1>文章标题</h1>
            <p>第一段文字</p>
            <p>第二段文字</p>
        </div>
    </body>
</html>
"""

# 解析HTML
tree = etree.HTML(html_str)

# 用XPath提取数据
title = tree.xpath('//h1/text()')[0]
paragraphs = tree.xpath('//p/text()')

print(f"标题: {title}")
print(f"段落: {paragraphs}")

输出结果:

标题: 文章标题
段落: ['第一段文字', '第二段文字']

你看,就这么几行代码,数据就取出来了。lxml的etree.HTML()会自动补全不规范的HTML标签,比如缺了</p>它会帮你补上。这一点在实战中特别有用——很多网站的HTML写得并不规范。

4.3.2 从文件或网络加载

实际项目中,我们通常不会把HTML硬编码在代码里。要么从文件读,要么从网络请求拿。

# 从文件读取
tree = etree.parse('page.html', etree.HTMLParser())

# 从网络请求(配合requests库)
import requests
resp = requests.get('https://example.com')
tree = etree.HTML(resp.text)

注意:etree.parse()时,一定要指定etree.HTMLParser()作为解析器。否则默认用XML解析器,遇到不规范的HTML会报错。我曾经因为这个bug排查了半天,最后发现是解析器没指定——嗯,这种低级错误犯过一次就够了。

4.3.3 实战:提取文章列表

假设我们要采集一个博客的文章列表,HTML结构大概是这样:

<div class="blog-list">
    <article>
        <h2><a href="/post/1">第一篇文章</a></h2>
        <p class="date">2024-01-15</p>
        <p class="summary">这是摘要...</p>
    </article>
    <article>
        <h2><a href="/post/2">第二篇文章</a></h2>
        <p class="date">2024-01-16</p>
        <p class="summary">这是另一篇摘要...</p>
    </article>
</div>

用lxml提取所有文章信息:

from lxml import etree

tree = etree.HTML(html_str)
articles = tree.xpath('//div[@class="blog-list"]/article')

for article in articles:
    title = article.xpath('.//a/text()')[0]
    link = article.xpath('.//a/@href')[0]
    date = article.xpath('.//p[@class="date"]/text()')[0]
    summary = article.xpath('.//p[@class="summary"]/text()')[0]
    
    print(f"标题: {title}")
    print(f"链接: {link}")
    print(f"日期: {date}")
    print(f"摘要: {summary}")
    print("---")

这里有个细节:在循环里,我用的是article.xpath('.//a/text()'),注意那个点号.。它表示从当前article节点开始查找,而不是从根节点。如果不加点号,//a会从整个文档的根节点找,结果可能不对。

避坑指南:我曾经在写XPath时,忘了在循环里加.,结果每次循环都返回所有文章的第一个链接。排查了半天才发现是相对路径和绝对路径的问题。记住:在循环里处理子节点时,XPath一定要用相对路径(加.)。

4.4 总结与建议

HTML解析这块,说难不难,说简单也不简单。核心就三点:

  1. 理解DOM树结构——这是基础,你得知道页面是怎么组织的
  2. 掌握XPath语法——这是工具,用得越熟,定位越准
  3. 熟练使用lxml——这是武器,Python生态里解析HTML的首选

我的建议是:刚开始别追求一步到位。先拿几个简单的页面练手,把XPath的基本语法跑通。遇到复杂的页面,多用浏览器的开发者工具调试。等你处理过几十个不同结构的页面后,自然就熟练了。

下一章,我们会讲正则表达式在数据清洗中的应用。到时候你会发现,解析和清洗是相辅相成的——解析拿到的数据往往很粗糙,需要清洗才能用。嗯,咱们到时候细聊。