用一个简单例子看 attrs

题目考点

这道题主要考察:

  1. 输入输出处理
    • 第一行输入整数 N
    • 接下来读取 N 行 HTML 代码
  2. HTML 解析
    • 识别 HTML 标签,例如 <head><object><param />
    • 识别标签中的属性,例如 typewidth
    • 识别属性值,例如 "application/x-flash""0"
  3. 类与方法重写
    • 使用 Python 自带的 HTMLParser
    • 自己定义一个类,继承 HTMLParser
    • 重写处理标签的方法
  4. 忽略注释
    • 注释中的内容不能被检测
    • 例如:
<!-- <param name="movie" value="your-file.swf" /> -->

这一行里面虽然有 <param>,但是它在注释里,所以不能输出。


审题

题目给我们一个 HTML 代码片段,要我们按照出现顺序打印:

标签名
-> 属性名 > 属性值

如果一个标签没有属性,只打印标签名。

例如:

<object type="application/x-flash" width="0">

应该输出:

object
-> type > application/x-flash
-> width > 0

需要注意的是,题目只关心:

<开始标签>
<自闭合标签 />

不需要输出结束标签,例如:

</head>
</object>

这些结束标签不用打印。


思路提示

这道题不要一开始就想着自己手写正则表达式去匹配 HTML。HTML 的情况比较复杂,比如属性可能换行,注释可能跨多行。如果用正则,初学者很容易漏情况。

Python 已经提供了一个专门解析 HTML 的工具:

from html.parser import HTMLParser

我们可以把思路拆成三步:

第一步,定义一个自己的解析器类,让它继承 HTMLParser

第二步,重写两个方法:

handle_starttag()
handle_startendtag()

其中:

handle_starttag()

负责处理普通开始标签,例如:

<object>

而:

handle_startendtag()

负责处理自闭合标签,例如:

<param />

第三步,在这两个方法里打印标签名和属性。


完整设计思路

第一步:读取输入

题目第一行是整数 N,表示后面有多少行 HTML。

所以可以这样读:

n = int(input())

然后读取接下来的 N 行:

html_code = ""
for _ in range(n):
    html_code += input() + "\n"

这里把所有行拼接成一个完整字符串,是因为 HTML 注释可能跨多行。如果一行一行处理,有时不方便判断注释范围。


第二步:创建 HTML 解析器类

我们定义一个类:

class MyHTMLParser(HTMLParser):

它继承自 Python 自带的 HTMLParser

HTMLParser 会帮我们分析 HTML 结构。当它遇到不同内容时,会自动调用不同方法。

比如遇到开始标签:

<head>

它会调用:

handle_starttag()

遇到自闭合标签:

<param name="quality" value="high"/>

它会调用:

handle_startendtag()

第三步:处理普通开始标签

普通开始标签包括:

<head>
<title>
<object type="application/x-flash">

我们重写:

def handle_starttag(self, tag, attrs):

其中:

tag

表示标签名。

attrs

表示属性列表。

例如:

<object type="application/x-flash" width="0">

解析后大致是:

tag = "object"

attrs = [
    ("type", "application/x-flash"),
    ("width", "0")
]

所以我们先打印标签名:

print(tag)

然后遍历属性:

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

第四步:处理自闭合标签

自闭合标签例如:

<param name="quality" value="high"/>

它也要按照同样格式输出。

所以 handle_startendtag() 里的代码和 handle_starttag() 很像。


代码实现

完整代码如下:

from html.parser import HTMLParser


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

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


n = int(input())

html_code = ""
for _ in range(n):
    html_code += input() + "\n"

parser = MyHTMLParser()
parser.feed(html_code)

代码逐段解释

1. 导入工具

from html.parser import HTMLParser

这里引入的是 Python 自带的 HTML 解析工具。

它的作用是:帮我们识别 HTML 里的标签、属性、注释、文本等内容。

这道题中,我们主要用它来识别:

开始标签
自闭合标签
属性
属性值

2. 定义自己的解析器

class MyHTMLParser(HTMLParser):

这表示我们创建了一个自己的解析器类。

它继承了 HTMLParser 的能力,但我们可以重写里面的方法,让它按照题目要求打印内容。


3. 处理开始标签

def handle_starttag(self, tag, attrs):
    print(tag)
    for name, value in attrs:
        print(f"-> {name} > {value}")

当解析器遇到这种标签时:

<object type="application/x-flash" width="0">

会自动调用这个方法。

其中:

tag

是标签名:

object
attrs

是属性列表:

[("type", "application/x-flash"), ("width", "0")]

所以这段代码会输出:

object
-> type > application/x-flash
-> width > 0

4. 处理自闭合标签

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

自闭合标签长这样:

<param name="quality" value="high"/>

它不是普通开始标签,而是开始和结束写在一起。

所以需要单独重写 handle_startendtag()


运行演示

题目样例输入大概是:

9
<head>
<title>HTML</title>
</head>
<object type="application/x-flash"
  data="your-file.swf"
  width="0" height="0">
  <!-- <param name="movie" value="your-file.swf" /> -->
  <param name="quality" value="high"/>
</object>

程序读取完之后,交给:

parser.feed(html_code)

解析器开始分析 HTML。

首先遇到:

<head>

输出:

head

接着遇到:

<title>

输出:

title

遇到:

</title>

这是结束标签,不输出。

遇到:

</head>

还是结束标签,不输出。

然后遇到:

<object type="application/x-flash"
  data="your-file.swf"
  width="0" height="0">

这是一个开始标签,标签名是 object,有四个属性,所以输出:

object
-> type > application/x-flash
-> data > your-file.swf
-> width > 0
-> height > 0

然后遇到:

<!-- <param name="movie" value="your-file.swf" /> -->

这是注释,里面的 <param> 不算,所以不输出。

最后遇到:

<param name="quality" value="high"/>

这是自闭合标签,输出:

param
-> name > quality
-> value > high

所以最终输出是:

head
title
object
-> type > application/x-flash
-> data > your-file.swf
-> width > 0
-> height > 0
param
-> name > quality
-> value > high

这道题的关键理解

这道题表面上像是字符串处理题,但更准确地说,它是一个 HTML 解析题

你需要识别的不是普通字符串,而是 HTML 结构:

标签
属性
属性值
注释
结束标签

所以更适合使用专门的解析器,而不是自己硬拆字符串。


方法总结

以后看到类似题目,可以这样判断:

如果题目要求你从 HTML 中提取标签、属性、文本内容,优先考虑:

from html.parser import HTMLParser

然后根据题目要求重写不同的方法:

方法作用
handle_starttag()处理开始标签,例如 <div>
handle_startendtag()处理自闭合标签,例如 <br />
handle_endtag()处理结束标签,例如 </div>
handle_comment()处理注释
handle_data()处理标签之间的文本

这道题只需要输出开始标签和自闭合标签,所以只重写:

handle_starttag()
handle_startendtag()

不用处理结束标签,也不用处理注释。


小练习

请你尝试处理下面这个输入:

5
<html>
<body class="main">
<img src="logo.png" alt="logo"/>
<!-- <a href="test.html">test</a> -->
<a href="home.html">Home</a>

提示:

  1. <html> 没有属性,只输出标签名。
  2. <body class="main"> 有一个属性。
  3. <img ... /> 是自闭合标签。
  4. 注释中的 <a> 不能输出。
  5. 最后一行的 <a href="home.html"> 要输出标签和属性。

这种题目是要求我们继承类,然后重写方法吗?

那attrs的数据结构到底是什么形式?本质上解析是靠的那个被继承类的方法,我们写的方法,只不过是为了将这个类里面处理好的数据进行打印出来,能不能这样子理解

可以这样理解,而且你的理解已经接近本质了。

这类题的核心不是让我们从零写一个 HTML 解析器,而是让我们继承 Python 已经写好的 HTMLParser 类,然后重写它在解析过程中会自动调用的方法

1. 这道题是不是要求继承类、重写方法?

是的,这道题的标准思路基本就是:

from html.parser import HTMLParser

然后自己写一个类:

class MyHTMLParser(HTMLParser):
    ...

这里的意思是:

我不重新发明 HTML 解析器,而是使用 Python 已经提供好的 HTMLParser。
我只负责告诉它:当你解析到标签时,我希望你怎么处理。

所以我们要重写这些方法:

def handle_starttag(self, tag, attrs):
    ...

def handle_startendtag(self, tag, attrs):
    ...

这两个方法不是我们随便起的名字,而是 HTMLParser 规定好的方法名。

当解析器遇到普通开始标签时,它会自动调用:

handle_starttag()

当解析器遇到自闭合标签时,它会自动调用:

handle_startendtag()

例如:

<object type="application/x-flash" width="0">

会触发:

handle_starttag(self, tag, attrs)

而:

<param name="quality" value="high"/>

会触发:

handle_startendtag(self, tag, attrs)

2. attrs 的数据结构到底是什么?

attrs 的本质是一个列表 list,列表里面的每一项是一个二元组 tuple

形式是:

[
    ("属性名1", "属性值1"),
    ("属性名2", "属性值2"),
    ("属性名3", "属性值3")
]

也就是说,attrs 的结构可以理解成:

attrs = [
    (attribute_name, attribute_value),
    (attribute_name, attribute_value),
    ...
]

举个例子。

如果 HTML 是:

<object type="application/x-flash" width="0" height="0">

那么解析器传给我们的数据大概是:

tag = "object"

attrs = [
    ("type", "application/x-flash"),
    ("width", "0"),
    ("height", "0")
]

所以我们可以这样遍历:

for name, value in attrs:
    print(name, value)

这里的 name 会依次拿到:

type
width
height

value 会依次拿到:

application/x-flash
0
0

所以题目要求输出:

-> type > application/x-flash
-> width > 0
-> height > 0

代码就是:

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

3. 你的理解是否正确?

你说的这句话:

本质上解析是靠的那个被继承类的方法,我们写的方法,只不过是为了将这个类里面处理好的数据进行打印出来。

这个理解是对的。

更准确地说,可以这样表述:

HTMLParser 负责真正的 HTML 解析工作,比如识别哪里是标签、哪里是属性、哪里是注释、哪里是结束标签。
我们继承它之后,重写指定的 handle_... 方法,相当于定义“解析器遇到某类内容时应该做什么”。
在这道题里,我们做的事情很简单,就是把它已经解析好的 tagattrs 按题目格式打印出来。

也就是说:

parser.feed(html_code)

这一句才是真正启动解析的地方。

它会扫描整个 HTML 字符串。扫描过程中,如果遇到开始标签,就自动调用你写的:

handle_starttag()

如果遇到自闭合标签,就自动调用你写的:

handle_startendtag()

所以可以把整个过程理解成:

HTML 字符串
    ↓
parser.feed(html_code)
    ↓
HTMLParser 内部开始解析
    ↓
发现开始标签
    ↓
自动调用 handle_starttag(tag, attrs)
    ↓
我们在方法里打印 tag 和 attrs

4. 为什么我们不用自己写解析逻辑?

因为 HTML 的结构不只是简单的字符串切割。

比如这个标签跨了多行:

<object type="application/x-flash"
  data="your-file.swf"
  width="0" height="0">

如果你自己用字符串切割,就要考虑:

标签从哪里开始?
标签到哪里结束?
属性之间怎么分隔?
属性值里的引号怎么处理?
注释里的标签要不要忽略?

这些事情都很麻烦。

但是 HTMLParser 已经帮你处理好了。

它最终直接把结果整理成:

tag = "object"

attrs = [
    ("type", "application/x-flash"),
    ("data", "your-file.swf"),
    ("width", "0"),
    ("height", "0")
]

我们只需要输出。


5. 用一个简单例子看 attrs

假设输入里有一行:

<a href="https://example.com" target="_blank">Example</a>

解析器遇到 <a ...> 时,会调用:

handle_starttag("a", attrs)

其中:

tag = "a"

attrs = [
    ("href", "https://example.com"),
    ("target", "_blank")
]

所以这段代码:

def handle_starttag(self, tag, attrs):
    print(tag)
    for name, value in attrs:
        print(f"-> {name} > {value}")

会输出:

a
-> href > https://example.com
-> target > _blank

6. 这类题的本质套路

以后你看到题目说:

检测 HTML 标签
检测 HTML 属性
检测 HTML 注释
检测 HTML 文本内容

就要想到:

from html.parser import HTMLParser

然后看题目要你处理哪种内容。

常见方法是:

handle_starttag()       # 开始标签
handle_endtag()         # 结束标签
handle_startendtag()    # 自闭合标签
handle_data()           # 标签中间的文本
handle_comment()        # 注释

这道题只要求打印开始标签和自闭合标签,所以我们主要重写:

handle_starttag()
handle_startendtag()

7. 一个关键类比

可以把 HTMLParser 想象成一个已经写好的扫描机器。

它负责扫描:

<object type="text" width="100">

然后告诉你:

我发现了一个开始标签
标签名是 object
属性有 type=text,width=100

你写的 handle_starttag() 就是接收这个通知的地方。

所以不是你主动去拆 HTML,而是:

解析器发现东西
    ↓
解析器自动调用你的方法
    ↓
你在方法里处理结果

这就是“继承 + 重写方法”的用途。


小练习

判断下面这个标签被解析后,tagattrs 分别是什么:

<img src="logo.png" alt="site logo" width="200"/>

提示:

  1. tag 是标签名。
  2. attrs 是一个列表。
  3. 列表中的每一项是一个二元组:("属性名", "属性值")

文末附加内容
暂无评论

发送评论 编辑评论


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