8.4. BaseHTMLProcessor.py
介绍
SGMLParser
自身不会产生任何结果。它只是分析,分析,再分析,对于它找到的有趣的东西会调用相应的一个方法,但是这些方法什么都不做。SGMLParser
是一个 HTML 消费者 (consumer):它接收 HTML,将其分解成小的、结构化的小块。正如您所看到的,在前一节中,您可以定义 SGMLParser
的子类,它可以捕捉特别标记和生成有用的东西,如一个网页中所有链接的一个列表。现在我们将沿着这条路更深一步。我们要定义一个可以捕捉 SGMLParser
所丢出来的所有东西的一个类,接着重建整个 HTML 文档。用技术术语来说,这个类将是一个 HTML 生产者 (producer)。
BaseHTMLProcessor
子类化 SGMLParser
,并且提供了全部的 8 个处理方法:unknown_starttag
、unknown_endtag
、handle_charref
、handle_entityref
、handle_comment
、handle_pi
、handle_decl
和 handle_data
。
例 8.8. BaseHTMLProcessor
介绍
class BaseHTMLProcessor(SGMLParser):
def reset(self):
self.pieces = []
SGMLParser.reset(self)
def unknown_starttag(self, tag, attrs):
strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
def unknown_endtag(self, tag):
self.pieces.append("</%(tag)s>" % locals())
def handle_charref(self, ref):
self.pieces.append("&#%(ref)s;" % locals())
def handle_entityref(self, ref):
self.pieces.append("&%(ref)s" % locals())
if htmlentitydefs.entitydefs.has_key(ref):
self.pieces.append(";")
def handle_data(self, text):
self.pieces.append(text)
def handle_comment(self, text):
self.pieces.append("<!--%(text)s-->" % locals())
def handle_pi(self, text):
self.pieces.append("<?%(text)s>" % locals())
def handle_decl(self, text):
self.pieces.append("<!%(text)s>" % locals())
[1] | reset 由 SGMLParser.__init__ 来调用。在调用父类方法之前将 self.pieces 初始化为空列表。self.pieces 是一个数据属性,将用来保存将要构造的 HTML 文档的片段。每个处理器方法都将重构 SGMLParser 所分析出来的 HTML,并且每个方法将生成的字符串追加到 self.pieces 之后。注意,self.pieces 是一个 list。也许您想将它定义为一个字符串,然后不停地将每个片段追加到它的后面。这样做是可以的,但是 Python 在处理 list 方面效率更高一些。 [5] |
[2] | 因为 BaseHTMLProcessor 没有为特别标记定义方法 (如在 URLLister 中的start_a 方法), SGMLParser 将对每一个开始标记调用 unknown_starttag 方法。这个方法接收标记 (tag ) 和属性的名字/值对的 list(attrs ) 两参数,重新构造初始的 HTML,接着将结果追加到 self.pieces 后。 这里的字符串格式化有些陌生,我们将留到下一节再说明。 |
[3] | 重构结束标记要简单得多,只是使用标记名字,把它包在 </...> 括号中。 |
[4] | 当 SGMLParser 找到一个字符引用时,会用原始的引用来调用 handle_charref 。如果 HTML 文档包含   这个引用,ref 将为 160 。重构原始的完整的字符引用只要将 ref 包装在 &#...; 字符中间。 |
[5] | 实体引用同字符引用相似,但是没有#号。重建原始的实体引用只要将 ref 包装在 &...; 字符串中间。(实际上,一位博学的读者曾经向我指出,除些之外还稍微有些复杂。仅有某种标准的 HTML 实体以一个分号结束;其它看上去差不多的实体并不如此。幸运的是,标准 HTML 实体集已经定义在 Python 的一个叫做 htmlentitydefs 的模块中了。从而引出额外的 if 语句。) |
[6] | 文本块则简单地不经修改地追加到 self.pieces 后。 |
[7] | HTML 注释包装在 <!--...--> 字符中。 |
[8] | 处理指令包装在 <?...> 字符中。 |
重要
HTML 规范要求所有非 HTML (像客户端的 JavaScript) 必须包括在 HTML 注释中,但不是所有的页面都是这么做的 (而且所有的最新的浏览器也都容许不这样做) 。BaseHTMLProcessor
不允许这样,如果脚本嵌入得不正确,它将被当作 HTML 一样进行分析。例如,如果脚本包含了小于和等于号,SGMLParser
可能会错误地认为找到了标记和属性。SGMLParser
总是把标记名和属性名转换成小写,这样可能破坏了脚本,并且BaseHTMLProcessor
总是用双引号来将属性封闭起来 (尽管原始的 HTML 文档可能使用单引号或没有引号) ,这样必然会破坏脚本。应该总是将您的客户端脚本放在 HTML 注释中进行保护。
例 8.9. BaseHTMLProcessor
输出结果
def output(self):
"""Return processed HTML as a single string"""
return "".join(self.pieces)
[1] | 这是在 BaseHTMLProcessor 中的一个方法,它永远不会被父类 SGMLParser 所调用。因为其它的处理器方法将它们重构的 HTML 保存在 self.pieces 中,这个函数需要将所有这些片段连接成一个字符串。正如前面提到的,Python 在处理列表方面非常出色,但对于字符串处理就逊色了。所以我们只有在某人确实需要它时才创建完整的字符串。 |
[2] | 如果您愿意,也可以换成使用 string 模块的 join 方法:string.join(self.pieces, "") 。 |
进一步阅读
- W3C 讨论了字符和实体引用。
- Python Library Reference 解答了您的怀疑,即
htmlentitydefs
模块的确名符其实。
Footnotes
[5] Python 处理 list 比字符串快的原因是:list 是可变的,但字符串是不可变的。这就是说向 list 进行追加只是增加元素和修改索引。因为字符串在创建之后不能被修改,像 s = s + newpiece
这样的代码将会从原值和新片段的连接结果中创建一个全新的字符串,然后丢弃原来的字符串。这样就需要大量昂贵的内存管理,并且随着字符串变长,所需要的开销也在增长。所以在一个循环中执行 s = s + newpiece
非常不好。用技术术语来说,向一个 list 追加 n
个项的代价为 O(n)
,而向一个字符串追加 n
个项的代价是 O(n<sup>2</sup>)
。