【Django】CSVファイルをfixture形式のJSONに一瞬で変換する方法 (Pandas利用)

# 概要
DjangoのFixturesって初期DB構築時にとても便利ですが、エクセルとかCSV形式で作成したデータをfixtures形式のJSONに変換してくれるサービスやコードが見当たらなかったので作ってみました。

## 前提
下記サンプルコードと同ディレクトリにCSVファイルが入ったディレクトリとJSON（fixtures）を入れるディレクトリがある前提です。それぞれ`csv`、`fixtures`というディレクトリ名とします。

また、`pk`は`1`から順番に採番してある想定とします。

CSVファイルは以下を使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/3351a857-440f-a0f2-3119-d71bebc76969.png)


# サンプルコード

```py
import pandas as pd
import json
import os

csv_directory ='./csv'
fixtures_directory =  './fixtures'

for filename in os.listdir(csv_directory):
    if filename.endswith('.csv'):
        csv_file_path = os.path.join(csv_directory, filename)
        
        df = pd.read_csv(csv_file_path, dtype=str) 
        
        fixtures_data = []
        
        for _, row in df.iterrows():
            model = row['model']
            pk = int(row['pk'])
            fields = {key: str(row[key]) for key in row.index[2:]}
            
            fixture = {
                "model": model,
                "pk": pk,
                "fields": fields
            }
            fixtures_data.append(fixture)

        fixtures_filename = f"{os.path.splitext(filename)[0]}_initial_data.json"
        fixtures_file_path = os.path.join(fixtures_directory, fixtures_filename)
        with open(fixtures_file_path, 'w', encoding='utf-8') as outfile:
            json.dump(fixtures_data, outfile, ensure_ascii=False, indent=4)
        
print("Fixtures作成が完了しました。")
```

上記コードの実行により、`fixtures`ディレクトリにJSON形式で保存してくれます。
複数のCSVファイルがあってもいずれもそのファイル名をもとにJSONファイル名を指定するので、一括変換も楽です。

結果は以下の通りです。

```json
[
    {
        "model": "common.FruitCd",
        "pk": 1,
        "fields": {
            "fruit_cd": "1",
            "fruit_nm": "リンゴ"
        }
    },
    {
        "model": "common.FruitCd",
        "pk": 2,
        "fields": {
            "fruit_cd": "2",
            "fruit_nm": "みかん"
        }
    },
    {
        "model": "common.FruitCd",
        "pk": 3,
        "fields": {
            "fruit_cd": "3",
            "fruit_nm": "スイカ"
        }
    },
    {
        "model": "common.FruitCd",
        "pk": 4,
        "fields": {
            "fruit_cd": "4",
            "fruit_nm": "メロン"
        }
    },
    {
        "model": "common.FruitCd",
        "pk": 5,
        "fields": {
            "fruit_cd": "5",
            "fruit_nm": "いちご"
        }
    },
    {
        "model": "common.FruitCd",
        "pk": 6,
        "fields": {
            "fruit_cd": "6",
            "fruit_nm": "レモン"
        }
    },
    {
        "model": "common.FruitCd",
        "pk": 7,
        "fields": {
            "fruit_cd": "7",
            "fruit_nm": "キウイ"
        }
    },
    {
        "model": "common.FruitCd",
        "pk": 8,
        "fields": {
            "fruit_cd": "8",
            "fruit_nm": "バナナ"
        }
    }
]
```

## 詳細

一つ一つ詳細に解説。

- ①`os.listdir(csv_directory)`：指定されたCSVディレクトリ内のファイルリストを取得
- ②`filename.endswith('.csv')`：CSVファイルのみを処理対象としてフィルタリング
- ③`pd.read_csv()`：各CSVファイルをDataFrame（データテーブル）として読み込む
    - `dtype=str`としているのは、Pandasは自動的に列のデータ型を推測するようで、`001`みたいなコードがゼロ落ちすることがあるためです。この辺は突き詰めていくとかなり深くなるところですが...
- ④`for _, row in df.iterrows()`：DataFrameの行を反復処理。
    - `row`は、DataFrame内の各行のこと（Pandas Seriesオブジェクト）
    - `iterrows()`メソッドはDataFrameの各行を反復処理するためのメソッド
    - `_`部分は`i`でも`x`でも良いですが、`_`（アンダースコア）は一般的に「無視されるべき変数」として扱われます。上記コードの場合は、「行番号やインデックスは無視してる変数だよ〜」ってことになります。

> ようは、使わない返り値に変数を割り当てるのはナンセンスだから、アンダースコアで無視しましょうということ。
引用元：[【備忘録】Pythonにおけるアンダースコア"_"の役割について](https://qiita.com/ikki8412/items/ab482690170a3eac8c76)

- ⑤`pk = int(row['pk'])`
    - プライマリーキーなので空白値はない前提ですが、空白値を許容する場合は、以下のようにします。
```
pk = int(row['pk']) if not pd.isnull(row['pk']) else None
```

この場合は`null`が入ります。上記がない場合は以下のエラーになります。

```
ValueError: cannot convert float NaN to integer
```

- ⑥` {key: str(row[key]) for key in row.index[2:]}`：
- ⑦最初の2つの列（`model`と`pk`）を除いた残りの列のインデックスを取得した後、各列のインデックスをキーとして、その列の値を文字列に変換し、辞書のキーと値として追加。
- ⑧各行の内容を辞書として抽出し、特定のキー（'model'、'pk'、およびフィールド名）と値のペアを持つJSONフィクスチャデータに追加します。これにより、直後の`fixtures`には以下が格納されます。

```
{'model': 'common.FruitCd', 'pk': 1, 'fields': {'fruit_cd': '1', 'fruit_nm': 'リンゴ'}}
{'model': 'common.FruitCd', 'pk': 2, 'fields': {'fruit_cd': '2', 'fruit_nm': 'みかん'}}
{'model': 'common.FruitCd', 'pk': 3, 'fields': {'fruit_cd': '3', 'fruit_nm': 'スイカ'}}
{'model': 'common.FruitCd', 'pk': 4, 'fields': {'fruit_cd': '4', 'fruit_nm': 'メロン'}}
{'model': 'common.FruitCd', 'pk': 5, 'fields': {'fruit_cd': '5', 'fruit_nm': 'いちご'}}
{'model': 'common.FruitCd', 'pk': 6, 'fields': {'fruit_cd': '6', 'fruit_nm': 'レモン'}}
{'model': 'common.FruitCd', 'pk': 7, 'fields': {'fruit_cd': '7', 'fruit_nm': 'キウイ'}}
{'model': 'common.FruitCd', 'pk': 8, 'fields': {'fruit_cd': '8', 'fruit_nm': 'バナナ'}}
```


- ⑨`json.dump()`：作成されたJSONデータをファイルに書き込み
- ⑩ファイル名は元のCSVファイル名をベースに`'XXX_initial_data.json'`という名前で保存。
    - `{os.path.splitext(filename)[0]}`は、拡張子を取り除いたファイルのベース名を取得する方法。`os.path`モジュールの`splitext()`関数が分割してくれ、0番目の要素(=拡張子以外の名前)を取得します。

以上で、fixturesに変換することができました。
初期DB構築時に、まずはエクセルで大量のデータを準備する、というケースは多いと思うのですが、なぜかネット記事あさってもこのような記事に辿りつきませんでした。

こちらを使うと一括で複数のCSVファイルをFixtures形式のJSONファイルに変換できて楽です。
Django同志の一助になればと思います！