XMLデータベース（BaseX）に格納されたXML文書をCSV形式に変換する（Python使用）

# 記事の内容

手元に複数のXMLファイルがあり、それらのデータを比較検討したいとします。
人間が複数のXMLファイルを見比べるのは辛いので、1つのXMLファイルにつき1行になるようなCSVファイルに変換することにします。
そのCSVファイルをExcel等で開けば、データの比較検討がやりやすくなるでしょう。
CSVへの変換はPythonで実装しましたが、コードの行儀があまり良くないのはお許しを。

## 環境

- OS：Windows10（試していませんが他のOSでも実行可能）
- XMLデータベース：BaseX 9.3.2
- Python 3.7.4

## 参考URL（感謝します）

- [【少しずつ追記】XMLデータベース「BaseX」のインストールからベンチマークまで](https://qiita.com/y_irabu/items/71f566709270623a2384)
- [GitHub BaseXdb/basex basex/basex-api/src/main/python/](https://github.com/BaseXdb/basex/tree/master/basex-api/src/main/python)
- [BaseX Documentation](https://docs.basex.org/wiki/Documentation)
- [W3C Recommendation XPath and XQuery Functions and Operators 3.1](http://www.w3.org/TR/xpath-functions-31/)
- [FunctX XQuery Functions](http://www.xqueryfunctions.com/xq/)

# BaseXにXML文書をロードする

まずは手元の複数のXML文書をBaseXにロードします。
[ここのコード](https://github.com/BaseXdb/basex/tree/master/basex-api/src/main/python) を参考にして、以下のPythonコードを書いて動かしました。

```py

from pathlib import Path
import os
import pprint
from BaseXClient import BaseXClient

# セッション作成
session = BaseXClient.Session('localhost', 1984, 'admin', 'admin')

try:
    # DBに格納するxmlファイル
    xml_directory = Path("C:\\") / "xml_data"
    list_xml_path = sorted(xml_directory.glob("*.xml"), key=os.path.getmtime)
    print("ロードするXMLファイル：")
    pprint.pprint(list_xml_path)

    # DBオープン
    session.execute("open testdb")
    print(session.info())

    # xmlファイルを読み込んでDBに追加する
    for path in list_xml_path:
        with open(path, mode='r', encoding="utf-8") as fi:
            str_xml = fi.read()

        session.add(path.name, str_xml)
        print(session.info())

    # DBの内容を表示
    print("\n" + session.execute("xquery /"))
    print("正常終了しました\n")

finally:
    # セッションを閉じる
    if session:
        session.close()
```

コードについて説明を補足しますと、

- 既存のデータベース「testdb」にXML文書をロードします。DB作成方法等は [この記事](https://qiita.com/y_irabu/items/71f566709270623a2384) が参考になります。
- XMLファイルの置き場所は「C:\xml_data」にしています。
- XMLファイルは、[このサイト](http://www.sotechsha.co.jp/xml/) の sample-1.xml ～ sample-8.xml をダウンロードし（感謝します）、中身を「encoding="UTF-8"」に書き換えてUTF-8形式で保存し直しました。
- XMLファイルは、ファイルのタイムスタンプが古い順にロードされます。

コードを実行すると、以下のように表示されてXML文書がロードされました。

```
ロードするXMLファイル：
[WindowsPath('C:/xml_data/sample-1.xml'),
 WindowsPath('C:/xml_data/sample-2.xml'),
 WindowsPath('C:/xml_data/sample-3.xml'),
 WindowsPath('C:/xml_data/sample-4.xml'),
 WindowsPath('C:/xml_data/sample-5.xml'),
 WindowsPath('C:/xml_data/sample-7.xml'),
 WindowsPath('C:/xml_data/sample-6.xml'),
 WindowsPath('C:/xml_data/sample-8.xml')]
Database 'testdb' was opened in 1.48 ms.

Resource(s) added in 3.72 ms.

Resource(s) added in 1.93 ms.

Resource(s) added in 8.89 ms.

Resource(s) added in 1.91 ms.

Resource(s) added in 2.05 ms.

Resource(s) added in 2.05 ms.

Resource(s) added in 1.93 ms.

（中略）

正常終了しました
```

# BaseXにクエリーを発行してCSVファイルに出力する


次に、BaseXにクエリーを発行して、1つのXML文書が1行になるようなCSVファイルを出力します。
以下のPythonコードを書いて動かしました。

```py

import pprint
from BaseXClient import BaseXClient


# 自前の例外クラス
class MyException(Exception):
    pass


# DBからroot要素のxpathをread
def read_xpath_root(session):
    set_xpath_root = set()

    query = f'''\
for $root in /*
return fn:path($root)
'''

    query_obj = session.query(query)
    query_obj.execute()

    # 全root要素のxpathを、重複を許さないsetに追加
    for typecode, item in query_obj.iter():
        set_xpath_root.add(item)

    print("root要素のXPath：")
    pprint.pprint(set_xpath_root)

    query_obj.close()

    # root要素は1種類のみ許容することにした。
    if len(set_xpath_root) == 1:
        pass
    else:
        msg = f"assert len(set_xpath_root)<{len(set_xpath_root)}> == 1"
        raise MyException(msg)

    return set_xpath_root.pop().replace("[1]", "[01]") + '/'


# DBからtext要素のxpathを全種類収集
def read_xpath_text(session):
    set_xpath_text = set()

    query = f'''\
for $text in //text()
return fn:path($text)
'''

    query_obj = session.query(query)
    query_obj.execute()

    # 全text要素のxpathを、重複を許さないsetに追加
    for typecode, item in query_obj.iter():
        set_xpath_text.add(item)

    query_obj.close()

    # sortしたいので、setからlistに移し替える
    list_xpath_text = []
    for xpath_text in set_xpath_text:
        list_xpath_text.append(
            # sortした時に[1]が[10]より後になってしまう問題に。とりあえずの対処
            xpath_text
                .replace("[1]", "[01]")
                .replace("[2]", "[02]")
                .replace("[3]", "[03]")
                .replace("[4]", "[04]")
                .replace("[5]", "[05]")
                .replace("[6]", "[06]")
                .replace("[7]", "[07]")
                .replace("[8]", "[08]")
                .replace("[9]", "[09]")
        )
    list_xpath_text.sort()
    print("text要素のXPath（全種類）：")
    pprint.pprint(list_xpath_text)
    return list_xpath_text


# セッション作成
session = BaseXClient.Session('localhost', 1984, 'admin', 'admin')

try:
    # DBオープン
    session.execute("open testdb")
    print(session.info())

    # root要素のxpathをread
    xpath_root = read_xpath_root(session)

    # text要素のxpathを全種類収集（csvの列名にする）
    list_xpath_text = read_xpath_text(session)

    # csvの行を返すクエリーを組み立てて発行する
    csv_header = "input_path"
    query = f'''\
for $root in /*
let $base_uri := fn:base-uri($root)
order by $base_uri
return <ROW>"{{fn:substring($base_uri, fn:string-length(db:name($root)) + 3)}}"\
'''

    # csvの列のループ
    for xpath_text in list_xpath_text:
        # xpath_textからcsvヘッダー用に適当に整形して、+=
        csv_header += ',' \
                      + xpath_text\
                          .replace(xpath_root, "")\
                          .replace("/text()[01]", "")\
                          .replace("Q{}", "")

        # xpath_textからXQuery用に整形して、+=
        query += ',"{' + xpath_text.replace(xpath_root, "$root/") + '}"'

    query += "</ROW>"

    # DB問い合わせ
    query_obj = session.query(query)
    query_obj.execute()

    # 出力ファイル名
    basename = xpath_root\
                       .replace("Q{}", "")\
                       .replace("[01]", "")\
                       .replace('/', "")

    # クエリ文出力
    with open(basename + "_xquery.txt", 'w') as fo:
        fo.write(query)
        fo.write('\n')

    # csv出力
    with open(basename + ".csv", 'w') as fo:
        # ヘッダー出力
        fo.write(csv_header)
        fo.write('\n')

        # 行出力
        for typecode, item in query_obj.iter():
            fo.write(item.replace("<ROW>", "").replace("</ROW>", '\n'))

    query_obj.close()
    print("正常終了しました\n")

finally:
    # セッションを閉じる
    if session:
        session.close()
```

コードについて説明を補足しますと、

- このコードは、DB内の全XML文書のroot要素名が同じであることを要求します。
- CSVファイル名はroot要素名を基に自動生成されます。
- XML文書からtext要素を抜き出して、CSVのデータとします。
- text要素のXPathを、CSVの列名とします。
- おおまかな手順としては、まず全XML文書からtext要素のXPathを全種類収集します（read_xpath_text関数で実装）。
- 次に、そのXPathを使ってXQuery文を組み立て、その問い合わせ結果をCSVに出力します。
- XMLは半構造データなので、クエリーに組み込んだXPathが存在しないXML文書があるかもしれません。XPathがXML文書に存在しない時は、その列データとして空文字がCSVに出力されます。
- CSVの第一列（列名=input_path）だけはXMLのtext要素からではなく、XQueryのdb:name関数の結果からXMLファイル名を抜き出して出力します。

コードを実行すると、以下のように表示されてCSVファイル「manyosyu.csv」が出力されました。

```
Database 'testdb' was opened in 1.12 ms.

root要素のXPath：
{'/Q{}manyosyu[1]'}
text要素のXPath（全種類）：
['/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[01]/Q{}image[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[01]/Q{}mean[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[01]/Q{}mkana[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[01]/Q{}pno[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[01]/Q{}poet[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[01]/Q{}yomi[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[02]/Q{}image[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[02]/Q{}mean[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[02]/Q{}mkana[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[02]/Q{}pno[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[02]/Q{}poet[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[02]/Q{}yomi[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[03]/Q{}image[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[03]/Q{}mean[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[03]/Q{}mkana[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[03]/Q{}pno[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[03]/Q{}poet[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[03]/Q{}yomi[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[04]/Q{}image[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[04]/Q{}mean[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[04]/Q{}mean[01]/text()[02]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[04]/Q{}mkana[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[04]/Q{}pno[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[04]/Q{}poet[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[04]/Q{}yomi[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[05]/Q{}image[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[05]/Q{}mean[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[05]/Q{}mkana[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[05]/Q{}pno[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[05]/Q{}poet[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[05]/Q{}yomi[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[06]/Q{}image[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[06]/Q{}mean[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[06]/Q{}mkana[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[06]/Q{}pno[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[06]/Q{}poet[01]/text()[01]',
 '/Q{}manyosyu[01]/Q{}volume[01]/Q{}poem[06]/Q{}yomi[01]/text()[01]']
正常終了しました
```

以下のクエリー文デバッグ用ファイルも出力されます。

```manyosyu_xquery.txt

for $root in /*
let $base_uri := fn:base-uri($root)
order by $base_uri
return <ROW>"{fn:substring($base_uri, fn:string-length(db:name($root)) + 3)}","{$root/Q{}volume[01]/Q{}poem[01]/Q{}image[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[01]/Q{}mean[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[01]/Q{}mkana[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[01]/Q{}pno[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[01]/Q{}poet[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[01]/Q{}yomi[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[02]/Q{}image[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[02]/Q{}mean[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[02]/Q{}mkana[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[02]/Q{}pno[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[02]/Q{}poet[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[02]/Q{}yomi[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[03]/Q{}image[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[03]/Q{}mean[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[03]/Q{}mkana[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[03]/Q{}pno[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[03]/Q{}poet[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[03]/Q{}yomi[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[04]/Q{}image[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[04]/Q{}mean[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[04]/Q{}mean[01]/text()[02]}","{$root/Q{}volume[01]/Q{}poem[04]/Q{}mkana[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[04]/Q{}pno[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[04]/Q{}poet[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[04]/Q{}yomi[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[05]/Q{}image[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[05]/Q{}mean[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[05]/Q{}mkana[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[05]/Q{}pno[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[05]/Q{}poet[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[05]/Q{}yomi[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[06]/Q{}image[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[06]/Q{}mean[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[06]/Q{}mkana[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[06]/Q{}pno[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[06]/Q{}poet[01]/text()[01]}","{$root/Q{}volume[01]/Q{}poem[06]/Q{}yomi[01]/text()[01]}"</ROW>
```

生成された「manyosyu.csv」をExcelで開いてみます。

![manyosyu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/6e16b80c-5827-852a-4cb7-f567e8b99a20.png)

DBにロードしたXMLファイル毎にCSVの行が生成されました。