xml.sax解析器与行号等

4 投票
2 回答
2964 浏览
提问于 2025-04-17 19:30

这个任务是解析一个简单的XML文档,并按行号分析内容。

看起来合适的Python库是xml.sax。但是我该怎么用呢?

在查阅文档后,我发现:

  • xmlreader.Locator接口提供了一个方法:getLineNumber()
  • handler.ContentHandler接口有setDocumentHandler()这个方法。

我的第一个想法是创建一个Locator,把它传给ContentHandler,然后在调用character()等方法时从Locator中读取信息。

但是,xmlreader.Locator只是一个空壳接口,它的任何方法都只能返回-1。作为一个可怜的用户,我该怎么办呢,难道要自己写一个完整的ParserLocator吗?

我会自己回答这个问题的。


(其实我本来会的,只是因为有个烦人的规定说我不能。)


我无法通过现有的文档(或者网上搜索)搞清楚这个问题,只好去阅读xml.sax的源代码(在我系统的/usr/lib/python2.7/xml/sax/下)。

xml.sax的函数make_parser()默认创建了一个真正的Parser,但那是什么东西呢?
在源代码中可以发现,它是一个ExpatParser,定义在expatreader.py里。而且……它有自己的Locator,叫ExpatLocator。但是,我们无法访问这个东西。解决这个问题的过程让我挠头不已。

  1. 写一个自己的ContentHandler,让它知道Locator,并用它来确定行号。
  2. xml.sax.make_parser()创建一个ExpatParser
  3. 创建一个ExpatLocator,把ExpatParser实例传给它。
  4. 创建ContentHandler,并给它这个ExpatLocator
  5. ContentHandler传给解析器的setContentHandler()
  6. Parser上调用parse()

例如:

import sys
import xml.sax

class EltHandler( xml.sax.handler.ContentHandler ):
    def __init__( self, locator ):
        xml.sax.handler.ContentHandler.__init__( self )
        self.loc = locator
        self.setDocumentLocator( self.loc )

    def startElement( self, name, attrs ): pass

    def endElement( self, name ): pass

    def characters( self, data ):
        lineNo = self.loc.getLineNumber()
        print >> sys.stdout, "LINE", lineNo, data

def spit_lines( filepath ):
    try:
        parser = xml.sax.make_parser()
        locator = xml.sax.expatreader.ExpatLocator( parser )
        handler = EltHandler( locator )
        parser.setContentHandler( handler )
        parser.parse( filepath )
    except IOError as e:
        print >> sys.stderr, e

if len( sys.argv ) > 1:
    filepath = sys.argv[1]
    spit_lines( filepath )
else:
    print >> sys.stderr, "Try providing a path to an XML file."

Martijn Pieters在下面指出了另一种方法,具有一些优点。如果正确调用了ContentHandler的父类初始化器,那么会发现一个看起来私有的、没有文档说明的成员._locator被设置了,它应该包含一个合适的Locator

优点:你不需要自己创建Locator(或者搞清楚怎么创建它)。

缺点:这没有任何文档说明,使用一个没有文档的私有变量显得很随意。

谢谢你,Martijn!

2 个回答

3

这是一个老问题,但我觉得可以给出比之前更好的答案,所以我还是想补充一下。

虽然在ContentHandler这个父类中,确实可能有一个叫做_locator的私有数据成员,正如上面Martijn提到的那样,但我觉得用这个数据成员来获取位置信息并不是它的正确用法。

我认为Steve White提出了一个好问题,为什么这个成员没有被文档化。我觉得答案可能是,它本来就不是给公众使用的。看起来它只是ContentHandler这个父类的一个私有实现细节。因为它没有文档说明,所以在未来的SAX库更新中,它可能会突然消失,因此依赖它可能会很危险。

从ContentHandler类的文档,特别是ContentHandler.setDocumentLocator的文档来看,设计者的意图是让用户重写ContentHandler.setDocumentLocator这个函数。这样,当解析器调用这个函数时,用户的内容处理子类可以保存传入的定位器对象的引用(这个对象是由SAX解析器创建的),然后可以在后面使用这个保存的对象来获取位置信息。例如:

class MyContentHandler(ContentHandler):
    def __init__(self):
        super().__init__()
        self._mylocator = None
        # initialize your handler

    def setDocumentLocator(self, locator):
        self._mylocator = locator

    def startElement(self, name, attrs):
        loc = self._mylocator
        if loc is not None:
            line, col = loc.getLineNumber(), loc.getColumnNumber()
        else:
            line, col = 'unknown', 'unknown'
        print 'start of {} element at line {}, column {}'.format(name, line, col)

采用这种方法,就不需要依赖那些没有文档说明的字段了。

4

SAX解析器本身应该给你的内容处理器提供一个定位器。这个定位器需要实现一些特定的方法,但只要它有这些方法,实际上可以是任何对象。xml.sax.xmlreader.Locator就是一个定位器应该实现的接口;如果解析器给你的处理器提供了一个定位器对象,那么你可以放心,这个定位器上会有那4个方法。

解析器只是被鼓励设置一个定位器,并不是必须要这样做。expat XML解析器是提供了这个功能的。

如果你创建一个子类,继承xml.sax.handler.ContentHandler(),那么它会为你提供一个标准的setDocumentHandler()方法,并且在处理器的.startDocument()方法被调用时,你的内容处理器实例会有self._locator被设置:

from xml.sax.handler import ContentHandler

class MyContentHandler(ContentHandler):
    def __init__(self):
        ContentHandler.__init__(self)
        # initialize your handler

    def startElement(self, name, attrs):
        loc = self._locator
        if loc is not None:
            line, col = loc.getLineNumber(), loc.getColumnNumber()
        else:
            line, col = 'unknown', 'unknown'
        print 'start of {} element at line {}, column {}'.format(name, line, col)

撰写回答