4.4. BaseHTMLProcessor.py介绍

SGMLParser 自身不会产生任何绍果。它只是分析,分析,再分析,对于它找到的有趣的东西会调用想应的一个方法,但是这些方法什么都不做。SGMLParser 是一个HTML消费者(consumer):它接收HTML,将其分解成小的、结构化的小块。正如你所看到的,在前一节中,你可以通过将 SGMLParser 子类化来定义一个类,它可以捕捉特别标记和生成有用东西,如一个网页中所有链接的一个列表。现在我们将沿着这条路更深一步。我们要定义一个可以捕捉 SGMLParser 所丢出来的所有东西的一个类,接着重建整个HTML文档。用技术术语来说,这个类将是一个HTML生产者(producer)

BaseHTMLProcessor 子类化 SGMLParser ,并且提供了全部的8个处理器方法:unknown_starttagunknown_endtaghandle_charrefhandle_entityrefhandle_commenthandle_pihandle_decl,和handle_data

例 4.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在处理列表方面效率更高一些。[8]

2

因为 BaseHTMLProcessor 没有为特别标记定义方法(如在 URLLister 中的 start_a 方法),SGMLParser 将对每一个开始标记调用 unknown_starttag 方法。这个方法接收标记(tag)和属性的名字/值对的列表(attrs)两参数,重新构造初始的HTML,接着将结果追加到 self.pieces 后。这里的 字符串格式化 有些陌生,我们将留到下一节再说明。

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注释中,但不是所有的页面都是这么做的(而且所有的最新的浏览器也都容许不这样做)。BaseHTMLProcessor 不允许这样,如果脚本嵌入的不正确,它将被当作HTML一样进行分析。例如,如果脚本包含了小于和等于号,SGMLParser 可能会错误地认为找到了标记和属性。SGMLParser 总是把标记名和属性名转换成小写,这样可能破坏了脚本,并且 BaseHTMLProcessor 总是用双引号来将属性封闭起来(尽管原始的HTML文档可能使用单引号或没有引号),这样必然会破坏脚本。应该总是将你的客户端脚本放在注释中进行保护。

例 4.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, "")


进一步阅读

脚注

[8] Python在处理列表比字符串快的原因是:列表是可变的,但字符串是不可变的。这就是说向列表进行追加只是增加元素和修改索引。因为字符串在创建之后不能被修改,象 s = s + newpiece 这样的代码将会从原值和新片段的连接结果中创建一个全新的字符串,然后丢弃原来的字符串。这样就需要大量昂贵的内存管理,并且随着字符串变长,所需要的开销也在增长。所以在一个循环中执行 s = s + newpiece 非常不好。用技术术语来说,向一个列表追加 n 个项的代价为 O(n),而向一个字符串追加 n 个项的代价是 O(n2)