8.4. 介绍 BaseHTMLProcessor.py

SGMLParser 本身不会产生任何输出。它会不断地解析,并在找到每个感兴趣的内容时调用一个方法,但这些方法本身不做任何事情。SGMLParser 是一个 HTML 消费者:它接收 HTML 并将其分解成小的、结构化的片段。正如您在上一节中所见,您可以继承 SGMLParser 来定义捕获特定标签并生成有用内容的类,例如网页上所有链接的列表。现在,您将更进一步,定义一个类来捕获 SGMLParser 抛出的所有内容,并重建完整的 HTML 文档。用技术术语来说,这个类将是一个 HTML 生产者

BaseHTMLProcessor 继承自 SGMLParser 并提供了所有 8 个基本处理程序方法:unknown_starttagunknown_endtaghandle_charrefhandle_entityrefhandle_commenthandle_pihandle_declhandle_data

示例 8.8. 介绍 BaseHTMLProcessor


class BaseHTMLProcessor(SGMLParser):
    def reset(self):                        1
        self.pieces = []
        SGMLParser.reset(self)

    def unknown_starttag(self, tag, attrs): 2
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

    def unknown_endtag(self, tag):          3
        self.pieces.append("</%(tag)s>" % locals())

    def handle_charref(self, ref):          4
        self.pieces.append("&#%(ref)s;" % locals())

    def handle_entityref(self, ref):        5
        self.pieces.append("&%(ref)s" % locals())
        if htmlentitydefs.entitydefs.has_key(ref):
            self.pieces.append(";")

    def handle_data(self, text):            6
        self.pieces.append(text)

    def handle_comment(self, text):         7
        self.pieces.append("<!--%(text)s-->" % locals())

    def handle_pi(self, text):              8
        self.pieces.append("<?%(text)s>" % locals())

    def handle_decl(self, text):
        self.pieces.append("<!%(text)s>" % locals())
1 resetSGMLParser.__init__ 调用,在调用祖先方法之前将 self.pieces 初始化为空列表。self.pieces 是一个数据属性,它将保存您正在构建的 HTML 文档的片段。每个处理程序方法都将重建 SGMLParser 解析的 HTML,并将该字符串追加到 self.pieces 中。请注意,self.pieces 是一个列表。您可能会试图将其定义为字符串,并将每个片段追加到该字符串中。这可行,但 Python 处理列表的效率要高得多。[2]
2 由于 BaseHTMLProcessor 没有为特定标签定义任何方法(例如 URLLister 中的 start_a 方法),因此 SGMLParser 会为每个开始标签调用 unknown_starttag。此方法接收标签(tag)和属性名称/值对列表(attrs),重建原始 HTML,并将其追加到 self.pieces 中。这里的字符串格式化有点奇怪;您将在本章后面解开这个谜团(以及看起来很奇怪的 locals 函数)。
3 重建结束标签要简单得多;只需获取标签名称并将其括在 </...> 括号中即可。
4 SGMLParser 找到字符引用时,它会使用原始引用调用 handle_charref。如果 HTML 文档包含引用 &#160;,则 ref 将为 160。重建原始的完整字符引用只需将 ref 括在 &#...; 字符中即可。
5 实体引用类似于字符引用,但没有井号。重建原始实体引用需要将 ref 括在 &...; 字符中。(实际上,正如一位博学的读者向我指出的那样,它比这稍微复杂一些。只有某些标准 HTML 实体以分号结尾;其他看起来相似的实体则没有。幸运的是,标准 HTML 实体集定义在 Python 模块 htmlentitydefs 的字典中。因此,这里有一个额外的 if 语句。)
6 文本块直接追加到 self.pieces 中,不做任何更改。
7 HTML 注释括在 <!--...--> 字符中。
8 处理指令括在 <?...> 字符中。
Important
HTML 规范要求所有非 HTML 代码(如客户端 JavaScript)都必须包含在 HTML 注释中,但并非所有网页都能正确执行此操作(而且所有现代 Web 浏览器对此都很宽容)。BaseHTMLProcessor 则不宽容;如果脚本嵌入不正确,它将被解析为 HTML。例如,如果脚本包含小于号和等于号,SGMLParser 可能会错误地认为它找到了标签和属性。SGMLParser 始终将标签和属性名称转换为小写,这可能会破坏脚本,而 BaseHTMLProcessor 始终将属性值括在双引号中(即使原始 HTML 文档使用单引号或不使用引号),这肯定会破坏脚本。始终将您的客户端脚本保护在 HTML 注释中。

示例 8.9. BaseHTMLProcessor 输出

    def output(self):               1
        """Return processed HTML as a single string"""
        return "".join(self.pieces) 2
1 这是 BaseHTMLProcessor 中唯一一个永远不会被祖先类 SGMLParser 调用的方法。由于其他处理程序方法将其重建的 HTML 存储在 self.pieces 中,因此需要使用此函数将所有片段连接成一个字符串。如前所述,Python 擅长处理列表,而处理字符串的能力一般,因此只有在有人明确请求时,您才需要创建完整的字符串。
2 如果您愿意,可以使用 string 模块的 join 方法来代替:string.join(self.pieces, "")

扩展阅读

脚注

[2] Python 处理列表比处理字符串更好的原因是列表是可变的,而字符串是不可变的。这意味着向列表追加元素只会添加元素并更新索引。由于字符串在创建后不能更改,因此像 s = s + newpiece 这样的代码会从原始字符串和新片段的串联创建一个全新的字符串,然后丢弃原始字符串。这涉及大量的内存管理开销,并且随着字符串变长,所需的工作量也会增加,因此在循环中执行 s = s + newpiece 是非常低效的。用技术术语来说,向列表追加 n 个元素的时间复杂度为 O(n),而向字符串追加 n 个元素的时间复杂度为 O(n2)