Pythonの汎用データバリデーションライブラリ「Cerberus」を使う

  • 26
    いいね
  • 0
    コメント

バリデーションチェック、めんどくさいですよね。

最初はFormEncodeでやろうとしたのですが、「もうちょっと今っぽいやつないのかな……」と探したところ、とてもいい感じのライブラリがあったので、使い方をメモしておきます。ご参考になれば幸いです。

Cerberusとは

英語読みで「サーベラス」でいいんでしょうか? いわゆる「ケルベロス」のことで、「冥界の番犬」のようにデータの入り口を守る、というのが名前の由来のようです。厨ニっぽい かっこいいですね。

導入

$ pip install cerberus

使い方

サンプル見て頂くのが早いです。こんな感じに今っぽく書けます。
(Python 3.5.2 で動作確認しています)

sample_cerberus.py
# -*- coding: utf-8 -*-

from cerberus import Validator

import re
from datetime import datetime, date
from pprint import pprint

# バリデーション定義
schema = {
    'name': {
        'type': 'string',
        'required': True,
        'empty': False,
    },
    'email': {
        'type': 'string',
        'required': True,
        'regex': '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$',
    },
    'age': {
        'type': 'integer',
        'min': 0,
    },
    'phones': {
        'type': 'list',
        'schema': {
            'type': 'string',
            'regex': '^[0-9]{2,4}-[0-9]{2,4}-[0-9]{3,4}$',
        }
    },
    'address': {
        'type': 'string',
        'empty': True,
    },
    'birthday': {
        'type': 'date',
    }
}

# バリデータを作成
v = Validator(schema)

# 入力値1(バリデーションOK)
data_ok = {
    'name': '田中一郎',
    'email': 'tanaka@test.co.jp',
    'age': 30,
    'phones': [
        '012-3456-7890',
        '0120-444-444',
    ],
    'address': '',
    'birthday': date(1990, 7, 7),
}

# 入力値2(バリデーションNG)
data_ng = {
    'name': '',
#    'email': 'tanaka@test.co.jp',
    'age': -300,
    'phones': [
        '01234567890',
        'skype',
    ],
    'address': None,
    'birthday': '1990-07-07',
    'sex': 'male',
}

# バリデーション実施、結果表示
print("-----------------------------------------")
print("◯OK")
print("-----------------------------------------")
pprint(v.validate(data_ok))
pprint(v.errors)

print("-----------------------------------------")
print("×NG")
print("-----------------------------------------")
pprint(v.validate(data_ng))
pprint(v.errors)

実行結果

-----------------------------------------
OK
-----------------------------------------
True
{}
-----------------------------------------
×NG
-----------------------------------------
False
{'age': ['min value is 0'],
 'birthday': ['must be of date type'],
 'email': ['required field'],
 'name': ['empty values not allowed'],
 'phones': [{0: ['value does not match regex '
                 "'^[0-9]{2,4}-[0-9]{2,4}-[0-9]{3,4}$'"],
             1: ['value does not match regex '
                 "'^[0-9]{2,4}-[0-9]{2,4}-[0-9]{3,4}$'"]}],
 'sex': ['unknown field']}

サンプルではよく使いそうなバリデーションルールを中心に取り上げましたが、他にも色々あるので、詳しくは以下をご覧ください。

Validation Rules — Cerberus is a lightweight and extensible data validation library for Python

バリデーションエラーメッセージの日本語化

このままでも使えますが、おおかた「メッセージは日本語にできないの?」って話になりますよね。たぶん。

残念ながら2016/11時点の最新バージョン(v1.0.1)では、リソースファイルなどを使った多言語対応はされていません。ですが、BasicErrorHandlerのメッセージ定義を上書きすることで、バリデーションエラーメッセージを日本語化することが可能です。

# -*- coding: utf-8 -*-

from pprint import pprint

from cerberus import Validator
from cerberus.errors import BasicErrorHandler

class CustomErrorHandler(BasicErrorHandler):
    """ BasicErrorHandler.message を上書きして日本語化 """
    def __init__(self, tree=None):
        super(CustomErrorHandler, self).__init__(tree)
        # 文言を適宜日本語化してください
        self.messages = {
            0x00: "{0}",
            0x01: "document is missing",
            0x02: "required field",
            0x03: "知らない項目 '{field}' です",
#            ... (略) ...
            0x93: "no definitions validate",
            0x94: "one or more definitions don't validate"
        }


schema = {
    'mail_address' : {'type' : 'string'},
}
data = {
    'name': '田中',     # 0x03: バリデーションエラー
    'mail_address' : 'tanaka_at_test.com',
}

# 拡張したBasicErrorHandlerを指定
v = Validator(schema, error_handler=CustomErrorHandler())

pprint(v.validate(data))
pprint(v.errors)

実行結果

False
{'name': ["知らない項目 'name' です"]}

こちらにIssueが上がっているので、そのうち、こんなことする必要はなくなるかもしれません。なるといいなあ……

バリデーションルールのカスタマイズ

上記サンプルを見ると、「メールアドレスをregexでチェックしているところ、カスタムバリデーションにできない?」って話になりますよね。たぶん。

ここにドキュメントがあるのですが、どういうときにどれを使うべきなのかがよくわからなかったので、簡単にまとめておきます。

1. カスタムデータ型を追加する (Custom input_data Types)

'type'として独自のデータ型を指定する場合です。

schema = {
    'mail_address' : {
        'type' : 'email',
    },
}

{'type': 'integer'} とか {'type': 'string'} とか、バリデーションに使えるデータタイプはいくつかありますが、ここに新しく{'type' : 'email'} を追加する、というイメージです。

Validatorを拡張したクラス(CustomValidator)を作成、そこにデータタイプを追加します。

# -*- coding: utf-8 -*-

import re
from pprint import pprint

from cerberus import Validator

REGEX_EMAIL = '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'

class CustomValidator(Validator):
    # [Custom input_data Types]
    # http://docs.python-cerberus.org/en/stable/customize.html#custom-input_data-types
    def _validate_type_email(self, value):
        if re.match(REGEX_EMAIL, value):
            return True

schema = {
    'mail_address' : {
        'type' : 'email',
    },
}
data = {
    'mail_address' : 'tanaka_at_test.com',
}

v = CustomValidator(schema)
pprint(v.validate(data))
pprint(v.errors)

実行結果

False
{'mail_address': ['must be of email type']}

エラーメッセージ 0x24: "must be of {constraint} type", の文言を個別に変更することはできませんが、これで用が足りる場合も多いでしょう。

2. バリデーションスキーマにバリデーション用メソッドを指定する (Custom Validator)

スキーマ定義にバリデーション用のメソッド名を指定し、メソッドの実行結果を受け取るイメージです。

schema = {
    'mail_address' : {
        'validator': check_mail,   # ←check_mail() メソッド
    },
}

これはソースコード見ていただいた方が早いですね。

# -*- coding: utf-8 -*-

import re
from pprint import pprint

from cerberus import Validator

REGEX_EMAIL = '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'

# [Custom Validator]
# @see http://docs.python-cerberus.org/en/stable/validation-rules.html#validator]
def check_mail(field, value, error):
    if not re.match(REGEX_EMAIL, value):
        msg = "指定された'%s'は、メールアドレスとして不正です。" % value
        error(field, msg)

schema = {
    'mail_address' : {
        'validator': check_mail,   # ←check_mail() メソッド
    },
}
data = {
    'mail_address' : 'tanaka_at_test.com',
}

v = Validator(schema)
pprint(v.validate(data))
pprint(v.errors)

実行結果

False
{'mail_address': ["指定された'tanaka_at_test.com'は、メールアドレスとして不正です。"]}

毎回これでやるのは大変そうですが、Validatorを拡張しなくていいのは非常に楽です。エラーメッセージも個別に指定できますし。

3. バリデーションルールを追加する (Custom Rules)

スキーマに独自のkey, valueを指定したい場合に使います。

schema = {
    'mail_address' : {
        'exists_keyword' : 'SUZUKI',
    },
}

上記2つと大きく違うのは、「スキーマで指定された値を取得できる」部分です。ここでは 「スキーマで指定されたキーワード(SUZUKI)が含まれていない場合、バリデーションを失敗させる」ようにします。

# -*- coding: utf-8 -*-

import re
from pprint import pprint

from cerberus import Validator

class CustomValidator(Validator):
    # [Custom Rules]
    # http://docs.python-cerberus.org/en/stable/customize.html#custom-rules
    def _validate_exists_keyword(self, exists_keyword, field, value):
        """
        ※以下のdocstringを書かないとwaringが出る

        The rule's arguments are validated against this schema: {'type': 'string'}
        """
        if value.find(exists_keyword) < 0:
            self._error(field, "与えられた文字列'%s'の中に、キーワード文字列'%s'が存在しませんでした。" % (value, exists_keyword))


schema = {
    'mail_address' : {
        'exists_keyword' : 'SUZUKI',
    },
}
data = {
    'mail_address' : 'tanaka_at_test.com',
}

v = CustomValidator(schema)
pprint(v.validate(data))
pprint(v.errors)

実行結果

False
{'mail_address': ["与えられた文字列'tanaka_at_test.com'の中に、キーワード文字列'SUZUKI'が存在しませんでした。"]}

docstringを書かないといけない部分がわかりにくいです。今回は {'exists_keyword' : 'SUZUKI'} とスキーマに指定したいので、'SUZUKI'に対応したデータ型 {'type': 'string'} をdocstringに記述しています。ちなみにThe rule's arguments are...の文字列は固定です。

記述しない場合、以下のようなwarningが発生します。
UserWarning: No validation schema is defined for the arguments of rule 'exists_keyword'

こちらは複雑な割に、どういう場合に使えばいいのかよくわからなかったのですが、「blacklistをスキーマに渡し、その中にマッチするものがあれば、バリデーションエラーにする」みたいなケースで使えるかもしれません。

なお、スキーマはdict形式でないといけないので、以下のようなカスタムルールを作ってもエラーになります。ご注意ください。

# ×NG
schema = {
    'mail_address' : {
        'has_bad_word',           # dict形式になっていない
    },
}

# ◯OK
schema = {
    'mail_address' : {
        'has_bad_word' : True,    # docstringで{'type': 'boolean'} 指定
    },
}

最後に

めんどくさいバリデーションですが、Cerberusによる「ケロちゃんチェック」で、幸せになりましょう!

あの決定的な敗戦から数十年――