Featured image of post Pythonでxmlファイルを操作する(1)

Pythonでxmlファイルを操作する(1)

IT関係の仕事を選び、最も良いと思うところはやはり色々な状況に置かれることによって得られる経験が多いということではないかと思います。なぜかというと、独学だけでは言語の基礎文法はわかっても実際のコーディングではどう設計したらいいか、どんなモジュール(ライブラリーを含め)を使ったらいいかわからない場合があるからです。そもそも何を作ればいいかわからない場合が多いですね。大分類としてはウェブアプリケーションか、バッチで動くコードかということなどがあり、細かくはどんなDBを使って、どんな作業をしたいか(結合する対象など)の詳細を一人で全部想定することはかなり難しい作業ではないかと思います。しかし仕事ではある程度要件が決まっているので、その結合の方法と必要な作業がわかればあとは頑張るだけですから。目標設定が何よりも重要だということはまさにこれのことかもしれません。

なので今回も仕事で任されたことです。要件はこうです。とあるツールを使って開発をしています。このツールではGUIでマップ上にアイコンを配置し、そのアイコンが一つの作業単位となっています。そしてアイコンとアイコンを結び、各アイコンにとある行動を設定することで全体なワークフローが出来上がる構造となっています。例えばDBのアイコンに接続先のDBの情報と発行するSQL文を入力し、ファイル出力のアイコンに繋ぐとそれを実行した時にDBからSQL文を実行した結果がファイルとして保存されます。このツールで作られたワークフローはxmlファイルとして保存されます。

問題は、開発環境と本場環境での設定が違うところがあるということです。主にこのツールを通してやっている作業はDB関係のものですが、開発環境と本場環境でそれぞれDBの接続先情報が違います。そしてそのDBの接続先の設定はxmlファイルに保存されているので、開発の終わったxmlファイルを本場環境にデプロイするときはただのコピーでは不完全なので「DBの接続先情報を書き換える」作業が必要です。この作業をどう実装したから今回のポストのテーマとなります。

事前準備

デプロイ先と、デプロイ元のサーバーは両方Linuxを使っています。そしてxmlファイルのソースはGitで管理することになっています。なのでまずデプロイ元からはGit pullし、DB情報を書き換えたあとはrsyncなどのコマンドでコピーすれば簡単に終わります。こちらの作業はJenkinsで自動化することにしました。ならば、残る問題はDB情報を書き換える作業をどう実装するかですね。

xmlを分析してみたら、各アイコンのタグの下にはそのアイコンの詳細設定情報がありました。DBの処理を行うアイコンは二つあって、一つ目はSELECTを発行する(以下、From)もので、二つ目はINSERTを発行する(以下、To)ものでした。FromとToで接続先のDBは種類も違って(片方はPostgreSQLで、片方はSQLServer)スキーマやテーブル名も違うので分けて処理する必要があります。

そして環境から考えると、Linuxで使える言語を選んだほうがいいでしょう。まずはシェルスクリプトを使ってコードを書いてみようと思いました。これが私の初のシェルスクリプトとなります。Javaよりは簡単ではないだろうか、という根拠のない自信からシェルを選びました。LinuxにはPythonも入っていましたが、そちらも触ったことがなかったのでまずシェルでやってみて、ダメだったらPythonに挑戦してみようかなと軽く思っていました。それが結果的には最初からPythonで書けばよかった…ってことになりましたが。

ともかくやりたいこと、環境、道具が揃ったので早速実装に入りました。

シェルスクリプトでコーディング

シェルは初めてだったので、試行錯誤が多かったです。最初に学んだのがJavaだったので、同じ感覚で書こうとしたら全然動きません。何回か失敗を重ねながら得られた結論は、関数を使うという考え方を捨てて、どうコマンドを組み合わせるかが重要だということでした。それに気づくにはだいぶ時間がかかりましたが、まず大事なことはわかったのであとはどんなことをするかですね。

まずはファイルを読み込むことからです。xmlファイルも結局はテキスト基盤なので、シェルでも読み込みはできます。For文一つで特定の拡張子をもつファイルを巡回しながら一行づつ読むことができるらしいです。そして既存のアイコン(このファイルを使うツールの表現を借りると、コンポーネント)のDB接続先の情報の行を把握し、書き換えれば完了。

ただ、前述したようにFromとToのコンポーネントを区別する必要があります。xmlファイルを覗くとどうやらコンポーネントの構造(タグの種類)はほぼ同じみたいなので、どう判定するかが問題でした。シェルではxmlをパーシングできるモジュールなどはないみたいですからね。それでまずは行数を比較して、Fromのコンポーネントがより上にあったら1番目に引っかかったDB設定がFromのやつだ、という風に判定することにしました。以下はその実装のコードです。

コードの例(シェルスクリプト)

#!/bin/bash
# 下のフォルダを巡回しながらxmlの拡張子を持つファイルを変数のfileNameに入れる
for fileName in `\find . -name '*.xml'`; do
    # コンポーネントの行数をつかめる(grepでコンポーネントのタグかを確認し、sedで行数を確保)
    getComponentLine=$(grep -n RDBGet ${fileName} | grep Component | sed -e 's/:.*//g')
    putComponentLine=$(grep -n RDBPut ${fileName} | grep Component | sed -e 's/:.*//g')

    # コネクションの行数をつかめる
    ConnectionOneLine=$(grep -n Connection ${fileName} | sed -e 's/:.*//g' | awk 'NR==1')
    ConnectionTwoLine=$(grep -n Connection ${fileName} | sed -e 's/:.*//g' | awk 'NR==2')

    # コネクション名をつかめる(cutでDB設定名だけを取り、awkで2種類以上の結果からどちらかを取る)
    ConnectionOneName=$(grep Connection ${fileName} | cut -d ">" -f 2 | cut -d "<" -f 1 | awk 'NR==1')
    ConnectionTwoName=$(grep Connection ${fileName} | cut -d ">" -f 2 | cut -d "<" -f 1 | awk 'NR==2')

    if [ "${getComponentLine}" -lt "${putComponentLine}" ]; then
        # get < putの場合ConnectionOneLineはgetのコネクション
        sed -i -e "${ConnectionOneLine} s/${ConnectionOneName}/HONBADBFROM/g" ${fileName}
        sed -i -e "${ConnectionTwoLine} s/${ConnectionTwoName}/HONBADBTO/g" ${fileName}
    else
        # get > putの場合ConnectionOneLineはputのコネクション
        sed -i -e "${ConnectionTwoLine} s/${ConnectionTwoName}/HONBADBFROM/g" ${fileName}
        sed -i -e "${ConnectionOneLine} s/${ConnectionOneName}/HONBADBTO/g" ${fileName}
    fi
done

問題点

ファイルの形式がxmlであり、タグのパーシングで確実にコンポーネントを分けて処理していない現行の方式ではあまり安全だとは言えない処理です。そしてこの方式だとFromとToのコンポーネントがそれぞれ一つづつある場合は大丈夫かもしれませんが、どちらかのコンポーネントが一つでも増えたら処理の方法を変えるしかないです。もしかしたらそんなケースが増えると、そのケースに合わせてそれぞれ違うコードを書く必要があるかもしれません。そしてそれはいちいちファイルをチェックしてそれにあうコードとマッチさせる必要がありますね。これなら手書きで変えるのとあまり変わらないのでは…と思いますね。

結果的に汎用性もなく、安全でもないコードとなってしまいました。こんなコードは本場では使えません。なので方法を変えることにしました。

Pythonで書き直す

次の方法として、Pythonを使ってちゃんとパーシングを行うことにしました。挑戦してみてからわかったのですが、こんな簡単な作業をするときはPythonが正解なのではないかと思うくらい簡単でした。それにLinuxの環境では基本的にPythonが入っている場合も多いようなので(yumがPythonを使う代表的な例です)、インストールしなくてもいいというのがメリットでもあります。それにBashがLinuxの基本機能であるのでPythonよりは速度が早いのではないかと思っていましたが、必ずしもそうでもないらしいですね。ならばますますシェルスクリプトにこだわる理由は無くなります。

ただLinuxに内蔵されているPythonは2が多いらしく(確認してみると、私の使うMacでもPython2が入っていました)、Pythonは2と3で文法が違うところも多くて特定の機能を使うには注意がいるらしいです。実際私の書いたコードでは、Python3でしか使えない部分があります。alternativeのようなコマンドでPythonのリンクを3に指定するという方法もありますが、それならPython2を使うプログラムで問題が起こる可能性があります。1なので最初からPython2のコードに書くか、実行するコードをPython3として実行するようにするか、Python2を使うプログラムの実行環境を変えるかの方法を工夫する必要がありました。

ここで私は、自分が書いたコードをPython3で実行するよう(Jenkinsに埋め込むので、そちらの設定を通しています)にしました。コードは以下となります。

コードの例(Python)

# -*- coding: UTF-8 -*-
# 日本語のコメントのために最初にエンコードを指定する

# xmlパーサーとフォルダからファイルを取得するモジュールをインポート
import xml.etree.ElementTree as ET
import glob

# 名前空間(prefix)をマップで宣言
ns = {'fb':'http://foo.com/builder', 'fe':'http://foo.com/engine', 'mp':'http://foo.com/mapper'}

# ファイル名を再帰的に取得(recursiveオプションはPython3専用)
fileList = glob.glob("**/HOGE*.xml", recursive=True)

# 取得したファイルを巡回しながらコネクション名の書き換え処理
for fileName in fileList:
    # ファイルをパーシング開始
    tree = ET.parse(fileName)

    # Toのコンポーネントで子要素であるコネクション名を取得(prefix内)
    putCon = tree.find("fe:WorkFlow/fe:Component[@type='RDB(Put)']/fe:Property[@name='Connection']", ns)
    putCon.text = putCon.text + 'x'
 
    # Fromのコンポーネントで子要素であるコネクション名を取得(prefix内)
    getCon = tree.find("fe:WorkFlow/fe:Component[@type='RDB(Get)']/fe:Property[@name='Connection']", ns)
    getCon.text = getCon.text + 'y'

    # 書き込みの時prefixが変わることを防止
    ET.register_namespace('fb', 'http://foo.com/builder')
    ET.register_namespace('fe', 'http://foo.com/engine')
    ET.register_namespace('mp', 'http://foo.com/mapper')

    # 書き換え処理
    tree.write(fileName, 'UTF-8', True)

最後に

コードの量もそんなに長くないし、ちゃんとパーシングで要素を捉えているのでシェルスクリプトに比べ安全な書き方になっています。それにタグによってコンポーネントを区別しているので、コンポーネントを数に変動があってもそのまま使えるという長所がありますね。タグに名前空間があると初期設定と書き込み直前にその処理が必要となるので少し面倒な部分はありますが2、確かにシェルスクリプトに比べ維持補修の面で手間がかからなくなったので満足できるコードを書けたと思います。速度も直接測定してみた訳ではないですが、相当早かったです(ただ単にPythonは遅いだろうという自分の偏見が問題だったかもしれません)。いやーPythonいいですね。

何よりもメインの関数やクラスを省略しても、本当にスクリプトぽい書き方でもちゃんと意図通りに動くということが素晴らしいですね。これからもLinux環境で簡単な反復作業を自動化したいという場合には、皆さんもぜひPythonを使ってみてくださいとオススメしたいくらいです。とても簡単な言語なので、これからもどんどん使って色々やってみたいなと思います。


  1. 例えば、yumで問題が発生しています。これはyumの実行環境を変える方法(/usr/bin/yumの設定を参照してください)もありますが、どれがPython2を使うかいちいち確認はできないのであまりおすすめしたくはない方法です。 ↩︎

  2. 特に最後の方のET.register_namespace()がないと、名前空間が勝手に変わってしまいます。 ↩︎

Built with Hugo
Theme Stack designed by Jimmy