供应商的 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 默认会解码 & 这类字符引用。保存文本时,把原始 URL 一起保留,方便追溯提取结果。解析器只提供结构,不判断页面内容是否可信。
from html import unescape
raw = "Tom & 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 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。