Python 自带的 HTML 解析器

题目考点

这道题主要考这几个点:

  1. 输入处理:先读整数 N,再读接下来的 N 行 HTML。
  2. 字符串整体拼接:把多行 HTML 作为一个整体交给解析器。
  3. 类与方法重写:继承 HTMLParser,重写指定的方法。
  4. 标签分类:
    • 开始标签 <tag>
    • 结束标签 </tag>
    • 空标签 <tag />
  5. 属性处理:标签里可能有属性,也可能没有;属性也可能没有值。

这题表面看像“字符串题”或者“正则题”,但它真正想考的是:会不会用 Python 自带的 HTML 解析器


审题

我们先把题目翻译成“程序到底要做什么”。

输入是:

  • 第一行一个整数 N
  • 接下来 N 行,每行是一段 HTML 代码

输出是:

  • 按照 HTML 中标签出现的顺序,分别打印:
    • Start : 标签名
    • End : 标签名
    • Empty : 标签名
  • 如果这个标签带属性,还要继续打印属性:
    • -> 属性名 > 属性值

还有两个细节很重要:

1. 要区分三种标签

例如:

<div>
</div>
<br />

它们分别属于:

  • 开始标签
  • 结束标签
  • 空标签

2. 注释里的内容不要解析

题目特别说了:

<!-- Comments -->

注释里面如果长得像标签,也不能当成真正的标签来处理。


思路提示

这题最关键的思路不是“我怎么手写判断 <>”,而是先想到:

HTML 这种结构化文本,最好不要自己硬拆,应该交给专门的解析器。

Python 里已经有现成工具:

from html.parser import HTMLParser

你要做的事情其实是:

第一步,定义一个类,继承 HTMLParser
第二步,告诉它:

  • 遇到开始标签时怎么打印
  • 遇到结束标签时怎么打印
  • 遇到空标签时怎么打印

第三步,把输入的 HTML 全部喂给它。

也就是说,这题的思路主线非常清楚:

读输入 → 建解析器类 → 重写处理标签的方法 → 解析整段 HTML


完整设计思路

第一步:为什么要继承 HTMLParser

HTMLParser 是 Python 内置的 HTML 解析器。

它已经会帮我们做这些事:

  • 识别开始标签
  • 识别结束标签
  • 识别空标签
  • 把属性拆成 (属性名, 属性值) 的形式
  • 正常跳过我们没有专门处理的内容

所以我们不需要自己写复杂的扫描逻辑。


第二步:分别重写三个方法

在这题里,最重要的是这三个方法:

handle_starttag(self, tag, attrs)
handle_endtag(self, tag)
handle_startendtag(self, tag, attrs)

它们分别对应:

  • 开始标签
  • 结束标签
  • 空标签

比如解析到:

<body data-modal-target class='1'>

那么:

  • tag 就是 "body"
  • attrs 就是一个列表,大概像这样:
[('data-modal-target', None), ('class', '1')]

这里特别注意:

  • 有值的属性,值会正常给出来
  • 没有值的属性,值会是 None

这正好符合题目的输出要求。


第三步:怎么打印属性

题目要求属性格式是:

-> 属性名 > 属性值

所以在开始标签或者空标签的方法里,我们可以遍历 attrs

for name, value in attrs:
    print("->", name, ">", value)

如果没有属性,attrs 就是空列表,循环自然不会打印任何东西。


第四步:为什么输入最好先拼起来

题目给的是 N 行 HTML。

虽然可以一行一行 feed(),但对于初学者,更稳妥的写法是:

  1. 用列表把这 N 行存起来
  2. 用换行符拼成一个完整字符串
  3. 一次性交给解析器

例如:

html = '\n'.join(lines)
parser.feed(html)

这样做的好处是:

  • 更接近“解析整个 HTML 文档”
  • 遇到跨行内容时更自然
  • 代码也更清晰

第五步:为什么这题不用自己处理注释

题目说不要解析注释里的标签。

如果你是手写字符串判断,这一点会非常麻烦。
但如果你用 HTMLParser,并且没有去专门把注释当作标签处理,那么一般就不会把注释内容误当成开始/结束/空标签输出。

所以这题最核心的经验是:

遇到结构化文本,优先想“有没有现成解析器”,不要一开始就自己硬写规则。


代码实现

下面是适合初学者、也最符合这题思路的写法。

from html.parser import HTMLParser

class MyHTMLParser(HTMLParser):
    def handle_starttag(self, tag, attrs):
        print("Start :", tag)
        for name, value in attrs:
            print("->", name, ">", value)

    def handle_endtag(self, tag):
        print("End   :", tag)

    def handle_startendtag(self, tag, attrs):
        print("Empty :", tag)
        for name, value in attrs:
            print("->", name, ">", value)


n = int(input())
lines = []

for _ in range(n):
    lines.append(input())

html = "\n".join(lines)

parser = MyHTMLParser()
parser.feed(html)

运行演示

我们用题目里的样例来手动走一遍。

输入:

2
<html><head><title>HTML Parser - I</title></head>
<body data-modal-target class='1'><h1>HackerRank</h1><br /></body></html>

拼接后,大致相当于:

<html><head><title>HTML Parser - I</title></head>
<body data-modal-target class='1'><h1>HackerRank</h1><br /></body></html>

解析过程是这样的。

1. 遇到 <html>

调用:

handle_starttag("html", [])

输出:

Start : html

2. 遇到 <head>

输出:

Start : head

3. 遇到 <title>

输出:

Start : title

4. 遇到 </title>

输出:

End   : title

5. 遇到 </head>

输出:

End   : head

6. 遇到 <body data-modal-target class='1'>

这里:

tag = "body"
attrs = [('data-modal-target', None), ('class', '1')]

输出:

Start : body
-> data-modal-target > None
-> class > 1

这里你要特别注意:

data-modal-target

这个属性没有写值,所以解析器给它的值就是 None


7. 遇到 <h1>

输出:

Start : h1

8. 遇到 </h1>

输出:

End   : h1

9. 遇到 <br />

这是空标签,所以会调用:

handle_startendtag("br", [])

输出:

Empty : br

10. 遇到 </body></html>

继续输出:

End   : body
End   : html

方法总结

这类题以后怎么识别、怎么下手,可以记住下面这个判断思路。

一看到“HTML / XML / 标签解析”题,先想这三件事

第一,题目是在考字符串,还是在考“结构化解析”?
这题明显不是普通字符串切片,而是结构化解析。

第二,Python 有没有现成工具?
有,HTMLParser

第三,题目让我在“解析过程中”做什么?
这题就是在不同类型标签出现时打印不同内容。

所以这类题的标准套路就是:

继承解析器 → 重写回调方法 → 输入内容喂给解析器


这题最容易走偏的地方

很多人一看到标签题,就会想:

  • split('<')
  • split('>')
  • 用正则强行匹配

这不是完全不能做,但会很麻烦,尤其是遇到:

  • 多个属性
  • 属性没有值
  • 单引号双引号
  • 空标签
  • 注释
  • 多行 HTML

所以这题的真正训练点是:

不要把所有题都当成纯字符串题。


补充说明:题目模板该怎么理解

如果平台模板里是这种写法:

n = int(input())

意思就是先读行数。

然后:

for _ in range(n):
    line = input()

意思是循环读入接下来的 n 行 HTML。

最后把它们拼起来:

html = "\n".join(lines)

这句的作用是:
把多行字符串合并成一个完整字符串,中间用换行符连接。

比如:

["<html>", "<body>"]

会变成:

"<html>\n<body>"

练习

下面给你一道同类型的小练习,先不要急着看答案,先按今天这题的方法自己想。

练习题

输入:

3
<div id="main">
<img src="a.png" alt="pic" />
</div>

请按照题目的同样格式,写出输出结果。

提示

先判断这 3 个标签分别是什么类型:

  1. <div id="main">
  2. <img src="a.png" alt="pic" />
  3. </div>

再分别考虑:

  • 标签名是什么
  • 属性有哪些
  • 哪个是开始标签,哪个是空标签,哪个是结束标签

为什么 handle_starttaghandle_startendtag 是分开的

这个问题非常好,因为它刚好碰到了这道题里最容易“看起来差不多,其实不是一回事”的地方。

你可以先记住一句最核心的话:

这两个方法分开,不是因为它们“长得像标签”,而是因为它们在源码里的写法不同,解析器要把这种写法区别出来。

也就是说,HTMLParser 不只是想知道“这是个标签”,它还想知道:

  1. 这是普通开始标签吗?
  2. 这是结束标签吗?
  3. 这是写成 /> 的空标签吗?

所以它才会有不同的回调方法。


先把三个方法的职责分清楚

在这道题里,最常见的是这三个方法:

handle_starttag(self, tag, attrs)
handle_endtag(self, tag)
handle_startendtag(self, tag, attrs)

它们分别处理的是:

handle_starttag

处理这种写法:

<div>
<body class="main">
<br>

注意,哪怕是 <br>,只要它写法上没有 />,对解析器来说,它也是“开始标签”的写法。


handle_endtag

处理这种写法:

</div>
</body>

也就是带斜杠在前面的结束标签。


handle_startendtag

处理这种写法:

<br />
img />
<meta />

更准确地说,是处理这种“开始和结束合在一个标签里写出来”的形式。

也就是结尾带 /> 的形式。


解析器为什么一定要分开处理

因为这两种写法,虽然最后都可能表示“这个标签不用包住内容”,但它们在源码里不是同一种东西。

比如:

<br>

<br />

从人的直觉看,好像都差不多,都是换行。
但从解析器角度看,它们是两种不同的输入形式:

  • <br>:看到的是“开始标签”
  • <br />:看到的是“自闭合标签”

而这道题要求你把三种标签分别打印出来,所以解析器必须分开告诉你。

如果不分开,你就没法知道原始 HTML 里到底写的是哪一种格式。


<br><br /> 到底有什么区别

这里要分成两层来看:语法写法实际语义


第一层:语法写法不同

这是最直接的一层。

<br> 的写法是:

<br>

它只有左尖括号和右尖括号,没有末尾的 /

<br /> 的写法是:

<br />

它在结束前多了一个 /,表示“这个标签在这里自己就结束了”。

所以在 HTMLParser 里:

  • <br> 会进入 handle_starttag
  • <br /> 会进入 handle_startendtag

这就是最直接的区别。


第二层:在 HTML 里,它们常常表达同一个意思

br 本身是一个特殊标签,表示换行。

它本来就不是那种需要包住内容的标签。你不会写:

<br>文字</br>

这种写法本身就不符合它的用途。

所以在浏览器眼里:

<br>

<br />

通常都会被当成“插入一个换行”。

也就是说:

在页面显示效果上,这两种写法对 br 来说通常没有区别。

但这不等于它们在解析时是同一件事。
题目关心的是“标签的形式”,不是“浏览器最终显示是不是一样”。


一个很关键的理解:解析器看的是“你怎么写的”,不是只看“它最后像什么”

这句话很重要。

很多初学者会想:

“既然 <br><br /> 最后都是换行,那为什么不能统一处理?”

因为这题不是在问浏览器渲染效果,而是在问:

HTML 代码里出现了什么形式的标签。

解析器要忠实地把源码中的结构告诉你。

所以:

  • 看到 <div>,它说:这是开始标签
  • 看到 </div>,它说:这是结束标签
  • 看到 <br />,它说:这是空标签

这其实是在保留源码信息。


为什么 <br> 不是 handle_startendtag

这是很多人最容易卡住的点。

你可能会觉得:

br 本来就是空的呀,它不就应该算空标签吗?”

这里一定要分清:

“这个标签本身通常没有内容”“它在代码里是不是写成 /> 不是一回事。

HTMLParser 调用哪个方法,首先看的是标签写法。

所以:

写成 <br>

解析器会理解为:

“这是一个开始标签。”

于是调用:

handle_starttag("br", attrs)

写成 <br />

解析器会理解为:

“这是一个开始结束合一的标签。”

于是调用:

handle_startendtag("br", attrs)

也就是说,HTMLParser 在这里分的是语法形式,不是你脑子里对这个标签用途的理解。


用一个类比帮助你理解

你可以把它想成“门的两种关闭方式”。

一种是:

“把门打开后,它本来就很快会关上。”

另一种是:

“你在动作里明确写出来:打开并立刻关上。”

这两件事结果可能差不多,但动作记录不一样。

在 HTML 解析里:

  • <br> 更像“这是一个开始动作”
  • <br /> 更像“这是一个开始并立刻结束的动作”

所以回调方法会不同。


这道题里为什么必须知道这个区别

因为题目明确要求输出三类:

  • Start
  • End
  • Empty

它并不是让你只判断“有没有标签”。

例如样例里的:

<br />

就必须输出:

Empty : br

如果你只会写 handle_starttag,那你就会把它错当成:

Start : br

这样答案就错了。

所以这道题的关键不是“知道 br 是换行”,而是:

知道解析器会把 <br><br /> 分别交给不同的方法。


用一段小代码直接看差别

你可以看这个例子:

from html.parser import HTMLParser

class MyHTMLParser(HTMLParser):
    def handle_starttag(self, tag, attrs):
        print("start:", tag)

    def handle_endtag(self, tag):
        print("end:", tag)

    def handle_startendtag(self, tag, attrs):
        print("empty:", tag)

parser = MyHTMLParser()

parser.feed("<br>")
parser.feed("<br />")

输出会是:

start: br
empty: br

这就非常直观地说明了:

  • <br> 走的是 handle_starttag
  • <br /> 走的是 handle_startendtag

再补一层:为什么题目把 <br /> 叫空标签

因为它在写法上已经把“开始”和“结束”合并进一个标签了。

比如普通标签通常是成对的:

<p>内容</p>

而空标签这种写法:

<br />

只有一个标签本体,没有单独的结束标签。

所以题目把它归到 Empty,非常合理。

注意,这里的“空”更偏向于“没有单独结束标签、一个标签就写完了”,而不是单纯说“这个标签里面没有文字”。


本节小结

你把这几个点记住,这个问题就算真正弄明白了。

第一,handle_starttaghandle_startendtag 分开,是因为解析器要区分不同的标签写法。

第二,<br><br /> 在页面效果上常常差不多,但在源码形式上不同。

第三,HTMLParser 判断调用哪个方法,首先看的不是“这个标签通常有没有内容”,而是“它在源码里是不是写成了 />”。

第四,所以:

<br>

会走:

handle_starttag

<br />

会走:

handle_startendtag

给你一个小练习

判断下面每个标签会触发哪个方法,不用写代码,先自己想。

<div>
</div>
<hr />
<img src="a.png" />
p>

提示你按这个顺序判断:

  1. 是开始标签、结束标签,还是 /> 形式?
  2. 对应 handle_starttaghandle_endtag、还是 handle_startendtag

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇