【Python】ディレクトリ構造をXML形式に変換する


概要

PythonでXMLを便利に扱えるElementTreeの使い方を紹介します。
あと少し自作ツールのお披露目です。

経緯

業務で使用したIDEが、プロジェクトにソースコードを1ファイルずつしか追加できないという衝撃的な仕様だったため、設定ファイルのXMLを直接作成しようということになりました。

ElementTree(通称ET)

PythonでXMLをツリー構造として操作できるライブラリです。

PythonのチュートリアルではETという略称が公式で使われています。

XMLの構造を保持する

ETでは、ツリー構造の一つの要素をElementクラスを使って保持します。
Elementクラスは、下記の属性を持っています。

  • tag
    • XMLのタグを表す属性
  • text
    • 要素のデータ
  • tail
    • アプリケーション固有の付加的なデータ
    • XMLの閉じタグの後に置かれるイメージ
  • attrib
    • 要素の属性
  • SubElement
    • 要素の子要素(Element型)

XMLのイメージでいうと、それぞれ下記の部分に該当します。

<tag attrib="attrib">
  text
  <SubElement>
    SubText
  <SubElement>
</tag>tail

XMLを構築する

ETを使って、XMLをツリー構造として構築していきます。
まずは、Element()メソッドでルートとなる要素を生成します。

import xml.etree.ElementTree as ET

root = ET.Element('root')   # rootタグを生成

データはtextに直接代入することで追加します。

root.text = 'data'

子要素はSubElement()で追加できます。
もしくは、Elementに直接append()を使って追加することもできます。

SubElement()は追加した要素を返してくれるので、要素の追加と取得を一度にできてお得です。

import xml.etree.ElementTree as ET

root = ET.Element('root')   # rootタグを生成
root.text = 'data'  # rootのデータとしてdataを設定

sub = ET.SubElement(root, 'sub')    # rootの子要素としてsubを追加
# root.append(ET.Element('sub'))    # こちらでも可能

要素の検索

ツリー構造の要素を検索するためにはElementクラスのfindall()メソッドもしくはfind()メソッドを利用します。
二つの違いは、findall()は指定したタグをすべて取得しますが、find()メソッドは最初に見つかった一つだけを取得します。

import xml.etree.ElementTree as ET

root = ET.Element('root')
sub1 = ET.SubElement(root, 'sub')
sub2 = ET.SubElement(root, 'sub')
sub3 = ET.SubElement(root, 'sub')

# findall()はリストが返ってくる
for ele in root.findall('sub'):
    ele.text = 'data'

# find()では最初の一つだけ
subEle = root.find('sub')
subEle.text = 'first'

ファイル入出力

ETはファイルからXMLの構造を読み込むこともできます。

import xml.etree.ElementTree as ET

ET.parse('example.xml') # example.xmlをツリー構造として取り込む

自分で作成したツリー構造をファイルに出力するためには、文字列に変換してからファイルに出力します。
文字列に変換するためにはtostring()メソッドを使います。

import xml.etree.ElementTree as ET

root = ET.Element('root')   # rootタグを生成
root.text = 'data'  # rootのデータとしてdataを設定

sub = ET.SubElement(root, 'sub')    # rootの子要素としてsubを追加
# root.append(ET.Element('sub'))    # こちらでも可能

fp = open('output.xml', 'w')
fp.write(ET.tostring(root, 'unicode'))  # 文字列に変換してからファイル出力

作成したツール

これまでの内容を使って、ディレクトリ構造をXML形式に変換するツールを作成しました。
業務で使っているIDEの環境に合わせて作っているので、あまり汎用的なツールになってはいませんが、参考になれば幸いです。

Dir2XML.py
# -*- coding: utf-8 -*-

import xml.etree.ElementTree as ET
import os
import sys
import re

# Define constants
tag_group='group'
tag_name='name'
tag_file='file'

# Main function
def main():
    if len(sys.argv) < 2:
        print('Please input the directory path which is as root: ')
        root_path = sys.stdin.readline().rstrip()
    else:
        root_path = sys.argv[1]

    root = ET.Element('root')

    print('Converting Directories to XML...')

    ele_curr = root
    for path, directories, files in os.walk(root_path):
        group_path = re.sub('^\\' + os.path.sep, '', path.replace(root_path, ''))
        if group_path != '':
            # the root path has no name, so skipped.
            ele_curr = find_named_element(root, group_path)
        for dir in directories:
            ele_curr.append(create_new_group(dir))
        for file in files:
            # This statement is specified for my tool.
            # $PROJ_DIR$ is context root for my tool.
            ele_curr.append(create_new_file(os.path.join('$PROJ_DIR$', group_path, file)))

    print('Convertion Completed!')

    output_file_name = os.path.join(root_path, 'result.xml')
    print('Output File : %s' %(output_file_name))
    output_to_file(root, output_file_name)

# Define functions
def find_named_element(ele, name):
    name_list = name.split(os.path.sep)
    ele_curr = ele
    for target in name_list:
        ele_pre = ele_curr
        for group in ele_curr.findall(tag_group):
            ele_name = group.find(tag_name)
            if ele_name.text == target:
                ele_curr = group
                break
        if ele_curr is ele_pre:
            ele_new = create_new_group(target)
            ele_curr.append(ele_new)
            ele_curr = ele_new

    return ele_curr

def create_new_element(name, tag):
    ele = ET.Element(tag)
    ele_name = ET.SubElement(ele, tag_name)
    ele_name.text = name

    return ele

def create_new_group(name):
    return create_new_element(name, tag_group)

def create_new_file(name):
    return create_new_element(name, tag_file)

def output_to_file(element, file_name):
    fp = open(file_name, 'w')
    fp.write(ET.tostring(element, 'unicode'))

main()

下記のディレクトリexampleをXML形式に変換してみます。

C:\EXAMPLE
│  file1.txt
│  file2.txt
│  
├─SubDir1
│      file1.txt
│      file2.txt
│      
└─SubDir2
    └─SubDir3
            file1.txt
            file2.txt

c:\>Dir2XML.py .\example
result.xml
<root>
  <group>
    <name>SubDir1</name>
    <file>
      <name>$PROJ_DIR$\SubDir1\file1.txt</name>
    </file>
    <file>
      <name>$PROJ_DIR$\SubDir1\file2.txt</name>
    </file>
  </group>
  <group>
    <name>SubDir2</name>
    <group>
      <name>SubDir3</name>
      <file>
        <name>$PROJ_DIR$\SubDir2\SubDir3\file1.txt</name>
      </file>
      <file>
        <name>$PROJ_DIR$\SubDir2\SubDir3\file2.txt</name>
      </file>
    </group>
  </group>
  <file>
    <name>$PROJ_DIR$\file1.txt</name>
  </file>
  <file>
    <name>$PROJ_DIR$\file2.txt</name>
  </file>
</root>

result.xmlは表示用に整形しています。実際は1行にすべて出力されています。

GitHubにも公開しています。
Dir2XML