Python(26)XPathでxmlの子要素からルートまで遡る方法

2017-09-02

旧ブログ

t f B! P L
xmlデータの子要素から親要素をルートまでたどりたかったのですが、適当なソフトをみつけられなかったのでPythonのスクリプトでたどることにしました。
(2017.11.4追記LibreOffice5(91)コンポーネントデータノードをルートまでたどるで結果をCalcに出力するマクロにしました。)

前の関連記事:Python(25)実行時間を計測する方法


xmlデータであるmain.xcdファイルの子要素からルートまでたどりたい


main.xcdファイルは/opt/libreoffice5.2/share/registryにあるLibreOfficeの設定ファイルです。

<prop oor:name="StartCenterBackgroundColor" oor:type="xs:int" oor:nillable="false">

今回ルートまでさかのぼりたい子要素はこのノードです。

このノードはタグがprop、属性がoor:name="StartCenterBackgroundColor" oor:type="xs:int" oor:nillable="false"で、コロンの前にあるoorやxsは名前空間です。

xmlデータを読み込んでルートを取得してみる

In [1]:
from xml.etree import ElementTree
xml.etree.ElementTree モジュールを使います。
In [2]:
tree = ElementTree.parse('/opt/libreoffice5.2/share/registry/main.xcd')
tree
Out[2]:
<xml.etree.ElementTree.ElementTree at 0xaea931ec>
xmlデータのあるファイルをparse()メソッドで読み込んで木(xml.etree.ElementTree.ElementTree)を取得します。
In [3]:
root = tree.getroot() 
root
Out[3]:
<Element '{http://openoffice.org/2001/registry}data' at 0xaea98b94>
木からgetroot()メソッドでルートノードを取得できます。
確認しただけで今回はルートノードは使いません。
波括弧で囲われた部分は名前空間です。
読み込んだファイルにあるルートノードをみてみると、xmlns:も続いて名前空間が定義されています。
この名前空間の定義はroot.attribとしても取得できませんでした。
どこで定義されているのかわかりませんが、main.xcdでは xml="http://www.w3.org/XML/1998/namespace" という名前空間も使われていました。

木からXPathで検索して子ノードを取得する

ルートはxmlデータにひとつしかないので、getroot()メソッドで取得出来ましたが、子ノードを取得するためにはルートからたどっていかないといけません。
しかし、今回はルートから子ノードへたどる経路がまだわからないので、まずXPathを使って、木から子ノードを検索して取得します。
20.5. xml.etree.ElementTree — ElementTree XML API — Python 3.5.3 ドキュメントにXPathの構文一覧がありますが、使い方の例はXML - Dive Into Python 3 日本語版がわかりやすいです。
クローラ作成に必須!XPATHの記法まとめ - Qiitaを読むとXPathはいろいろ複雑はクエリが組めるようですが、PythonのElementTreeではPythonのドキュメントに載っている構文一覧以外のものは使えないようです。
さらにXPathの先頭に、現在のノードを表す.をつけないといけない点も通常のXPathとは異なります。
でも先頭に.を付け忘れても、FutureWarning: This search is broken in 1.3 and earlier,... とはでてきますが、結果は取得できます。
<prop oor:name="StartCenterBackgroundColor" oor:type="xs:int" oor:nillable="false">
今回検索したいこの子要素は、属性が3つありますが、ElementTreeのXPathではandは使えないようなので、oor:name="StartCenterBackgroundColor"という要素だけを使います。
findall()メソッドの引数にこのXPathを入れて、ひとつだけ子ノードが入ったリストを取得出来ました。
名前空間のoor:の部分は自動的に変換してくれないので、 {http://openoffice.org/2001/registry} と入力しないといけません。
ElementTrueeオブジェクトから文字列にして書き出すときはregister_namespace(prefix, uri)を使えば自動的に名前空間を置換してくれ、findall()メソッドでも20.5.1.7. 名前空間のある XML の解析の例にあるように第二引数に名前空間の辞書を渡せば置換してくれますが、どうも汎用性に欠けるので使っていません。
XPathの最初のドットは先ほど述べたように現在のフォルダを表すドットです。
//は条件に一致するすべてのノードを選択することを表し、結果はリストで返ってきます。
続いてタグ名を指定し、角括弧の中では@に続いて属性の条件を指定しています。
タグ名を限定しない時はタグ名に変わって*を使います。
*を省略するとエラーになりました。

 XPathに..をつけて親ノードを取得する。


木から子ノードを検索するXPathが完成したら、あとはそのXPathに..を付け加えて行って親ノードを順番にルートノードまで取得すれば目的達成です。

問題は木から子ノードを検索したときに複数のノードが返ってくる時です。

複数のノードが返ってくるXPathの後ろに..を付けてその親ノードを取得すると、またすべての子ノードに対する親ノードが返ってきます。

各子ノードには親ノードは一つしか存在しないので、各親ノードについてその子ノードを調べて、元の子ノードが存在したときにその親ノードと決定することにしました。

GUI/getparentnode.py at 7e536193e0cfab42cf128f9db289e950477fba0e · p--q/GUI
# -*- coding: utf-8 -*-
from xml.etree import ElementTree
def traceToRoot():
 tree = ElementTree.parse('/opt/libreoffice5.2/share/registry/main.xcd')  # xmlデータからElementTreeオブジェクト(xml.etree.ElementTree.ElementTree)を取得する。ElementTree.parse()のElementTreeはオブジェクト名ではなくてモジュール名。
 xpath = './/prop[@oor:name="StartCenterBackgroundColor"]'  # 子ノードを取得するXPath。1つのノードだけ選択する条件にしないといけない。
#  xpath = './/prop[@oor:name="CaptionText"]'  # 子ノードを取得するXPath。複数の子ノードを返ってくる例。うまく動かない。
 namespaces = {"oor": "{http://openoffice.org/2001/registry}",\
    "xs": "{http://www.w3.org/2001/XMLSchema}",\
    "xsi": "{http://www.w3.org/2001/XMLSchema-instance}",\
    "xml": "{http://www.w3.org/XML/1998/namespace}"}  # 名前空間の辞書。replace()で置換するのに使う。
 replaceWithValue, replaceWithKey = createReplaceFunc(namespaces)
 xpath = replaceWithValue(xpath)  # 名前空間の辞書のキーを値に変換。
 nodes = tree.findall(xpath)  # 起点となる子ノードを取得。
 if len(nodes)==1:
  node = nodes[0]
  print(replaceWithKey(formatNode(node)))  # 名前空間の辞書の値をキーに変換して出力する。
  while node is not None:
   xpath ="{}..".format(xpath)  # 親ノードのxpathを取得。
   node = tree.find(xpath)  # 親ノードを取得。親はひとつのはずなのでfind()メソッドを使う。
   if node is not None:  # 親ノードが取得できたとき
    print(replaceWithKey(formatNode(node)))
 elif len(nodes)>1:  # 調べる子ノードが複数あるとき。
  for node in nodes:  # 各子ノードについて。
   print("\n{}".format(replaceWithKey(formatNode(node))))  # 名前空間の辞書の値をキーに変換して出力する。
   path = xpath  # 子ノードのxpathを取得。
   childnode = node  # 子ノードを取得。
   parentnodes = True
   while parentnodes:  # 親ノードのリストの要素があるときTrue。
    path ="{}..".format(path)  # 親ノードのxpathを取得。
    parentnodes = tree.findall(path)  # 親ノードのリストを取得。
    for parentnode in parentnodes:  # 各親ノードについて
     if childnode in list(parentnode):  # 親ノードに子ノードのオブジェクトが存在するとき。
      print(replaceWithKey(formatNode(parentnode)))  # 親ノードを出力。
      childnode = parentnode  # 親ノードを子ノードにする。
      break  # この階層を抜ける。
def formatNode(node):  # 引数はElement オブジェクト。タグ名と属性を出力する。属性の順番は保障されない。
 tag = node.tag  # タグ名を取得。
 attribs = []  # 属性をいれるリスト。
 for key, val in node.items():  # ノードの各属性について。
  attribs.append('{}="{}"'.format(key, val))  # =で結合。
 attrib = " ".join(attribs)  # すべての属性を結合。
 n = "{} {}".format(tag, attrib) if attrib else tag  # タグ名と属性を結合する。
 return "<{}>".format(n)     
def createReplaceFunc(namespaces):  # 引数はキー名前空間名、値は名前空間を波括弧がくくった文字列、の辞書。
 def replaceWithValue(txt):  # 名前空間の辞書のキーを値に置換する。
  for key, val in namespaces.items():
   txt = txt.replace("{}:".format(key), val)
  return txt
 def replaceWithKey(txt):  # 名前空間の辞書の値をキーに置換する。
  for key, val in namespaces.items():
   txt = txt.replace(val, "{}:".format(key))
  return txt
 return replaceWithValue, replaceWithKey
if __name__ == "__main__": 
 traceToRoot()
これでxpathで検索してでてきた各子ノードに対してルートノードまでたどれるようになりました。

名前空間は単純に文字列の置換として処理しています。
<prop oor:type="xs:int" oor:name="StartCenterBackgroundColor" oor:nillable="false">
<group oor:name="StartCenter">
<group oor:name="Help">
<component>
<oor:component-schema oor:name="Common" xml:lang="en-US" oor:package="org.openoffice.Office">
<oor:data>
このような結果が取得できます。

一番下がルートノードになります。
configreader = createConfigReader(ctx, smgr)
root = configreader("/org.openoffice.Office.Common/Help/StartCenter")
startcenterbackgroundcolor = root.getPropertyValue("StartCenterBackgroundColor")
LibreOffice5(68)画像フィルターリストの作成ででてきた汎用関数createConfigReader()を使うとこれでこのノードの値が取得できます。

(2018.3.23追記。親ノードの取得方法を変更しました。

まずxpathでrootまでたどってすべての親について子をキーとする辞書を作成しています。
# -*- coding: utf-8 -*-
from xml.etree import ElementTree
from collections import ChainMap 
def traceToRoot():
 tree = ElementTree.parse('/opt/libreoffice5.2/share/registry/main.xcd')  # xmlデータからElementTreeオブジェクト(xml.etree.ElementTree.ElementTree)を取得する。ElementTree.parse()のElementTreeはオブジェクト名ではなくてモジュール名。
#  xpath = './/prop[@oor:name="StartCenterBackgroundColor"]'  # 子ノードを取得するXPath。1つのノードだけ選択する条件にしないといけない。
 xpath = './/prop[@oor:name="CaptionText"]'  # 子ノードを取得するXPath。複数の子ノードを返ってくる例。うまく動かない。
 namespaces = {"oor": "{http://openoffice.org/2001/registry}",\
    "xs": "{http://www.w3.org/2001/XMLSchema}",\
    "xsi": "{http://www.w3.org/2001/XMLSchema-instance}",\
    "xml": "{http://www.w3.org/XML/1998/namespace}"}  # 名前空間の辞書。replace()で置換するのに使う。
 replaceWithValue, replaceWithKey = createReplaceFunc(namespaces)
 xpath = replaceWithValue(xpath)  # 名前空間の辞書のキーを値に変換。
 nodes = tree.findall(xpath)  # 起点となる子ノードを取得。
 maps = []  # 子ノードをキー、親ノードの値とする辞書のリスト。
 parentnodes = True
 while parentnodes:  # 親ノードのリストの要素があるときTrue。
  xpath = "/".join([xpath, ".."])  # 親のノードのxpathの取得。
  parentnodes = tree.findall(xpath)  # 親ノードのリストを取得。
  maps.append({c:p for p in parentnodes for c in p})  # 新たな辞書をリストに追加。
 parentmap = ChainMap(*maps)  # 辞書をChainMapにする。
 for c in nodes:  # 各子ノードについて。
  print("\n{}".format(replaceWithKey(formatNode(c))))  # 名前空間の辞書の値をキーに変換して出力する。
  while c in parentmap:  # 親ノードが存在する時。
   c = parentmap[c]  # 親ノードを取得。
   print(replaceWithKey(formatNode(c)))  # 親ノードを出力。
def formatNode(node):  # 引数はElement オブジェクト。タグ名と属性を出力する。属性の順番は保障されない。
 tag = node.tag  # タグ名を取得。
 attribs = []  # 属性をいれるリスト。
 for key, val in node.items():  # ノードの各属性について。
  attribs.append('{}="{}"'.format(key, val))  # =で結合。
 attrib = " ".join(attribs)  # すべての属性を結合。
 n = "{} {}".format(tag, attrib) if attrib else tag  # タグ名と属性を結合する。
 return "<{}>".format(n)     
def createReplaceFunc(namespaces):  # 引数はキー名前空間名、値は名前空間を波括弧がくくった文字列、の辞書。
 def replaceWithValue(txt):  # 名前空間の辞書のキーを値に置換する。
  for key, val in namespaces.items():
   txt = txt.replace("{}:".format(key), val)
  return txt
 def replaceWithKey(txt):  # 名前空間の辞書の値をキーに置換する。
  for key, val in namespaces.items():
   txt = txt.replace(val, "{}:".format(key))
  return txt
 return replaceWithValue, replaceWithKey
if __name__ == "__main__": 
 traceToRoot()
)

参考にしたサイト


20.5. xml.etree.ElementTree — ElementTree XML API — Python 3.5.3 ドキュメント
標準モジュールでサポートされているXPathは限定的です。

XML - Dive Into Python 3 日本語版
XML文書の中からノードを検索する例。

クローラ作成に必須!XPATHの記法まとめ - Qiita
フルサポートのXPathの使い方例。xml.etree.ElementTreeでは使えないものが多いです。

次の関連記事:Python(27)Essential SQLAlchemyのコード例を動かす

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ