10.5. 按节点类型创建单独的处理程序

第三个有用的 XML 处理技巧是根据节点类型和元素名称将代码分离到逻辑函数中。解析后的 XML 文档由各种类型的节点组成,每个节点都由一个 Python 对象表示。文档本身的根级别由 Document 对象表示。 Document 对象包含一个或多个 Element 对象(用于实际的 XML 标签),每个 Element 对象可以包含其他 Element 对象、Text 对象(用于文本片段)或 Comment 对象(用于嵌入式注释)。 Python 可以轻松编写调度程序来分离每种节点类型的逻辑。

示例 10.17. 解析后的 XML 对象的类名

>>> from xml.dom import minidom
>>> xmldoc = minidom.parse('kant.xml') 1
>>> xmldoc
<xml.dom.minidom.Document instance at 0x01359DE8>
>>> xmldoc.__class__                   2
<class xml.dom.minidom.Document at 0x01105D40>
>>> xmldoc.__class__.__name__          3
'Document'
1 假设 kant.xml 位于当前目录中。
2 正如您在第 9.2 节“包”中看到的,解析 XML 文档返回的对象是一个 Document 对象,该对象在 xml.dom 包的 minidom.py 中定义。正如您在第 5.4 节“实例化类”中看到的,__class__ 是每个 Python 对象的内置属性。
3 此外,__name__ 是每个 Python 类的内置属性,它是一个字符串。这个字符串并不神秘;它与您自己定义类时键入的类名相同。(请参阅第 5.3 节“定义类”。)

好的,现在您可以获取任何特定 XML 节点的类名(因为每个 XML 节点都表示为一个 Python 对象)。如何利用这一点来分离解析每种节点类型的逻辑?答案是 getattr,您在第 4.4 节“使用 getattr 获取对象引用”中第一次看到它。

示例 10.18. parse,一个通用的 XML 节点调度程序

    def parse(self, node):          
        parseMethod = getattr(self, "parse_%s" % node.__class__.__name__) 1 2
        parseMethod(node) 3
1 首先,请注意,您正在根据传递给您的节点(在 node 参数中)的类名构造一个更大的字符串。因此,如果您传递了一个 Document 节点,则您正在构造字符串 'parse_Document',依此类推。
2 现在,您可以将该字符串视为函数名,并使用 getattr 获取对函数本身的引用。
3 最后,您可以调用该函数并将节点本身作为参数传递。下一个示例显示了这些函数中每个函数的定义。

示例 10.19. 由 parse 调度程序调用的函数

    def parse_Document(self, node): 1
        self.parse(node.documentElement)

    def parse_Text(self, node):    2
        text = node.data
        if self.capitalizeNextWord:
            self.pieces.append(text[0].upper())
            self.pieces.append(text[1:])
            self.capitalizeNextWord = 0
        else:
            self.pieces.append(text)

    def parse_Comment(self, node): 3
        pass

    def parse_Element(self, node): 4
        handlerMethod = getattr(self, "do_%s" % node.tagName)
        handlerMethod(node)
1 parse_Document 只会被调用一次,因为一个 XML 文档中只有一个 Document 节点,并且解析后的 XML 表示中只有一个 Document 对象。它只是简单地解析语法文件的根元素。
2 parse_Text 在表示文本片段的节点上调用。该函数本身执行一些特殊处理来处理句子第一个单词的自动大写,但除此之外只是将表示的文本追加到列表中。
3 parse_Comment 只是一个 pass,因为您不关心语法文件中的嵌入式注释。但是请注意,您仍然需要定义该函数并明确使其不执行任何操作。如果该函数不存在,则通用 parse 函数会在遇到注释时立即失败,因为它会尝试查找不存在的 parse_Comment 函数。为每种节点类型定义一个单独的函数,即使是您不使用的函数,也可以使通用 parse 函数保持简单和愚蠢。
4 parse_Element 方法本身实际上是一个调度程序,它基于元素标签的名称。基本思想是相同的:获取区分元素的因素(它们的标签名称),并为每个元素调度到一个单独的函数。您构造一个字符串,例如 'do_xref'(对于 <xref> 标签),找到该名称的函数,然后调用它。对于在解析语法文件过程中可能找到的其他标签名称(<p> 标签、<choice> 标签)依此类推。

在本例中,调度函数 parseparse_Element 只是在同一个类中查找其他方法。如果您的处理非常复杂(或者您有许多不同的标签名称),您可以将代码分解成单独的模块,并使用动态导入来导入每个模块并调用您需要的任何函数。动态导入将在第 16 章“函数式编程”中讨论。