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

More than 1 year has passed since last update.

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

最初は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による「ケロちゃんチェック」で、幸せになりましょう!

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