4

任务是解析一个简单的 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()默认创建一个 real Parser,但那是什么东西?
在源代码中发现它是一个ExpatParser,定义在 expatreader.py 中。并且...它有自己的Locator, 一个ExpatLocator. 但是,这个东西是无法访问的。在这和解决方案之间出现了很多令人头疼的问题。

  1. 编写自己ContentHandler的 r,它知道Locator,并用它来确定行号
  2. 创建ExpatParser一个xml.sax.make_parser()
  3. 创建一个ExpatLocator,将ExpatParser实例传递给它。
  4. ContentHandler,给它这个ExpatLocator
  5. 传递ContentHandler给解析器setContentHandler()
  6. 呼吁。parse()_Parser

例如:

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(或了解如何创建它)。缺点:它没有记录在案,并且使用未记录的私有变量是草率的。

谢谢马丁!

4

2 回答 2

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)
于 2013-03-18T13:16:40.363 回答
2

这是一个老问题,但我认为有一个比给出的更好的答案,所以无论如何我都会添加另一个答案。

虽然在 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)

使用这种方法,无需依赖未记录的字段。

于 2018-10-01T12:23:47.797 回答