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

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

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

# ElementTree（通称ET）
PythonでXMLをツリー構造として操作できるライブラリです。

[Pythonのチュートリアル](https://docs.python.jp/3/library/xml.etree.elementtree.html)では``ET``という略称が公式で使われています。

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

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

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

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

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

```python
import xml.etree.ElementTree as ET

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

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

```python
root.text = 'data'
```

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

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

```python
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()``メソッドは最初に見つかった一つだけを取得します。

```python
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の構造を読み込むこともできます。

```python
import xml.etree.ElementTree as ET

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

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

```python
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の環境に合わせて作っているので、あまり汎用的なツールになってはいませんが、参考になれば幸いです。

```python: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
```

```xml: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](https://github.com/hamata0222/Dir2XML)