第 37 课 · 实战 · 18 分钟

Python HTML 解析

让解析器理解 HTML 结构,而不是用正则猜。

HTMLParserhandle_starttaghandle_data属性

供应商的 HTML 里表格套着表格,正则数不清层数。解析器按顺序走标签。哪些片段该留,哪些该丢?

HTML 是嵌套结构,不是扁平字符串。Python 的 html.parser 会按顺序经过开始标签、文本和结束标签。继承 HTMLParser,只保存需要的部分,解析结果既可测试,也不依赖某个固定页面布局。 你还会用栈配对嵌套标签,并把脚本内容排除在可见文本之外。

继承解析器

创建一个继承 HTMLParser 的类,并在添加自己的状态前调用 super().__init__。重写 handle_starttag 处理开始标签。然后创建实例,把 HTML 字符串传给 feed。

from html.parser import HTMLParser

class Tags(HTMLParser):
    def __init__(self):
        super().__init__()
        self.names = []

    def handle_starttag(self, tag, attrs):
        self.names.append(tag)

parser = Tags()
parser.feed("<h1>Hello</h1><p>World</p>")
print(parser.names)

按顺序记录开始标签

把属性转换成字典

attrs 参数是名称和值组成的列表。需要按名称查找时,用 dict 转换。还要检查 None:布尔属性可能没有值,缺失 href 的链接不应被当成有效地址。

from html.parser import HTMLParser

class Links(HTMLParser):
    def __init__(self):
        super().__init__()
        self.links = []

    def handle_starttag(self, tag, attrs):
        values = dict(attrs)
        if tag == "a" and values.get("href"):
            self.links.append(values["href"])

parser = Links()
parser.feed('<a href="/a">A</a><a href="/b">B</a>')
print(parser.links)

安全读取 href

收集可见文本

handle_data 接收标签之间的文本。去掉首尾空白并忽略空字符串,输出就不会被缩进填满。如果只需要某个元素的文本,可以用布尔标记记录当前是否位于该元素内部。

from html.parser import HTMLParser

class Text(HTMLParser):
    def __init__(self):
        super().__init__()
        self.parts = []

    def handle_data(self, data):
        text = data.strip()
        if text:
            self.parts.append(text)

parser = Text()
parser.feed("<h1>Title</h1><p>First sentence.</p>")
print(" | ".join(parser.parts))

去掉缩进后的文本

解码实体并保留来源

HTMLParser 默认会解码 &amp; 这类字符引用。保存文本时,把原始 URL 一起保留,方便追溯提取结果。解析器只提供结构,不判断页面内容是否可信。

from html import unescape

raw = "Tom &amp; Jerry"
print(unescape(raw))
print(raw)

解码但保留原始值

用栈配对嵌套标签

HTMLParser 分别报告开始和结束标签,元素嵌套时,单个布尔标志容易配错结束标签。栈可以记住当前打开的 href,遇到匹配的结束标签再弹出。这个小解析器适合受控样例;任意网页还是完整 HTML 解析器更稳。

from html.parser import HTMLParser

class LinkCollector(HTMLParser):
    def __init__(self):
        super().__init__()
        self.stack = []
        self.links = []

    def handle_starttag(self, tag, attrs):
        if tag == "a":
            self.stack.append(dict(attrs).get("href"))

    def handle_endtag(self, tag):
        if tag == "a" and self.stack:
            href = self.stack.pop()
            if href:
                self.links.append(href)

parser = LinkCollector()
parser.feed('<a href="/one">One</a><a href="/two"><b>Two</b></a>')
print(parser.links)

开始入栈,结束出栈

把脚本和样式挡在可见文本外

handle_data 也会收到 script 和 style 的内容。解析时统计这些元素并忽略其中的文本,否则一段 JavaScript 字符串可能混进本应只含可见文字的摘要。

from html.parser import HTMLParser

class VisibleText(HTMLParser):
    def __init__(self):
        super().__init__()
        self.skip = 0
        self.parts = []

    def handle_starttag(self, tag, attrs):
        if tag in {"script", "style"}:
            self.skip += 1

    def handle_endtag(self, tag):
        if tag in {"script", "style"} and self.skip:
            self.skip -= 1

    def handle_data(self, data):
        if not self.skip and data.strip():
            self.parts.append(data.strip())

parser = VisibleText()
parser.feed("<h1>Menu</h1><script>alert('x')</script><p>Tea</p>")
print(" | ".join(parser.parts))

只要可见文本

本课要点

  • 继承 HTMLParser,并在保存状态前调用 super().__init__。
  • handle_starttag 接收标签名和属性键值对列表。
  • handle_data 接收文本节点,也包括空白字符。
  • 用 feed 处理 HTML 字符串,解析器会调用你重写的方法。
  • 把来源 URL 和提取文本一起保存,方便核对。
  • 嵌套元素用栈;提取可见文本时要跳过 script 和 style。

常见错误

  • 忘记 super().__init__,导致解析器内部状态缺失。
  • 以为 attrs 已经是字典,直接按名称取值。
  • 用正则解析嵌套 HTML,而不是使用解析器。
  • 用一个布尔值处理嵌套,结果把文字挂到了错误的结束标签上。
轮到你

从保存的 HTML 字符串中提取所有 (文本, href) 元组,跳过没有 href 的链接。

动手试试

改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。

浏览器内 Python本地运行 · 无需账号
首次运行会从 CDN 下载 Pyodide。修改后点击“运行代码”。

练习本课

12 道题,作答后立即看到解释。

查看 12 道题答案开始练习
← HTTP 请求URL 解析 →