
题目考点
这道题主要考的是下面几个点:
HTMLParser的使用- 继承类并重写回调方法
- 字符串判断,比如是否包含换行符
\n - 输入处理:把多行 HTML 代码读进来
- 按题目要求格式输出
这题本质上不是让你“自己硬写一个 HTML 解析器”,而是让你学会用 Python 已经提供好的 HTMLParser。
审题
我们先把题目翻译成更容易下手的话。
题目给你一段包含 N 行的 HTML 代码,要求你按出现顺序输出三类内容:
- 单行注释
- 多行注释
- 数据内容
这里最关键的是要分清:
什么是注释
HTML 注释长这样:
<!-- 注释内容 -->
如果注释内容跨了多行,就是多行注释;如果只在一行里,就是单行注释。
什么是数据
标签里面真正的文本内容就是 data,例如:
<div>Welcome to HackerRank</div>
这里的 data 就是:
Welcome to HackerRank
容易忽略的点
题目特别提醒:
如果
data == '\n',不要打印。
也就是说,有些“数据”其实只是一个换行符,这种无效内容要跳过。
思路提示
先不要急着写代码,先想做法。
这题的关键思路是:
第一步:不要自己手动找注释和数据
因为 HTML 结构自己处理会很麻烦,所以直接使用 Python 的 HTMLParser。
第二步:让解析器帮我们“碰到什么就告诉我们”
HTMLParser 在解析 HTML 时:
- 遇到注释,会调用一个方法
- 遇到普通文本数据,也会调用一个方法
所以我们只需要把这些方法改写掉,在里面按题目格式打印即可。
第三步:判断单行还是多行
当解析器把注释内容传给我们时:
- 如果内容里有
\n,说明它跨行了,是多行注释 - 否则就是单行注释
第四步:处理 data
如果 data 只是一个换行符 '\n',跳过;
否则输出:
>>> Data
具体内容
完整设计思路
现在把整个过程展开成清晰步骤。
第一步:定义一个新的解析器类
我们继承 HTMLParser,比如写成:
class MyHTMLParser(HTMLParser):
然后重写两个方法:
handle_comment(self, data):处理注释handle_data(self, data):处理数据
第二步:在 handle_comment() 里分类输出
解析器遇到注释时,会把注释内部的内容传给 data。
比如:
<!--[if IE 9]>IE9-specific content<![endif]-->
传进来的 data 实际是:
[if IE 9]>IE9-specific content<![endif]
这时候我们判断:
'\n' in data:多行注释- 否则:单行注释
然后按题目要求打印。
第三步:在 handle_data() 里处理文本内容
如果 data 是普通文本,就输出:
>>> Data
data
但有一个例外:
如果 data == '\n',说明只是换行,不输出。
第四步:把输入的 N 行 HTML 读进来
题目会先给一个整数 N,表示后面有多少行 HTML。
所以先读:
n = int(input())
然后循环读入 n 行。
这里建议把所有行拼接成一个完整字符串,再一次性交给解析器:
html = "\n".join(...)
为什么这样做?
因为多行注释本来就可能跨很多行,如果你一行一行单独喂给解析器,虽然有时也能过,但从整体上拼成一段 HTML 再解析,更符合“这是一个完整 HTML 片段”的思路,也更稳。
代码实现
下面是适合初学者理解的写法。
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def handle_comment(self, data):
if '\n' in data:
print(">>> Multi-line Comment")
else:
print(">>> Single-line Comment")
print(data)
def handle_data(self, data):
if data == '\n':
return
print(">>> Data")
print(data)
n = int(input())
html_lines = []
for _ in range(n):
html_lines.append(input())
html = '\n'.join(html_lines)
parser = MyHTMLParser()
parser.feed(html)
运行演示
我们用题目的样例来手动走一遍。
输入
4
<!--[if IE 9]>IE9-specific content
<![endif]-->
<div> Welcome to HackerRank</div>
<!--[if IE 9]>IE9-specific content<![endif]-->
第一步:读入 HTML
前 4 行 HTML 会被拼成一个完整字符串:
<!--[if IE 9]>IE9-specific content
<![endif]-->
<div> Welcome to HackerRank</div>
<!--[if IE 9]>IE9-specific content<![endif]-->
第二步:解析第一段注释
前两行组成一个注释:
<!--[if IE 9]>IE9-specific content
<![endif]-->
注释内容里有换行符,所以:
'\n' in data
为真,因此输出:
>>> Multi-line Comment
[if IE 9]>IE9-specific content
<![endif]
第三步:解析 <div> 里的文本
<div> Welcome to HackerRank</div>
标签中的文本是:
Welcome to HackerRank
这不是单独的 '\n',所以输出:
>>> Data
Welcome to HackerRank
第四步:解析最后一个注释
<!--[if IE 9]>IE9-specific content<![endif]-->
这次没有换行符,所以是单行注释,输出:
>>> Single-line Comment
[if IE 9]>IE9-specific content<![endif]
方法总结
这类题以后可以这样识别、这样下手。
看到“解析 HTML / XML / 标签”时先想:
不要自己手动切字符串,优先想有没有现成解析工具。
在 Python 里,和 HTML 相关的题,常见工具就是:
HTMLParser
这题的通用套路
遇到这种“解析过程中碰到什么就做什么”的题,通常按这个模式做:
- 继承系统提供的解析器类
- 重写对应的处理函数
- 在处理函数里完成输出或统计
- 最后把整段内容交给解析器去扫
这题真正难点不是代码,而是审题
你要抓住两个判断条件:
- 注释里有没有
\n- 有:多行注释
- 没有:单行注释
- 数据是不是刚好等于
'\n'- 是:不输出
- 不是:输出 data
本节小结
这题最核心的一句话就是:
不用自己分析 HTML 结构,而是借助 HTMLParser,在“注释回调”和“数据回调”里按规则打印。
练习
你可以先自己做这一题,不要急着看答案。
练习题
给定下面这段 HTML:
<!--hello-->
<p>Python</p>
<!--line1
line2-->
请按题目同样的规则输出结果。
提示
你可以先自己判断:
- 第一段注释是单行还是多行?
<p>Python</p>里面的 data 是什么?- 最后一段注释里有没有
\n?
这次我们不直接看答案,而是从空白开始搭出来
这道题很适合练一种能力:先把程序骨架搭起来,再一点点把细节填进去。
你以后做题时,如果总觉得“我知道大概在考什么,但就是写不出来”,那就特别需要这种训练方式。
这次我们就严格按你说的顺序来:
- 先写类
- 再写方法
- 最后写主程序
先想清楚:这道题的程序长什么样
这题不是普通的“读入数字然后计算”的题,它更像是:
- 我们先造一个“会解析 HTML 的对象”
- 这个对象在遇到注释时,自动调用一个方法
- 在遇到文本数据时,自动调用另一个方法
- 我们只需要在这两个方法里写“怎么输出”
所以整个程序结构其实很固定:
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
# 写处理注释的方法
# 写处理数据的方法
# 主程序
# 读入 n
# 读入 n 行 html
# 创建解析器对象
# 喂给它 html
你先不要管方法里面怎么写,先把这个整体骨架记住。
第一步:先把类写出来
先写导入,再写一个继承 HTMLParser 的类。
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
pass
这里在干什么
HTMLParser 是 Python 已经写好的 HTML 解析器。
我们现在不是从零造轮子,而是“继承”它,然后在它原有能力上加自己的处理逻辑。
为什么先写 pass
因为我们现在只是先把类的框架搭出来。pass 的意思可以理解成:
这个位置先占着,后面再补内容。
这一步的目标不是“写完”,而是先把程序的大框架立起来。
第二步:先补第一个方法 handle_comment
这题要求输出注释,所以我们先把“处理注释”的方法写出来。
在 HTMLParser 里,遇到 HTML 注释时,会自动调用:
handle_comment(self, data)
所以我们把它加到类里。
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def handle_comment(self, data):
pass
第三步:想清楚 handle_comment 里面要做什么
先不要急着写代码,先想逻辑。
题目要求:
- 如果注释是单行的,输出
>>> Single-line Comment - 如果注释是多行的,输出
>>> Multi-line Comment - 然后再输出注释内容本身
那么问题就变成:
怎么判断单行还是多行?
答案就是看注释内容里有没有换行符 \n。
也就是说:
- 如果
'\n' in data,就是多行注释 - 否则就是单行注释
所以这一段的逻辑可以先写成“中文思路”:
def handle_comment(self, data):
如果 data 里有 \n:
打印多行注释标题
否则:
打印单行注释标题
打印注释内容
第四步:把 handle_comment 真正写成代码
现在把刚才的思路翻译成 Python。
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def handle_comment(self, data):
if '\n' in data:
print(">>> Multi-line Comment")
else:
print(">>> Single-line Comment")
print(data)
这一步为什么这样写
这里最重要的是这个判断:
if '\n' in data:
它的意思是:
如果字符串
data里面包含换行符,就说明这个注释跨了多行。
然后无论是单行还是多行,最后都要输出注释内容本身,所以后面统一写:
print(data)
第五步:再补第二个方法 handle_data
题目除了注释,还要输出数据内容。
所以我们还需要处理“标签中的普通文本”。
在 HTMLParser 里,遇到普通文本时,会自动调用:
handle_data(self, data)
所以我们先把这个方法加上。
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def handle_comment(self, data):
if '\n' in data:
print(">>> Multi-line Comment")
else:
print(">>> Single-line Comment")
print(data)
def handle_data(self, data):
pass
第六步:想清楚 handle_data 里面要做什么
这一部分也先别急着写代码。
题目要求:
- 输出数据前先打印
>>> Data - 然后打印数据内容
- 但是如果
data == '\n',就不要打印
这说明 handle_data 的逻辑是:
def handle_data(self, data):
如果 data == '\n':
什么也不做,直接结束
否则:
先打印 >>> Data
再打印 data
这里的关键点是:
为什么要跳过 '\n'
HTML 中有时标签与标签之间本来就会有换行。
解析器看到这些换行,也会把它们当成 data。
但题目说了,这种只有一个换行符的内容不算有效数据,所以要忽略。
第七步:把 handle_data 真正写出来
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def handle_comment(self, data):
if '\n' in data:
print(">>> Multi-line Comment")
else:
print(">>> Single-line Comment")
print(data)
def handle_data(self, data):
if data == '\n':
return
print(">>> Data")
print(data)
为什么这里用 return
if data == '\n':
return
意思是:
如果这次读到的数据只是一个换行符,那这个方法就立刻结束,后面的打印代码都不执行。
这是一种很常见的写法。
先把“不需要处理的情况”排除掉,后面代码就更清楚。
到这里,类已经写完了
你现在已经完成了这题最核心的部分。
也就是说,这一段已经不是“半成品”了,而是一个真正能工作的解析器类:
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def handle_comment(self, data):
if '\n' in data:
print(">>> Multi-line Comment")
else:
print(">>> Single-line Comment")
print(data)
def handle_data(self, data):
if data == '\n':
return
print(">>> Data")
print(data)
接下来要做的事情其实只是:
- 把输入读进来
- 把读到的 HTML 交给这个解析器
第八步:开始写主程序,先读入 n
主程序第一步很简单,先读题目给的行数。
n = int(input())
这里是什么意思
input()读入一行内容,结果是字符串int(...)把字符串转成整数
所以这句的作用就是:
读入 HTML 总共有多少行。
第九步:准备一个列表,存下这 n 行 HTML
因为后面有多行输入,所以通常先准备一个列表。
html_lines = []
这个列表后面用来存每一行 HTML。
第十步:循环读入 n 行
for _ in range(n):
html_lines.append(input())
这里要会读
这段代码的意思是:
- 循环
n次 - 每次读入一行 HTML
- 放到
html_lines这个列表里
比如如果输入是:
4
<!--[if IE 9]>IE9-specific content
<![endif]-->
<div> Welcome to HackerRank</div>
<!--[if IE 9]>IE9-specific content<![endif]-->
那么最后 html_lines 里存的大概就是:
[
"<!--[if IE 9]>IE9-specific content",
"<![endif]-->",
"<div> Welcome to HackerRank</div>",
"<!--[if IE 9]>IE9-specific content<![endif]-->"
]
第十一步:把这几行拼成一个完整的 HTML 字符串
这一步很重要。
html = '\n'.join(html_lines)
为什么要拼接
因为题目给的是一个“HTML 片段”,它本来就是多行的。
我们希望把它恢复成完整文本,再交给解析器统一处理。
为什么中间要用 '\n'
因为原本每一行之间就是换行关系。
如果你直接写:
html = ''.join(html_lines)
那多行注释就会被错误地连在一起,换行信息丢了。
所以必须写成:
html = '\n'.join(html_lines)
这样才能保留原始结构。
第十二步:创建解析器对象
前面类已经写好了,现在就可以“实例化”它。
parser = MyHTMLParser()
这句可以理解成:
按照
MyHTMLParser这个类,真正造出一个解析器对象。
第十三步:把 HTML 喂给解析器
最后一步:
parser.feed(html)
这句是整题的执行开关
feed(html) 的意思是:
把这一大段 HTML 交给解析器,请你开始解析。
然后解析器就会自动扫描这段 HTML:
- 遇到注释,就调用
handle_comment - 遇到文本数据,就调用
handle_data
所以我们前面写的方法,这时才真正开始发挥作用。
把主程序补完整
现在把刚才的几步连起来:
n = int(input())
html_lines = []
for _ in range(n):
html_lines.append(input())
html = '\n'.join(html_lines)
parser = MyHTMLParser()
parser.feed(html)
最终完整代码
现在把“类 + 主程序”合起来,就是完整答案。
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def handle_comment(self, data):
if '\n' in data:
print(">>> Multi-line Comment")
else:
print(">>> Single-line Comment")
print(data)
def handle_data(self, data):
if data == '\n':
return
print(">>> Data")
print(data)
n = int(input())
html_lines = []
for _ in range(n):
html_lines.append(input())
html = '\n'.join(html_lines)
parser = MyHTMLParser()
parser.feed(html)
再带你手动走一遍执行过程
我们还是看样例输入:
4
<!--[if IE 9]>IE9-specific content
<![endif]-->
<div> Welcome to HackerRank</div>
<!--[if IE 9]>IE9-specific content<![endif]-->
第 1 步:读入 n
n = 4
第 2 步:读入 4 行 HTML
html_lines = [
"<!--[if IE 9]>IE9-specific content",
"<![endif]-->",
"<div> Welcome to HackerRank</div>",
"<!--[if IE 9]>IE9-specific content<![endif]-->"
]
第 3 步:拼接
html = "<!--[if IE 9]>IE9-specific content\n<![endif]-->\n<div> Welcome to HackerRank</div>\n<!--[if IE 9]>IE9-specific content<![endif]-->"
注意第一段注释里面保留了 \n,这就是后面能判断成多行注释的原因。
第 4 步:开始解析
parser.feed(html)
解析时会发生这些事:
先遇到第一段注释
它的内容是:
[if IE 9]>IE9-specific content
<![endif]
里面有 \n,所以输出:
>>> Multi-line Comment
[if IE 9]>IE9-specific content
<![endif]
然后遇到 <div> 里的文本
文本内容是:
Welcome to HackerRank
它不是单独的 '\n',所以输出:
>>> Data
Welcome to HackerRank
最后遇到单行注释
内容是:
[if IE 9]>IE9-specific content<![endif]
里面没有 \n,所以输出:
>>> Single-line Comment
[if IE 9]>IE9-specific content<![endif]
你现在应该真正记住的,不是代码,而是“写题顺序”
以后你再遇到这种题,不要一上来就盯着完整答案发懵。
你要按这个顺序思考:
第一步:先看要不要自定义一个类
这题要,因为要继承 HTMLParser。
第二步:先把类的空壳写出来
哪怕先写 pass,也比对着空白发呆强。
第三步:一个方法一个方法补
先写 handle_comment,再写 handle_data。
第四步:最后写主程序
主程序通常只是:
- 读输入
- 整理输入
- 调用前面写好的类
这就是很多题都能通用的“搭骨架”方法。
这题的易错点
易错点 1:忘了继承 HTMLParser
错误写法像这样:
class MyHTMLParser:
这样就没有解析 HTML 的能力了。
必须写成:
class MyHTMLParser(HTMLParser):
易错点 2:忘了跳过 data == '\n'
如果不写这句:
if data == '\n':
return
输出里就会多出很多没意义的空行数据。
易错点 3:把多行输入直接连起来了,没有加 \n
错误写法:
html = ''.join(html_lines)
这样会把原来的换行弄丢,影响多行注释判断。
正确写法:
html = '\n'.join(html_lines)
易错点 4:把注释标签一起打印出来
题目要打印的是注释内容,不是 <!-- 和 --> 本身。
好在 HTMLParser 已经帮你处理好了,handle_comment(self, data) 里的 data 只会是内部内容。
本节小结
这题从空白写出来,其实就是三件事:
第一,先继承 HTMLParser,写一个自己的解析器类。
第二,在类里写两个方法:一个处理注释,一个处理数据。
第三,主程序负责读入 HTML,然后 feed 给解析器。
真正要学会的是这种顺序:
先搭框架,再补方法,最后接主程序。
这比死记答案有用得多。
练习
你现在可以自己试着不看上面的完整代码,独立写这一题的“简化版”。
题目
输入:
3
<!--hello-->
<h1>Title</h1>
<!--a
b-->
请按原题要求输出结果。
提示
你可以按下面顺序自己做:
- 先写
class MyHTMLParser(HTMLParser): - 先补
handle_comment - 再补
handle_data - 主程序读入 3 行
- 用
'\n'.join(...)拼起来 - 最后
parser.feed(html)



