8.8. 介绍 dialect.py

DialectizerBaseHTMLProcessor 的一个简单(而且有点傻)的子类。它通过一系列替换来处理文本块,但它确保 <pre>...</pre> 块内的任何内容都保持不变。

为了处理 <pre> 块,您需要在 Dialectizer 中定义两个方法:start_preend_pre

示例 8.17. 处理特定标签

    def start_pre(self, attrs):             1
        self.verbatim += 1                  2
        self.unknown_starttag("pre", attrs) 3

    def end_pre(self):                      4
        self.unknown_endtag("pre")          5
        self.verbatim -= 1                  6
1 每当 SGMLParserHTML 源代码中找到 <pre> 标签时,就会调用 start_pre。(稍后您将看到这是如何发生的。)该方法接受一个参数,attrs,它包含标签的属性(如果有)。attrs 是一个键/值元组列表,就像 unknown_starttag 接受的那样。
2 reset 方法中,您初始化了一个数据属性,作为 <pre> 标签的计数器。每次遇到 <pre> 标签时,您就增加计数器;每次遇到 </pre> 标签时,您就减少计数器。(您可以将其用作标志,并将其设置为 1 并将其重置为 0,但这样做同样简单,并且可以处理嵌套 <pre> 标签的奇怪(但可能)情况。)稍后,您将看到如何有效地使用此计数器。
3 就是这样,这就是您对 <pre> 标签所做的唯一特殊处理。现在,您将属性列表传递给 unknown_starttag,以便它可以执行默认处理。
4 每当 SGMLParser 找到 </pre> 标签时,就会调用 end_pre。由于结束标签不能包含属性,因此该方法不带任何参数。
5 首先,您要像处理任何其他结束标签一样执行默认处理。
6 其次,您减少计数器以表示此 <pre> 块已关闭。

在这一点上,值得进一步探讨一下 SGMLParser。我一直声称(到目前为止,您一直对此深信不疑),SGMLParser 会为每个标签查找并调用特定的方法(如果存在)。例如,您刚刚看到了用于处理 <pre></pre>start_preend_pre 的定义。但这是如何实现的呢?嗯,这不是魔术,只是优秀的 Python 编码。

示例 8.18. SGMLParser

    def finish_starttag(self, tag, attrs):               1
        try:                                            
            method = getattr(self, 'start_' + tag)       2
        except AttributeError:                           3
            try:                                        
                method = getattr(self, 'do_' + tag)      4
            except AttributeError:                      
                self.unknown_starttag(tag, attrs)        5
                return -1                               
            else:                                       
                self.handle_starttag(tag, method, attrs) 6
                return 0                                
        else:                                           
            self.stack.append(tag)                      
            self.handle_starttag(tag, method, attrs)    
            return 1                                     7

    def handle_starttag(self, tag, method, attrs):      
        method(attrs)                                    8
1 此时,SGMLParser 已经找到了一个开始标签并解析了属性列表。剩下要做的唯一事情是确定是否有针对此标签的特定处理程序方法,或者您是否应该回退到默认方法 (unknown_starttag)。
2 SGMLParser 的“魔力”不过是您的老朋友,getattr。您以前可能没有意识到的是,getattr 将在对象的子类以及对象本身中查找定义的方法。这里的对象是 self,即当前实例。因此,如果 tag'pre',则对 getattr 的此调用将在当前实例(它是 Dialectizer 类的一个实例)上查找 start_pre 方法。
3 如果在对象(或其任何子类)中找不到它要查找的方法,getattr 会引发 AttributeError,但这没关系,因为您将对 getattr 的调用包装在 try...except 块中并显式捕获了 AttributeError
4 由于您没有找到 start_xxx 方法,因此在放弃之前,您还将查找 do_xxx 方法。这种备用命名方案通常用于独立标签,例如 <br>,它们没有相应的结束标签。但是您可以使用任何一种命名方案;如您所见,SGMLParser 会为每个标签尝试这两种方案。(您不应该为同一个标签同时定义 start_xxxdo_xxx 处理程序方法;否则只会调用 start_xxx 方法。)
5 另一个 AttributeError,这意味着对 getattr 的调用失败,并带有 do_xxx。由于您既没有找到针对此标签的 start_xxx 方法,也没有找到 do_xxx 方法,因此您捕获异常并回退到默认方法 unknown_starttag
6 请记住,try...except 块可以有一个 else 子句,如果在 try...except 块期间 没有引发异常,则会调用该子句。从逻辑上讲,这意味着您 确实 为此标签找到了一个 do_xxx 方法,因此您将调用它。
7 顺便说一句,不要担心这些不同的返回值;从理论上讲,它们是有意义的,但实际上从未使用过。也不必担心 self.stack.append(tag)SGMLParser 在内部跟踪您的开始标签是否与相应的结束标签平衡,但它也不会对这些信息做任何处理。从理论上讲,您可以使用此模块来验证您的标签是否完全平衡,但这可能不值得,而且超出了本章的范围。您现在有更重要的事情要担心。
8 start_xxxdo_xxx 方法不会被直接调用;标签、方法和属性会被传递给这个函数 handle_starttag,以便子类可以覆盖它并改变 所有 开始标签的分派方式。您不需要这种级别的控制,因此您只需让此方法执行其操作,即使用属性列表调用方法(start_xxxdo_xxx)。请记住,method 是一个函数,从 getattr 返回,而函数是对象。(我知道您已经厌倦了听到这句话,我保证一旦我找不到利用它的方法,我就会停止说这句话。)在这里,函数对象作为参数传递给这个分派方法,而这个方法反过来又调用该函数。此时,您不需要知道该函数是什么,它的名称是什么,或者它是在哪里定义的;您唯一需要知道的关于该函数的信息是,它是用一个参数 attrs 调用的。

现在回到我们定期安排的程序:Dialectizer。当您离开时,您正在为 <pre></pre> 标签定义特定的处理程序方法。只剩下最后一件事要做,那就是使用预定义的替换来处理文本块。为此,您需要覆盖 handle_data 方法。

示例 8.19. 覆盖 handle_data 方法

    def handle_data(self, text):                                         1
        self.pieces.append(self.verbatim and text or self.process(text)) 2
1 调用 handle_data 时只有一个参数,即要处理的文本。
2 在祖先类 BaseHTMLProcessor 中,handle_data 方法只是将文本追加到输出缓冲区 self.pieces 中。这里的逻辑只稍微复杂一点。如果您正在处理 <pre>...</pre> 块,则 self.verbatim 将是大于 0 的某个值,并且您希望将文本原封不动地放入输出缓冲区中。否则,您将调用一个单独的方法来处理替换,然后将结果放入输出缓冲区中。在 Python 中,这可以使用 and-or 技巧 一行完成。

您快要完全理解 Dialectizer 了。唯一缺少的环节是文本替换本身的性质。如果您了解任何 Perl,您就会知道,当需要进行复杂的文本替换时,唯一真正的解决方案是正则表达式。dialect.py 中后面的类定义了一系列对 HTML 标签之间的文本进行操作的正则表达式。但是您刚刚学习了 整整一章关于正则表达式的知识。您真的不想再费力地学习正则表达式了吧?天知道我不想。我认为您在本章已经学到了足够多的东西。