1.初めに

この記事ではMacOS/LinuxでPythonでJsonSchemaを用いたcsvバリデーションを行う方法を解説します。

2.前提

動作環境

・python: 3.11.0
・ライブラリ:jsonschema: 4.17.3
・実行環境: mac OS 13.5

3.環境のセットアップ

下記の環境のセットアップにおいて、
venvを利用して、仮想環境のセットアップ行い、
jsonschemaのインストールを行なっています。
venvはPython 3.3から標準ライブラリとして取り込まれた仮想環境管理ツールであるため、追加のインストールは不要になります。

mkdir csv_validation_project
cd csv_validation_project
python -m venv venv
source venv/bin/activate
pip install jsonschema

4. Json Schemaについて

Json Schemaの基本構造

例えば、以下のようなJSON Schemaがあった場合、

{
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer", "minimum": 0},
        "email": {"type": "string", "format": "email"}
    },
    "required": ["name", "age"]
}


このSchemaは、

  • nameが文字列であること
  • ageが0以上の整数であること
  • emailがメールアドレスの形式であること
  • を要求しています。
    requiredは、必須のプロパティを指定するリストです。nameとageが必須であることを示しています。

    5. CSVデータの準備

    バリデーションを行う対象となる以下のようなcsvファイルを用意します。

    name,age,email
    John Doe,25,john.doe@example.com
    Jane Smith,30,jane.smith@example.com
    

    6. CSVバリデーション用のJson Schemaを作成

    csvカラムとJSON Schemaの対応付け

    csvの各列と対応するJSONSchemaを作成します。
    この時、csvの項目名と一致しているかどうかに気をつけてください。

    {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "age": {"type": "integer", "minimum": 0},
            "email": {"type": "string", "format": "email"}
        },
        "required": ["name", "age", "email"]
    }
    
    データ型と制約の定義

    上記の例では、年齢が負の値にならないようにするため、minimum属性を使っています。
    また、メールアドレスが正しい形式であることを保証するためにformat属性を使用しています。

    7. Json Schemaを用いたcsvファイルのバリデーション

    下記に今回csvバリデーションを行うための全体のコードを記載します。
    main.py

    import csv
    from jsonschema import ValidationError, Draft7Validator, FormatChecker
    
    
    # データを適切な方に変換する関数
    def convert_type(value):
        try:
            # 整数に変換できるか試す
            return int(value)
        except ValueError:
            try:
                # 浮動小数点数に変換できるか試す
                return float(value)
            except ValueError:
                # どちらでも変換できない場合はそのまま返す
                return value
    
    
    # CSVを読み込む関数
    def read_csv(file_path):
        with open(file_path, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            data = []
            for row in reader:
                # 各列のデータ型を変換
                converted_row = {key: convert_type(value) for key, value in row.items()}
                data.append(converted_row)
        return data
    
    
    # JSON Schemaの定義
    schema = {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "age": {"type": "integer", "minimum": 0},
            "email": {"type": "string", "format": "email"}
        },
        "required": ["name", "age", "email"]
    }
    
    
    # バリデーション関数
    def validate_csv_data(data, schema):
        for i, row in enumerate(data):
            try:
                validator = Draft7Validator(schema, format_checker=FormatChecker())
                validator.validate(row)
                print(f"Row {i}: OK")
            except ValidationError as e:
                print(f"Error at row {i}: {e.message}")
    
    # 実行部分
    if __name__ == '__main__':
        data = read_csv('sample.csv')
        validate_csv_data(data, schema)
    
  • csv.DictReaderを使用すると、数値として入力されたageも文字列型として読み込まれます。
  • 上記の例では、読み込んだ後にageフィールドを整数型(または必要に応じて浮動小数点型)に変換する処理を記載しています。
  • 実行方法

    main.pyとsample.csvを同じ階層に設置し、下記のコマンドを実行することでバリデーション結果を確認することができます。

    $ python main.py
    Row 0: OK
    Row 1: OK
    (.venv) 
    

    バリデーション成功時の動作を確認できたため、次に失敗時の動作について確認します。
    csvのデータにバリデーションエラーが発生するデータを挿入して実行してみます。
    以下のようにcsvに追記します。

    name,age,email
    John Doe,25,john.doe@example.com
    Jane Smith,30,jane.smith@example.com
    12345,40,invalid@example.com
    Alex Johnson,-5,alex.johnson@example.com
    Emily Davis,28,a
    

    そして、再度実行します。
    すると、下記のようにエラーになったデータについて、
    何がエラーかについて表示されます。

    $ python main.py
    Row 0: OK
    Row 1: OK
    Error at row 2: 12345 is not of type 'string'
    Error at row 3: -5 is less than the minimum of 0
    Error at row 4: 'a' is not a 'email'
    (venv)
    

    これで基本的なバリデーションの実装は完了になります。

    8. 応用

    より複雑な制約を持ったスキーマを導入することもできます。

    # JSON Schemaの定義
    schema = {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "age": {"type": "integer", "minimum": 0},
            "email": {"type": "string", "format": "email"},
            "phone": {"type": "string", "pattern": "^[0-9]{2,4}-[0-9]{2,4}-[0-9]{3,4}$"},
        },
        "required": ["name", "age", "email"],
        "if": {
            "properties": {
                "age": {"minimum": 15},
            },
        },
        "then": {
            "required": ["email", "phone"]
        },
    }
    

    このスキーマでは、18歳以上の場合に、emailとphoneが必須になる条件が追加されています。
    また、電話番号が次の形式に従う必要があります

  • 数字が2~4桁 + ハイフン(-) + 数字が2~4桁 + ハイフン(-) + 数字が3~4桁
  • 9.まとめ

    JsonSchemaを使用することで、データが予想通りの形式と内容を持つことを保証し、不正なデータの入力や保存を防ぐことが可能になります。
    また、JsonSchemaの制約には様々な種類があるので、今回の紹介で興味を持っていただいた方は、機会があればぜひ使ってみてください。