说一下Python项目中的验参

在积累了一定的工作与项目实战经验后,越来越意识到验参的重要性。
前几天又重读总结了一下【程序员修炼之道】,书中提到,卓有成效的程序员从不相信任何人,包括自己。
关于这句话的一个很重要的实践即是在自己编写的程序中,做好验参工作,使对字段的限制与文档一致。这点可以显著得增强系统的稳定性,保护系统的健壮与数据的一致。

在实际工作中,发现验参环节并不是那么容易做好,微服务系统有多层结构,每个服务内代码的组织又是分层的, 在哪些场景与环节进行验参比较实用是自己需要考虑清楚的点。
这篇总结一下工作中见到的各Python项目对于验参的各种处理,以及常用的验参的库。


需要验参的几个场景

假设项目为一个简单的两层结构,gateway接受浏览器http请求,通过thrift rpc协议调下层service,service将数据写到DB中。

这样一个简单的调用链可以抽出几处需要验参的地方:

  1. 收到http请求时,对前端传来的参数进行验证,确保前端传来的参数与前端文档中约定的一致;
  2. service服务代码分层为handler, service, model,handler接受到thrift请求,将thrift request对象序列化为dict后,调用service层代码前,需要进行参数验证,确保client端传来的参数与idl中定义的参数规则一致;
  3. service层往DB写数据前(包括新增与更新时)需要验参,确保写到数据库中的数据与model文件(orm定义)中定义的规则一致。

验参如何做

if else直接干

之前在写一个Java项目的时候,问到组内的一个稍有经验的Java开发在Java中验参通常怎么做,怎样是比较地道的写法。
他开开玩笑说,if else不就好了嘛。(当然,之后还是告诉了我可以用javax.validation中的注解很自然轻易得完成这个任务)

诚然,if else可以撸出一切。而且的确我也在一些项目中包括刚参加工作时的小公司的代码里见到过这样的做法,不用引入第三方库,直接进行判断,某字段是否存在,某字段是否为None,字段长度是否超长等。
这的确可以完成工作,但真的不够clean,每次在函数的前部分都要处理这些东西,代码写出来很丑。若要把这种if抽出来,粒度又太细,同时又要使用比如装饰器这种技术将它和函数本体编织起来,而且不同函数要验证的条件往往又是不一样的,之前写的验证方法可能还要再改以达到通用,这就又要再改引用了这个验证方法的方法,等等情况会出现很多问题,所以并不是一个长久的组织方法。

下面介绍几个我见过的用于验参的第三方包,可接避免上面那样的重复造轮子。

marshmallow

marshmallow本身是Python中的一个出色的用于做序列化的库,同时也提供了在验参功能。
它允许开发者定义一些schema,schema中可以以各种方式(allow_none, required, lambda)来表述一个字段的规则,同时在反序列化(将外部参数转化为领域对象)时,会自动进行验参工作,并将不符合规则的参数统一组织起来,甚至允许开发者自己提供个性化的相应报错信息。

上面说到反序列化,那这位又问了,序列化时它不做验参吗?的确如此,marshmallow的作者认为,序列化是指将自己系统内的数据给提供出去(相当于to_json()),对于它们的质量与来源是我们可以保证的,故不需要在这个时候进行反序列化。
但实际情况也并不往往如此,比如我最近接触到的项目,由于之前没有做好验参工作,且表结构较复杂,会有一些存在库中的历史数据其实是少字段的或者是不符合规则的,在这种情况下,可以先将数据温和地取出来,进行序列化生成dict对象,再用dict对象来调用schema.validate(dict)来专门进行验证,从而搜集信息,修补数据。

它的schema可以按照如下方式定义:

from marshmallow import Schema, fields, validate, validates

class UserSchema(Schema):
    name = fields.Str(required=True, validate=lambda n: n)
    age = fields.Decimal(required=True, validate=lambda n: n > 18)
    location = fields.Str(required=True)

    @validates('location')
    def validate_location(self, value):
        valid_locations = [u'SHANGHAI', u'TOKYO', u'NEWYORK']
        if value not in valid_locations:
            raise ValidationError('Unknown location.')
使用marshmallow进行验参

此外,marshmallow还提供了一些常见规则的验证,比如Email,URL来验证字段是否符合规则,不用再去硬写一些让人头疼的正则。它们都继承于Validator类,你也可以继承它来编写自己的验证规则类来扩展marshmallow的能力,使得一切都很地道、好用。

webargs

在gateway层面,面对http请求,可以使用webargs包来进行参数提取与校验。
我们知道,http请求传参有多种可能,比如在url中的?key=value&key2=value2这种格式,此外,post方法传参的可能就更多了,有plan text,application/json等。

而webargs包便是用来简化这一拿参验参的过程,它让开发者可以通过定义一个schema或是dict的结构来表示自己期望从http中得到哪些参数,以及使用哪些规则。查一下源码的话,很容易发现,它内部也是使用了marshmallow,调用了marshmallow的load函数,最后返回一个dict。
这种方式使用起来对开发还是很友好的,举例如下(抄自项目readme):

from flask import Flask
from webargs import fields
from webargs.flaskparser import use_args

app = Flask(__name__)

hello_args = {"name": fields.Str(required=True)}
# or use schema
# HelloArgs(Schema):
#     name = fields.Str(required=True)


@app.route("/")
@use_args(hello_args)
# 对应上面
# @use_args(HelloArgs(strict=True))
def index(args):
    return "Hello " + args["name"]


if __name__ == "__main__":
    app.run()

# curl http://localhost:5000/\?name\='World'
# Hello World

schema

说了上面,哪位又问了,marshmallow与webargs所做的验参都不够专一,前者是为序列化服务,后者更多地是为取参同时进行,有些情况下只有两个参数,不想去定义那些schema,感觉好麻烦,而且只想用单一的验参功能,要怎么做呢?

那么schema就是你想要的。Schema validation just got Pythonic
它的用法与api比较pythonic,很语义化而且够函数式,写起来还是比较好玩的,不过要注意正确性。
在工作中我见过一些同事拿它在service中的handler代码层验参。
抄袭项目readme示例代码如下,诸位可以感受下,还是蛮有意思的:

schema示例代码


一定不要小看验参这件事,在大型项目的开发中,可能有很多历史遗留问题,兼容性问题,甚至是来自脚本的恶意请求等。你根本无法确定你编写的代码会被怎样调用,如果这些环节失去了这些保障,线上的复杂情况,会让你的代码在一些匪夷所思的地方报出经典的NoneType error,甚至有些数据库的字段会被莫名其妙地被写为空,却根本不知道它是在何处发生的。这些错误的发生会让开发人员不知所措,措手不及,因为明明在测试时根本没出现过,极度难以调试。
等到时候再要加校验,已经很困难了。

在经历了一些莫明其妙的问题后,我不得不开始重视验参环节,毕竟没吃过亏还是不知道疼。
我认为验参环节是一种运行时的assert技术,marshmallow与webargs提供的序列化、取参数的同时进行验参我认为是比较好的方案,它不会让开发者在代码中多写一行专门去调验参函数又能把这件事给做得很棒。

做好参数验证,是一次请求,一个函数运行,一次持久化成功的第一道保障,client环境太复杂,我们这些写service的还是要保护好自己啊!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • ​ 小朋友们,你们是不是像丁丁熊一样挑食呢,但是丁丁熊后来改掉了这个毛病,你知道发生了什么吗? 丁丁熊总是不好好吃...
    善良的狼妈妈阅读 270评论 0 0
  • 卷首语: 一个人马不停蹄可以走的很快,有了伙伴们的信任、鼓励和支持,可以走的更远。 101、宝生堂李明贤大黄蜂 对...
    袁春楠阅读 546评论 0 0
  • 作者 周定先 1979年的夏天,有一个真人真事的故事,发生在今遵义市播州区石板镇池坪村张家寨。 小...
    寒冬花阅读 576评论 1 3