読者です 読者をやめる 読者になる 読者になる

初老のボケ防止日記

おっさんのひとりごとだから気にしないようにな。

スポンサーリンク

PythonのData Validation Library「Cerberus」を使ってみた(続)



ケルベロス・トシキ&オメガトライブ

前回、Pythonのバリデーションライブラリの「Cerberus」というを使ってみたのです。

osa030.hatenablog.com

で、1000%便利というのがあったのでそれをメモ。

文字列の検証

とある文字列を検証するパターン。

import cerberus


def create_validator_1():
    schema = {
        'ipaddr': {
            'type': 'string',
            'required': True,
            'empty': False
        }
    }

    return cerberus.Validator(schema)

documents1 = (
    {'ipaddr': ''},              # blank
    {'ipaddr': 'hogehoge'},      # not ip
    {'ipaddr': '192.168.1.1'},   # ipv4
    {'ipaddr': '192.168.1.aa'},  # not ipv4
)

def do_validate(v, target):
    if v.validate(target):
        print('[{}]OK'.format(target))
        print('[{}]done.'.format(v.document))
    else:
        print('[{}]NG:{}'.format(target, v.errors))

if __name__ == '__main__':

    v = create_validator_1()
    documents = documents1

    for document in documents:
        do_validate(v, document)

これを実行すると結果はこう。

[{'ipaddr': ''}]NG:{'ipaddr': 'empty values not allowed'}
[{'ipaddr': 'hogehoge'}]OK
[{'ipaddr': 'hogehoge'}]done.
[{'ipaddr': '192.168.1.1'}]OK
[{'ipaddr': '192.168.1.1'}]done.
[{'ipaddr': '192.168.1.aa'}]OK
[{'ipaddr': '192.168.1.aa'}]done.

空文字は拒否されるけど、書式までは検証されない。

文字列が特定書式かの検証

'regex'を使えば正規表現で特定書式かどうかの検証もできる。

def create_validator_2():
    schema = {
        'ipaddr': {
            'type': 'string',
            'required': True,
            'regex': '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
        }
    }

    return cerberus.Validator(schema)

if __name__ == '__main__':

    v = create_validator_2()
    documents = documents1

    for document in documents:
        do_validate(v, document)

これを実行すると結果はこう。

[{'ipaddr': ''}]NG:{'ipaddr': "value does not match regex '^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$'"}
[{'ipaddr': 'hogehoge'}]NG:{'ipaddr': "value does not match regex '^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$'"}
[{'ipaddr': '192.168.1.1'}]OK
[{'ipaddr': '192.168.1.1'}]done.
[{'ipaddr': '192.168.1.aa'}]NG:{'ipaddr': "value does not match regex '^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$'"}

じゃあ、次にこんなデータを検証したらどうなるか。

documents2 = (
    {'ipaddr': ''},              # blank
    {'ipaddr': 'hogehoge'},      # not ip
    {'ipaddr': '192.168.1.1'},   # ipv4
    {'ipaddr': '192.168.1.aa'},  # not ipv4
    {'ipaddr': '192.168.1.999'}, # not ipv4
)

if __name__ == '__main__':

    v = create_validator_2()
    documents = documents2

    for document in documents:
        do_validate(v, document)

これを実行すると結果はこう。

[{'ipaddr': ''}]NG:{'ipaddr': "value does not match regex '^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$'"}
[{'ipaddr': 'hogehoge'}]NG:{'ipaddr': "value does not match regex '^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$'"}
[{'ipaddr': '192.168.1.1'}]OK
[{'ipaddr': '192.168.1.1'}]done.
[{'ipaddr': '192.168.1.aa'}]NG:{'ipaddr': "value does not match regex '^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$'"}
[{'ipaddr': '192.168.1.999'}]OK
[{'ipaddr': '192.168.1.999'}]done.

まあ当然ながら、正規表現でIPアドレスの範囲まで検証してないので通ってしまう。正規表現を頑張れば防ぐことはできるだろうけど、面倒なので独自型として処理しちゃおう。

独自の型の検証

標準で定義されているデータ・タイプは以下

  • string
  • integer
  • float
  • number (integer or float)
  • boolean
  • datetime
  • dict (formally collections.mapping)
  • list (formally collections.sequence, excluding strings)
  • set

これ以外のデータ・タイプを検証することもできる。

Custom Data Types

具体的にはValidatorを継承して任意の名称のメソッドを追加するだけ。今回は「ipaddr」というデータ・タイプを検証するメソッドを書いて、内部でPython3系で追加されたipaddressモジュールを使って検証させている。

import ipaddress


class MyValidator(cerberus.Validator):

    # 'ipaddr'というデータ・タイプの検証持に呼ばれる
    def _validate_type_ipaddr(self, field, value):
        try:
      # ファクトリ関数に検証をお任せ
            ipaddress.ip_address(value)
        except Exception as e:
            self._error(field, str(e))


def create_validator_3():
    schema = {
        'ipaddr': {
            'type': 'ipaddr', #データ・タイプを'ipaddr'に
            'required': True,
        }
    }
   
  # 自前のバリデータを使う
    return MyValidator(schema)

if __name__ == '__main__':

    v = create_validator_3()
    documents = documents2

    for document in documents:
        do_validate(v, document)

これを実行すると結果はこう。

[{'ipaddr': ''}]NG:{'ipaddr': "'' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': 'hogehoge'}]NG:{'ipaddr': "'hogehoge' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': '192.168.1.1'}]OK
[{'ipaddr': '192.168.1.1'}]done.
[{'ipaddr': '192.168.1.aa'}]NG:{'ipaddr': "'192.168.1.aa' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': '192.168.1.999'}]NG:{'ipaddr': "'192.168.1.999' does not appear to be an IPv4 or IPv6 address"}


もちろん、IPv6の色々な表記も網羅できちゃう。

インターネット10分講座:RFC5952 -IPv6アドレスの推奨表記 - JPNIC

documents3 = (
    {'ipaddr': ''},                      # blank
    {'ipaddr': 'hogehoge'},              # not ip
    {'ipaddr': '192.168.1.1'},           # ipv4
    {'ipaddr': '192.168.1.aa'},          # not ipv4
    {'ipaddr': '192.168.1.999'},         # not ipv4
    {'ipaddr': '2001:db8:0:0:1:0:0:1'},  # ipv6
    {'ipaddr': '2001:0db8:0:0:1:0:0:1'}, # ipv6
    {'ipaddr': '2001:db8::1:0:0:1'},     # ipv6
    {'ipaddr': '2001:db8::0:1:0:0:1'},   # ipv6
    {'ipaddr': '2001:0db8::1:0:0:1'},    # ipv6
    {'ipaddr': '2001:db8:0:0:1::1'},     # ipv6
    {'ipaddr': '2001:db8:0000:0:1::1'},  # ipv6
    {'ipaddr': '2001:DB8:0:0:1::1'},     # ipv6
)

if __name__ == '__main__':

    v = create_validator_3()
    documents = documents3

    for document in documents:
        do_validate(v, document)

これを実行すると結果はこう。

[{'ipaddr': ''}]NG:{'ipaddr': "'' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': 'hogehoge'}]NG:{'ipaddr': "'hogehoge' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': '192.168.1.1'}]OK
[{'ipaddr': '192.168.1.1'}]done.
[{'ipaddr': '192.168.1.aa'}]NG:{'ipaddr': "'192.168.1.aa' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': '192.168.1.999'}]NG:{'ipaddr': "'192.168.1.999' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': '2001:db8:0:0:1:0:0:1'}]OK
[{'ipaddr': '2001:db8:0:0:1:0:0:1'}]done.
[{'ipaddr': '2001:0db8:0:0:1:0:0:1'}]OK
[{'ipaddr': '2001:0db8:0:0:1:0:0:1'}]done.
[{'ipaddr': '2001:db8::1:0:0:1'}]OK
[{'ipaddr': '2001:db8::1:0:0:1'}]done.
[{'ipaddr': '2001:db8::0:1:0:0:1'}]OK
[{'ipaddr': '2001:db8::0:1:0:0:1'}]done.
[{'ipaddr': '2001:0db8::1:0:0:1'}]OK
[{'ipaddr': '2001:0db8::1:0:0:1'}]done.
[{'ipaddr': '2001:db8:0:0:1::1'}]OK
[{'ipaddr': '2001:db8:0:0:1::1'}]done.
[{'ipaddr': '2001:db8:0000:0:1::1'}]OK
[{'ipaddr': '2001:db8:0000:0:1::1'}]done.
[{'ipaddr': '2001:DB8:0:0:1::1'}]OK
[{'ipaddr': '2001:DB8:0:0:1::1'}]done.

独自の型の検証して値の変換

今回の様に、せっていドキュメントでは文字列が指定されているけど、実際はその文字列を引数にクラスのインスタンスを生成して処理に使うパターンとか有ると思う。公式には記載されていないのだけれども、ソースを読んだらできそうだったので試してみた。

class MyValidator2(cerberus.Validator):
    def _validate_type_ipaddr(self, field, value):
        try:
             # 通常は、引数のvalueがそのまま設定されるが、
             # 生成したIPvXAddressのインスタンスで更新する
             self.document[field] = ipaddress.ip_address(value)
        except Exception as e:
            self._error(field, str(e))

def create_validator_4():
    schema = {
        'ipaddr': {
            'type': 'ipaddr',
            'required': True,
        }
    }

    return MyValidator2(schema)

if __name__ == '__main__':

    v = create_validator_4()
    documents = documents3

    for document in documents:
        do_validate(v, document)

Validatorオブジェクト内部に変更後ののドキュメントを保持しているのでそこに対して変更後の値をセットすればよい。これは'coerce'指定時のコードを参考にしている。

これを実行すると結果はこう。

[{'ipaddr': ''}]NG:{'ipaddr': "'' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': 'hogehoge'}]NG:{'ipaddr': "'hogehoge' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': '192.168.1.1'}]OK
[{'ipaddr': IPv4Address('192.168.1.1')}]done.
[{'ipaddr': '192.168.1.aa'}]NG:{'ipaddr': "'192.168.1.aa' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': '192.168.1.999'}]NG:{'ipaddr': "'192.168.1.999' does not appear to be an IPv4 or IPv6 address"}
[{'ipaddr': '2001:db8:0:0:1:0:0:1'}]OK
[{'ipaddr': IPv6Address('2001:db8::1:0:0:1')}]done.
[{'ipaddr': '2001:0db8:0:0:1:0:0:1'}]OK
[{'ipaddr': IPv6Address('2001:db8::1:0:0:1')}]done.
[{'ipaddr': '2001:db8::1:0:0:1'}]OK
[{'ipaddr': IPv6Address('2001:db8::1:0:0:1')}]done.
[{'ipaddr': '2001:db8::0:1:0:0:1'}]OK
[{'ipaddr': IPv6Address('2001:db8::1:0:0:1')}]done.
[{'ipaddr': '2001:0db8::1:0:0:1'}]OK
[{'ipaddr': IPv6Address('2001:db8::1:0:0:1')}]done.
[{'ipaddr': '2001:db8:0:0:1::1'}]OK
[{'ipaddr': IPv6Address('2001:db8::1:0:0:1')}]done.
[{'ipaddr': '2001:db8:0000:0:1::1'}]OK
[{'ipaddr': IPv6Address('2001:db8::1:0:0:1')}]done.
[{'ipaddr': '2001:DB8:0:0:1::1'}]OK
[{'ipaddr': IPv6Address('2001:db8::1:0:0:1')}]done.

ポイントとしては、validate()の引数のドキュメントは変更されずに、Validator内部に保持しているdocumentに変更後のドキュメントが保存されているということ。

ということで、うまく使えばスマートに処理が書けそうですよ、奥さん。

入門 Python 3

入門 Python 3

実践 Python 3

実践 Python 3

Effective Python ―Pythonプログラムを改良する59項目

Effective Python ―Pythonプログラムを改良する59項目

1986オメガトライブ/カルロス・トシキ&オメガトライブ スーパーベスト・コレクション

1986オメガトライブ/カルロス・トシキ&オメガトライブ スーパーベスト・コレクション

  • アーティスト: カルロス・トシキ&1986オメガトライブ
  • 出版社/メーカー: ワーナーミュージック・ジャパン
  • メディア: CD
  • クリック: 1回
  • この商品を含むブログを見る

スポンサーリンク