
题目考点
这道题主要考这几个点:
- 输入处理:先读整数
N,再读接下来的N行 HTML。 - 字符串整体拼接:把多行 HTML 作为一个整体交给解析器。
- 类与方法重写:继承
HTMLParser,重写指定的方法。 - 标签分类:
- 开始标签
<tag> - 结束标签
</tag> - 空标签
<tag />
- 开始标签
- 属性处理:标签里可能有属性,也可能没有;属性也可能没有值。
这题表面看像“字符串题”或者“正则题”,但它真正想考的是:会不会用 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(),但对于初学者,更稳妥的写法是:
- 用列表把这
N行存起来 - 用换行符拼成一个完整字符串
- 一次性交给解析器
例如:
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 个标签分别是什么类型:
<div id="main"><img src="a.png" alt="pic" /></div>
再分别考虑:
- 标签名是什么
- 属性有哪些
- 哪个是开始标签,哪个是空标签,哪个是结束标签
为什么 handle_starttag 和 handle_startendtag 是分开的
这个问题非常好,因为它刚好碰到了这道题里最容易“看起来差不多,其实不是一回事”的地方。
你可以先记住一句最核心的话:
这两个方法分开,不是因为它们“长得像标签”,而是因为它们在源码里的写法不同,解析器要把这种写法区别出来。
也就是说,HTMLParser 不只是想知道“这是个标签”,它还想知道:
- 这是普通开始标签吗?
- 这是结束标签吗?
- 这是写成
/>的空标签吗?
所以它才会有不同的回调方法。
先把三个方法的职责分清楚
在这道题里,最常见的是这三个方法:
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 />更像“这是一个开始并立刻结束的动作”
所以回调方法会不同。
这道题里为什么必须知道这个区别
因为题目明确要求输出三类:
StartEndEmpty
它并不是让你只判断“有没有标签”。
例如样例里的:
<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_starttag 和 handle_startendtag 分开,是因为解析器要区分不同的标签写法。
第二,<br> 和 <br /> 在页面效果上常常差不多,但在源码形式上不同。
第三,HTMLParser 判断调用哪个方法,首先看的不是“这个标签通常有没有内容”,而是“它在源码里是不是写成了 />”。
第四,所以:
<br>
会走:
handle_starttag
而
<br />
会走:
handle_startendtag
给你一个小练习
判断下面每个标签会触发哪个方法,不用写代码,先自己想。
<div>
</div>
<hr />
<img src="a.png" />
p>
提示你按这个顺序判断:
- 是开始标签、结束标签,还是
/>形式? - 对应
handle_starttag、handle_endtag、还是handle_startendtag?



