HTMLParser 的使用

题目考点

这道题主要考的是下面几个点:

  1. HTMLParser 的使用
  2. 继承类并重写回调方法
  3. 字符串判断,比如是否包含换行符 \n
  4. 输入处理:把多行 HTML 代码读进来
  5. 按题目要求格式输出

这题本质上不是让你“自己硬写一个 HTML 解析器”,而是让你学会用 Python 已经提供好的 HTMLParser


审题

我们先把题目翻译成更容易下手的话。

题目给你一段包含 N 行的 HTML 代码,要求你按出现顺序输出三类内容:

  1. 单行注释
  2. 多行注释
  3. 数据内容

这里最关键的是要分清:

什么是注释

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

这题的通用套路

遇到这种“解析过程中碰到什么就做什么”的题,通常按这个模式做:

  1. 继承系统提供的解析器类
  2. 重写对应的处理函数
  3. 在处理函数里完成输出或统计
  4. 最后把整段内容交给解析器去扫

这题真正难点不是代码,而是审题

你要抓住两个判断条件:

  1. 注释里有没有 \n
    • 有:多行注释
    • 没有:单行注释
  2. 数据是不是刚好等于 '\n'
    • 是:不输出
    • 不是:输出 data

本节小结

这题最核心的一句话就是:

不用自己分析 HTML 结构,而是借助 HTMLParser,在“注释回调”和“数据回调”里按规则打印。


练习

你可以先自己做这一题,不要急着看答案。

练习题

给定下面这段 HTML:

<!--hello-->
<p>Python</p>
<!--line1
line2-->

请按题目同样的规则输出结果。

提示

你可以先自己判断:

  1. 第一段注释是单行还是多行?
  2. <p>Python</p> 里面的 data 是什么?
  3. 最后一段注释里有没有 \n

这次我们不直接看答案,而是从空白开始搭出来

这道题很适合练一种能力:先把程序骨架搭起来,再一点点把细节填进去
你以后做题时,如果总觉得“我知道大概在考什么,但就是写不出来”,那就特别需要这种训练方式。

这次我们就严格按你说的顺序来:

  1. 先写类
  2. 再写方法
  3. 最后写主程序

先想清楚:这道题的程序长什么样

这题不是普通的“读入数字然后计算”的题,它更像是:

  • 我们先造一个“会解析 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-->

请按原题要求输出结果。

提示

你可以按下面顺序自己做:

  1. 先写 class MyHTMLParser(HTMLParser):
  2. 先补 handle_comment
  3. 再补 handle_data
  4. 主程序读入 3 行
  5. '\n'.join(...) 拼起来
  6. 最后 parser.feed(html)

文末附加内容
暂无评论

发送评论 编辑评论


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