【Python】Drawioで書いたER図をCSV等で出力するコード

今回の記事は、

https://qiita.com/moto3tt08/items/8ef4a3ffbe3fdce71537

こちらの記事に触発されて書いたものです。
記事の中で紹介されているコードを参考にしつつ、jsonやtsv形式のファイルを出力できるようにいじってみました。
VScodeの拡張機能であるdrawioについて、詳しくは、次の記事をご参照ください：

https://qiita.com/riku-shiru/items/5ab7c5aecdfea323ec4e

# 1. 作成したdrawio

![mydraw.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3275023/b24ca5fd-535d-78e5-93d5-730f0c229d7d.png)

ざっくりと主キー、外部キー、そのほかといった分類でフィールドを分けています。

**作成した`.drawio`拡張子のファイルからXMLを読み取る方法**

大きく分けて2つ方法があり、

1. ER図を作成後の`XXX.drawio`ファイルの拡張子`.drawio`を`XXX.xml`のように拡張子`.xml`に変更する
2. `.drawio`をテキストエディタで開きなおす。以下で手順を紹介。

手順１：画面上部のファイル名が表示されている部分で、右クリックを押下。
手順２：以下画像の、「Reopen Editor With...」を選択。（日本語だともう少しわかりやすいかも？）
![editorを開きなおす.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3275023/e6361303-39b8-91fd-e7fa-24c4c2cd7906.png)

手順３：コマンドパレットのプルダウンから「Text-Editor」を選択。
![select_TextEditor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3275023/f2cc31f5-3f5b-f57c-c0a1-67b8e683cee6.png)

すると、XMLを確認することができる。（見づらいのでキャプチャはなしで。百聞は一見に如かずということで。）

# 2. 処理の概要

XMLの中身を確認してみると、次のようになっていました：

1. テーブル名について：
styleの中にshape=table;が含まれているかつvalue属性に値あり（これがテーブル名にあたる）
2. テーブル名とフィールドの紐づけのための箱？：
styleの中にshape=table;が含まれているかつvalue属性に値なし（これは行にあたる）
3. フィールドのプロパティについて：
styleの中にshape=partialRectangle;が含まれている（これがフィールドのプロパティにあたる）

1.で「Customers」などのテーブル名を取得し、3.でテーブルのフィールドのプロパティ等を取得（主キーや外部キーの分別も可能）、そしてテーブルとフィールドを紐づける中間的なオブジェクトを2.で取得しているといったイメージ。

# 3. 実際のコード

（自分が）わかりやすくするために、変数を多めに設定してコードを記載：

```python: main.py
import os
import json
import xml.etree.ElementTree as ET
from datetime import datetime

# 元ネタ：https://qiita.com/moto3tt08/items/8ef4a3ffbe3fdce71537

now = datetime.strftime(datetime.now(), "%Y%m%d%H%M%S%f")
dirname = os.path.dirname(__file__)

tables = []
rows = []
fields = []
for file in os.listdir(dirname):
    basename, extension = os.path.splitext(file) # file名と拡張子を分離→タプル
    
    if extension == ".xml":
        root_tree = ET.parse(os.path.join(dirname, file)).getroot() # XMLのルートツリーを取得
        for cell in root_tree.iter("mxCell"):
            table = {}
            row = {}
            field = {}
            id = cell.get("id", "")
            parent = cell.get("parent", "")
            value = cell.get("value", "")
            style = cell.get("style", "")

            if "shape=table;" in style:
                table.update({"id": id, "table": value})
            elif "shape=tableRow;" in style:
                row.update({"id": id, "parent": parent})
            elif "shape=partialRectangle;" in style:
                if "PK" == value:
                    field.update({"id": id, "parent": parent, "PK": value})
                elif "FK" == value:
                    field.update({"id": id, "parent": parent, "FK": value})
                else:
                    field.update({"id": id, "parent": parent, "FIELD": value})
            
            if table:
                tables.append(table)
            if row:
                rows.append(row)
            if field:
                fields.append(field)

header = ["table_name", "field(pk)", "field(fk)", "field(others)"]
for row in rows:
    row_id = row.get("id")
    for field in fields:
        if row_id == field.get("parent"):
            field.pop("parent")
            row.update(field)

records_dict = [] # dict形式でテーブルを整理
records_comma = [] # カンマ区切りでテーブルを整理(複数フィールドがある場合は、パイプ区切り"|")
records_tab = [] # タブ区切りでテーブルを整理(複数フィールドがある場合は、パイプ区切り"|")
for table in tables:
    id = table.get("id")
    table_name = table.get("table")
    field_pk = []
    field_fk = []
    field_others = []
    for row in rows:
        if id == row.get("parent"):
            if row.get("PK",""):
                field_pk.append(row.get("FIELD")) 
            elif row.get("FK",""):
                field_fk.append(row.get("FIELD"))
            else:
                field_others.append(row.get("FIELD"))
    records_dict.append({
        "table_name": table_name,
        "field(pk)": field_pk,
        "field(fk)": field_fk, 
        "field(others)": field_others
    })
    records_comma.append(
        ",".join([table_name, "|".join(field_pk), "|".join(field_fk), "|".join(field_others)])
    )
    records_tab.append(
        "\t".join([table_name, "|".join(field_pk), "|".join(field_fk), "|".join(field_others)])
    )

json_file = f"{now}.json" # jsonファイル名
csv_file = f"{now}.csv" # csvファイル名
tsv_file = f"{now}.tsv" # tsvファイル名
with open(os.path.join(dirname, json_file), "w", encoding="utf-8") as w:
    w.write(json.dumps(records_dict,indent=4, ensure_ascii=False))
with open(os.path.join(dirname, csv_file), "w", encoding="utf-8") as w:
    w.write(",".join(header)+"\n")
    [w.write(record+"\n") for record in records_comma]
with open(os.path.join(dirname, tsv_file), "w", encoding="utf-8") as w:
    w.write("\t".join(header)+"\n")
    [w.write(record+"\n") for record in records_tab]
```

※VScodeの設定でタブをスペースに置換している場合、Excelに張り付けてもタブ区切りで分割できない。そのため参照のページで設定を変更する必要あり。（参照：https://boonx4m312s.hatenablog.com/entry/2022/06/24/210000）

上記コードを実行すると、`json, csv, tsv`の3つのファイルが作成されるはずです。以下はサンプルです。：

```20230919000000000000.json
[
    {
        "table_name": "Customers",
        "field(pk)": [
            "customer_id int NOT NULL"
        ],
        "field(fk)": [],
        "field(others)": [
            "customer_name char(50) NOT NULL",
            "Row 2",
            "Row 3"
        ]
    },
    {
        "table_name": "Orders",
        "field(pk)": [
            "order_id int NOT NULL"
        ],
        "field(fk)": [
            "customer_id int NOT NULL"
        ],
        "field(others)": [
            "order_date date NOT NULL",
            "Row 3"
        ]
    },
    {
        "table_name": "Shipments",
        "field(pk)": [
            "shipment_id int NOT NULL"
        ],
        "field(fk)": [
            "order_id int NOT NULL"
        ],
        "field(others)": [
            "shipment_date date NOT NULL",
            "Row 3"
        ]
    }
]
```

```20230919000000000000.csv
table_name,field(pk),field(fk),field(others)
Customers,customer_id int NOT NULL,,customer_name char(50) NOT NULL|Row 2|Row 3
Orders,order_id int NOT NULL,customer_id int NOT NULL,order_date date NOT NULL|Row 3
Shipments,shipment_id int NOT NULL,order_id int NOT NULL,shipment_date date NOT NULL|Row 3
```

```20230919000000000000.tsv
table_name	field(pk)	field(fk)	field(others)
Customers	customer_id int NOT NULL		customer_name char(50) NOT NULL|Row 2|Row 3
Orders	order_id int NOT NULL	customer_id int NOT NULL	order_date date NOT NULL|Row 3
Shipments	shipment_id int NOT NULL	order_id int NOT NULL	shipment_date date NOT NULL|Row 3
```

※複数のフィールドがある場合は、パイプ区切りで表現しています。

# 4. まとめ

今回はXMLからcsv, tsvおよびjsonファイルを作成するコードを紹介しました。
次回は、csvまたはtsvファイルなどからXMLを作成し、作図ができないか検証をしてみようと思います。