試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

PythonのExpatモジュールのXMLデータ解析処理で要素の階層を処理する

DocBook 5 XML文書の改訂履歴からRSS 2.0ファイルを生成するスクリプト(別館サイト向け)」の処理に関する追加メモとして、XMLファイルの解析に関する部分についてを扱う。
Expatのような(ストリーム/イベント駆動の)XML解析方式ではドキュメントのツリー構造は扱われず、現在処理中の要素に関する情報*1に一時的にアクセスできるだけという形となる(ランダムアクセスは不可)。そのため、「revhistoryの中のrevisionの中のrevnumberなのかどうか」といったような要素どうしの関係を判断することはできない。
そこで、今回はPythonのリストを用いて、現在処理されている要素の階層について

  1. StartElementHandlerの関数の中で要素名(1番目の引数)をappend()で追加
  2. EndElementHandlerの関数の中でpop()により最後に追加した要素名を削除

と処理する流れを作ることにし、そのリストの最初の要素を最上位要素,最後の要素(len()にリストを渡したものから1引いたもの)を現在処理中の要素として、要素の階層にアクセスできるようにした。ただし、ドキュメント全体のツリー構造を自由に移動したりすることはできず、現在処理している要素までの一本道の階層のみしかアクセスできない(例:['article', 'info', 'revhistory', 'revision', 'revnumber'])。
下は動作テスト例。テスト用のXMLデータも中に含んでいる。ただ、リストには要素名と一緒にxml:id属性もセットにしてタプルの形でデータを出し入れしている。「DocBook 5 XML文書の改訂履歴からRSS 2.0ファイルを生成するスクリプト(別館サイト向け)」のソースでも同様のことをしている。

#! /usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import print_function

from xml.parsers.expat import ParserCreate
from xml.parsers.expat import ExpatError
import sys


xmldata = '''<?xml version="1.0" encoding="utf-8" standalone="no"?>
<article xmlns="http://docbook.org/ns/docbook"
         version="5.0" xml:lang="ja">

 <info>
  <title>ドキュメントタイトル</title>

  <author>
   <personname>kakurasan</personname>
   <affiliation>
    <address><email>name@example.com</email></address>
   </affiliation>
  </author>

  <copyright>
   <year>2010</year>
   <holder>kakurasan</holder>
  </copyright>

  <abstract>
   <para>文書の概要</para>
  </abstract>

  <revhistory>
   <revision>
    <revnumber>2</revnumber>
    <date>2010/1/2</date>
    <revdescription>
     <itemizedlist role="navigation addedsection">
      <title>追加したセクション</title>
      <listitem><para><link linkend="sectname3" endterm="title.sectname3" /></para></listitem>
      <listitem><para><link linkend="sectname4" endterm="title.sectname4" /></para></listitem>
     </itemizedlist>
     <itemizedlist role="navigation modifiedsection">
      <title>修正したセクション</title>
      <listitem><para><link linkend="sectname1" endterm="title.sectname1" /> - 修正点1</para></listitem>
      <listitem><para><link linkend="sectname2" endterm="title.sectname2" /> - 修正点2</para></listitem>
     </itemizedlist>
    </revdescription>
   </revision>
   <revision>
    <revnumber>1</revnumber>
    <date>2010/1/1</date>
    <revdescription>
     <itemizedlist role="navigation addedsection">
      <title>追加したセクション</title>
      <listitem><para><link linkend="sectname1" endterm="title.sectname1" /></para></listitem>
      <listitem><para><link linkend="sectname2" endterm="title.sectname2" /></para></listitem>
     </itemizedlist>
    </revdescription>
   </revision>
  </revhistory>
 </info>

 <sect1 xml:id="about">
  <title xml:id="title.about">このドキュメントについて</title>
  <para>セクションの本文(以下略)</para>
 </sect1>

</article>'''


class XMLParserAndData:
  """
  XML解析器とデータ
  """
  __hierarchy = []  # 要素の階層・タプル([要素名], [xml:id])を保持するスタック
  __xmldata = None
  def __init__ (self, xmldata):
    self.__xmldata = xmldata
    self.__parser = ParserCreate ()
    self.__parser.buffer_text = True
    self.__parser.StartElementHandler = self.__on_element_start
    self.__parser.EndElementHandler = self.__on_element_end
    self.__parser.CharacterDataHandler = self.__on_character_data
  def parse (self):
    """
    解析の実行
    """
    self.__parser.Parse (self.__xmldata)
  def __on_element_start (self, name, attrs):
    """
    要素の開始
    """
    # 階層スタックに要素名とxml:id属性を積む
    if 'xml:id' in attrs:
      self.__hierarchy.append ((name, attrs['xml:id']))
    else:
      self.__hierarchy.append ((name, None))
    # 末尾の要素(self.__hierarchy[len (self.__hierarchy) - 1])が現在の要素となり
    # 前にたどると親要素,更にその親要素とたどれる
    if self.__hierarchy[len (self.__hierarchy) - 1][0] == 'revision' and \
     self.__hierarchy[len (self.__hierarchy) - 2][0] == 'revhistory' and \
     self.__hierarchy[len (self.__hierarchy) - 3][0] == 'info':
       print ('** new revision **')
  def __on_element_end (self, name):
    """
    要素の終了
    """
    last_elem_name, xmlid = self.__hierarchy.pop ()
    if last_elem_name != name:
      sys.exit ('Error: __on_element_end: last_elem_name != name')
  def __on_character_data (self, data):
    """
    文字列データ
    """
    hierarchy = ''  # 階層表示用文字列
    for elem, id in self.__hierarchy:
      hierarchy += '/{0}'.format (elem)
    # 現在の階層,xml:id,文字列データを表示
    if sys.version_info.major == 2:
      print ('{0}(id:{1}): "{2}"'.format (hierarchy, self.__hierarchy[len (self.__hierarchy) - 1][1], data.encode ('utf-8').strip ()))
    else:
      print ('{0}(id:{1}): "{2}"'.format (hierarchy, self.__hierarchy[len (self.__hierarchy) - 1][1], data.strip ()))
    # revision revhistory infoの順のときに
    # その下がrevnumberかdateの場合をチェックするものとする
    if self.__hierarchy[len (self.__hierarchy) - 2][0] == 'revision' and \
     self.__hierarchy[len (self.__hierarchy) - 3][0] == 'revhistory' and \
     self.__hierarchy[len (self.__hierarchy) - 4][0] == 'info':
      if self.__hierarchy[len (self.__hierarchy) - 1][0] == 'revnumber':
        print ('** revision revhistory info revnumber:{0} **'.format (data))
      elif self.__hierarchy[len (self.__hierarchy) - 1][0] == 'date':
        print ('** revision revhistory info date:{0} **'.format (data))


xml = XMLParserAndData (xmldata)
try:
  xml.parse ()
except ExpatError as e:
  sys.exit ('parse error: {0}'.format (e))

(2014/10/12)Python 3でも動作するように修正
これを実行すると以下のように表示される。

/article(id:None): ""
/article/info(id:None): ""
/article/info/title(id:None): "ドキュメントタイトル"
/article/info(id:None): ""
/article/info/author(id:None): ""
/article/info/author/personname(id:None): "kakurasan"
/article/info/author(id:None): ""
/article/info/author/affiliation(id:None): ""
/article/info/author/affiliation/address/email(id:None): "name@example.com"
/article/info/author/affiliation(id:None): ""
/article/info/author(id:None): ""
/article/info(id:None): ""
/article/info/copyright(id:None): ""
/article/info/copyright/year(id:None): "2010"
/article/info/copyright(id:None): ""
/article/info/copyright/holder(id:None): "kakurasan"
/article/info/copyright(id:None): ""
/article/info(id:None): ""
/article/info/abstract(id:None): ""
/article/info/abstract/para(id:None): "文書の概要"
/article/info/abstract(id:None): ""
/article/info(id:None): ""
/article/info/revhistory(id:None): ""
** new revision **
/article/info/revhistory/revision(id:None): ""
/article/info/revhistory/revision/revnumber(id:None): "2"
** revision revhistory info revnumber:2 **
/article/info/revhistory/revision(id:None): ""
/article/info/revhistory/revision/date(id:None): "2010/1/2"
** revision revhistory info date:2010/1/2 **
/article/info/revhistory/revision(id:None): ""
/article/info/revhistory/revision/revdescription(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist/title(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist/title(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist/listitem/para(id:None): "- 修正点1"
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist/listitem/para(id:None): "- 修正点2"
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription(id:None): ""
/article/info/revhistory/revision(id:None): ""
/article/info/revhistory(id:None): ""
** new revision **
/article/info/revhistory/revision(id:None): ""
/article/info/revhistory/revision/revnumber(id:None): "1"
** revision revhistory info revnumber:1 **
/article/info/revhistory/revision(id:None): ""
/article/info/revhistory/revision/date(id:None): "2010/1/1"
** revision revhistory info date:2010/1/1 **
/article/info/revhistory/revision(id:None): ""
/article/info/revhistory/revision/revdescription(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist/title(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription/itemizedlist(id:None): ""
/article/info/revhistory/revision/revdescription(id:None): ""
/article/info/revhistory/revision(id:None): ""
/article/info/revhistory(id:None): ""
/article/info(id:None): ""
/article(id:None): ""
/article/sect1(id:about): ""
/article/sect1/title(id:title.about): "このドキュメントについて"
/article/sect1(id:about): ""
/article/sect1/para(id:None): "セクションの本文(以下略)"
/article/sect1(id:about): ""
/article(id:None): ""

出力の中で「**」を含む行は現在処理中の要素の階層が特定の条件を満たしたときのもので、その行だけを抜き出すと

** new revision **
** revision revhistory info revnumber:2 **
** revision revhistory info date:2010/1/2 **
** new revision **
** revision revhistory info revnumber:1 **
** revision revhistory info date:2010/1/1 **

となる。

参考URL:

使用したバージョン:

*1:ハンドラにより要素名,開始タグの各種属性,文字列部分の中から1つもしくは2つ