Python School 2.0.0 documentation

DOM, ElementTree, SAX

«  XML ファイルの処理方法   ::   Contents   ::   プロジェクトの構成  »

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 のダンプデータを処理して、膨大な記事から特徴的なテキストを抽出してみましょう。

宿題

IBM developerWorks には XML に関する記事が数多く寄稿されています。 “XML”, “Python” などで検索していくつかの記事を読んでみましょう。 特に、以下の記事には目を通して lxml を使ってみてください。 古びた記述を見つけた場合はまとめてみましょう。

«  XML ファイルの処理方法   ::   Contents   ::   プロジェクトの構成  »