DOM, ElementTree, SAX¶
XML 処理で利用する API¶
XML 処理で利用する API としては、次の2つが有名です。
- SAX - Simple API for XML
- DOM - Document Object Model
多くの場合には DOM を使います。WebブラウザでXMLを扱う場合にも活用してくれます。 一方、XML データがメモリに乗り切らないほどに巨大である場合は SAX を使います。
その他に、Pull Parser という API もあります。 (近年は XML 自体の利用頻度が減少している影響もあってあまり活発には聞きませんが)
Python の場合、SAX と DOM の API は標準ライブラリに完備されており、 Python 2.5 以上では ElementTree も標準ライブラリとして利用できます。 高速化・簡略化したい場合は lxml を使いましょう。
ここでは、Maven で扱う POM ファイルを読み込み、 アーティファクトに関する情報をオブジェクトにマップするスクリプトを記述してみます。
POM ファイル (xml-1.xml) は以下のものを使います。
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>sample-group</groupId>
<artifactId>sample-group-commons</artifactId>
<version>1.0.0</version>
</project>
DOM のサンプル¶
Python スクリプト (xml-1.py):
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Parse Maven POM file.
"""
import logging
import os
from xml.dom.minidom import parse
from pyschool.cmdline import parse_args
class InvalidArtifact(Exception):
pass
class Artifact(object):
DEFAULT_GROUP = 'sample-group'
DEFAULT_VERSION = '1.0.0-SNAPSHOT'
def __init__(self, artifactId, groupId=None, version=None):
self.artifactId = artifactId
self.groupId = groupId or Artifact.DEFAULT_GROUP
self.version = version or Artifact.DEFAULT_VERSION
def __repr__(self):
return "%s/%s/%s" % (self.groupId, self.artifactId, self.version)
@staticmethod
def from_pom_file(pom):
xml = parse(pom)
els = xml.getElementsByTagName('artifactId')
if els:
artifact = Artifact(els[0].firstChild.data)
else:
raise InvalidArtifact("'artifactId' is missing in " + pom)
els = xml.getElementsByTagName('groupId')
if els:
artifact.groupId = els[0].firstChild.data
else:
logging.info("'groupId' is missing in " + pom)
els = xml.getElementsByTagName('version')
if els:
artifact.version = els[0].firstChild.data
else:
logging.info("'groupId' is missing in " + pom)
return artifact
def main():
args = parse_args()
fname = args.filename[0]
if not os.path.exists(fname):
raise SystemExit('"{}" is not found.'.format(fname))
artifact = Artifact.from_pom_file(fname)
print(artifact)
def test():
fname = "etc/xml-1.xml"
artifact = Artifact.from_pom_file(fname)
assert 'sample-group/sample-group-commons/1.0.0' == repr(artifact)
if __name__ == '__main__':
main()
# vim: set et ts=4 sw=4 cindent fileencoding=utf-8 :
実行結果
$ python xml-1.py xml-1.xml
sample-group/sample-group-commons/1.0.0
ElementTree のサンプル¶
DOM をそのまま扱うのは冗長な感じがありますので、多くの場合に何らかのライブラリを使います。 Python では標準モジュールの ElementTree が良い選択肢と言えます。
ElementTree モジュールを使うと次のように (xml-2.py) 記述できます。 ソースコードの分量はあまり変わりませんが、API の使い方としてはこちらの方が簡単でしょう。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Parse Maven POM file.
"""
import logging
import os
from xml.etree.ElementTree import parse
from pyschool.cmdline import parse_args
class InvalidArtifact(Exception):
pass
class Artifact(object):
NAMESPACE = "http://maven.apache.org/POM/4.0.0"
DEFAULT_GROUP = 'sample-group'
DEFAULT_VERSION = '1.0.0-SNAPSHOT'
def __init__(self, artifactId, groupId=None, version=None):
self.artifactId = artifactId
self.groupId = groupId or Artifact.DEFAULT_GROUP
self.version = version or Artifact.DEFAULT_VERSION
def __repr__(self):
return "%s/%s/%s" % (self.groupId, self.artifactId, self.version)
@staticmethod
def from_pom_file(pom):
xml = parse(pom)
element = xml.find('{%s}artifactId' % (Artifact.NAMESPACE,))
if element is None:
raise InvalidArtifact("'artifactId' is missing in " + pom)
artifact = Artifact(element.text)
element = xml.find('{%s}groupId' % (Artifact.NAMESPACE,))
if element is None:
logging.info("'groupId' is missing in " + pom)
else:
artifact.groupId = element.text
element = xml.find('{%s}version' % (Artifact.NAMESPACE,))
if element is None:
logging.info("'groupId' is missing in " + pom)
else:
artifact.version = element.text
return artifact
def main():
args = parse_args()
fname = args.filename[0]
if not os.path.exists(fname):
raise SystemExit('"{}" is not found.'.format(fname))
artifact = Artifact.from_pom_file(fname)
print(artifact)
def test():
fname = "etc/xml-1.xml"
artifact = Artifact.from_pom_file(fname)
assert 'sample-group/sample-group-commons/1.0.0' == repr(artifact)
if __name__ == '__main__':
main()
# vim: set et ts=4 sw=4 cindent fileencoding=utf-8 :
実行結果
$ python xml-2.py xml-1.xml
sample-group/sample-group-commons/1.0.0
SAX のサンプル¶
DOM はデータをメモリに読み込むため、実行マシンのメモリ量を超えてしまうような XML データを扱うことができません。 大きなデータを扱う場合にはストリーミング処理が必要になります。これを実現するのが SAX です。
xml-3.py ではタグの開始と終了にフックさせて、子要素のテキストを抽出しています。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Parse Maven POM file.
"""
import os
import logging
import xml.sax
from xml.sax.handler import ContentHandler
from pyschool.cmdline import parse_args
class Artifact(object):
DEFAULT_GROUP = 'sample-group'
DEFAULT_VERSION = '1.0.0-SNAPSHOT'
def __init__(self, artifactId, groupId=None, version=None):
self.artifactId = artifactId
self.groupId = groupId or Artifact.DEFAULT_GROUP
self.version = version or Artifact.DEFAULT_VERSION
def __repr__(self):
return "%s/%s/%s" % (self.groupId, self.artifactId, self.version)
class ArtifactParser(ContentHandler):
targets = (
"artifactId",
"groupId",
"version"
)
artifact = {}
_current = None
def startElement(self, name, attrs):
if name in self.targets:
self._current = name
self.artifact[self._current] = ''
def endElement(self, name):
pass
def characters(self, content):
c = content.strip()
if self._current and c:
self.artifact[self._current] += c
def main():
args = parse_args()
fname = args.filename[0]
if not os.path.exists(fname):
raise SystemExit('"{}" is not found.'.format(fname))
parser = ArtifactParser()
xml.sax.parse(fname, parser)
print(Artifact(**(parser.artifact)))
def test():
fname = "etc/xml-1.xml"
parser = ArtifactParser()
xml.sax.parse(fname, parser)
artifact = Artifact(**(parser.artifact))
assert 'sample-group/sample-group-commons/1.0.0' == repr(artifact)
if __name__ == "__main__":
main()
# vim: set et ts=4 sw=4 cindent fileencoding=utf-8 :
実行結果
$ python xml-3.py xml-1.xml
sample-group/sample-group-commons/1.0.0
ここでは比較のために同じ XML ファイルを扱っていますが、 たとえば Wikipedia のダンプデータを処理する場合には SAX が役に立ちます。 実際に自分で Wikipedia のダンプデータを処理して、膨大な記事から特徴的なテキストを抽出してみましょう。
- Index of /jawiki/latest/ - dumps.wikimedia.org
宿題¶
IBM developerWorks には XML に関する記事が数多く寄稿されています。 “XML”, “Python” などで検索していくつかの記事を読んでみましょう。 特に、以下の記事には目を通して lxml を使ってみてください。 古びた記述を見つけた場合はまとめてみましょう。