xml.sax解析器与行号等
这个任务是解析一个简单的XML文档,并按行号分析内容。
看起来合适的Python库是xml.sax
。但是我该怎么用呢?
在查阅文档后,我发现:
xmlreader.Locator
接口提供了一个方法:getLineNumber()
。handler.ContentHandler
接口有setDocumentHandler()
这个方法。
我的第一个想法是创建一个Locator
,把它传给ContentHandler
,然后在调用character()
等方法时从Locator
中读取信息。
但是,xmlreader.Locator
只是一个空壳接口,它的任何方法都只能返回-1。作为一个可怜的用户,我该怎么办呢,难道要自己写一个完整的Parser
和Locator
吗?
我会自己回答这个问题的。
(其实我本来会的,只是因为有个烦人的规定说我不能。)
我无法通过现有的文档(或者网上搜索)搞清楚这个问题,只好去阅读xml.sax
的源代码(在我系统的/usr/lib/python2.7/xml/sax/下)。
xml.sax
的函数make_parser()
默认创建了一个真正的Parser
,但那是什么东西呢?
在源代码中可以发现,它是一个ExpatParser
,定义在expatreader.py里。而且……它有自己的Locator
,叫ExpatLocator
。但是,我们无法访问这个东西。解决这个问题的过程让我挠头不已。
- 写一个自己的
ContentHandler
,让它知道Locator
,并用它来确定行号。 - 用
xml.sax.make_parser()
创建一个ExpatParser
。 - 创建一个
ExpatLocator
,把ExpatParser
实例传给它。 - 创建
ContentHandler
,并给它这个ExpatLocator
。 - 把
ContentHandler
传给解析器的setContentHandler()
。 - 在
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 个回答
这是一个老问题,但我觉得可以给出比之前更好的答案,所以我还是想补充一下。
虽然在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)
采用这种方法,就不需要依赖那些没有文档说明的字段了。
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)