关于服务器数据验证,一种比较优雅简便的Python解决方法


这两天整服务器代码遇到如题所说问题,不得已上了chinaunix发了个帖子,寻找一种比较好的解决方式,最后找到了一种比较好的方法,中间学到了挺多东西,现记录在此。

原帖地址:http://bbs.chinaunix.net/thread-4111060-1-1.html


感谢@Hadron74 跨生物学界和编程界的博士后,业余时间也翻译了一本python书《Python for Bioinformatics》,搞生物学的pythoner可以去读一下~~

他的博客地址是:http://blog.sciencenet.cn/home.php?mod=space&uid=565112


原抛出来的问题是这样的:

1楼:Sasoritattoo 

要求是这样的,有一个项目是手机应用客户端与服务器通信,但是每次网络来的请求都会有很多的字符串参数需要转换,而且需要验证其基本信息是否符合基本格式要求,如果不符合会返回错误码给客户端,符合了就返回正常的数据。

拿一个具体的例子来说吧:
第一代原型函数是这样的:

  1. @app.route('/test', methods = ['GET', 'POST'])
  2. def test_route():
  3.     '''
  4.     这是一条路由,假如是IP+"/test/"就可以访问到此函数。然后其form中带了三个参数:lat,lon,id。
  5.     '''
  6.     lat = request.form.get('LAT')
  7.     lon = request.form.get('LON')
  8.     id = request.form.get('OBJECT_ID')

  9.      #接下来每个参数都要处理一遍,非常蛋疼的行为!
  10.     try:
  11.         lat = float(lat)
  12.     except:
  13.         error = ERROR_LAT
  14.         return error

  15.     try:
  16.         lon = float(lon)
  17.     except:
  18.         error = ERROR_LON
  19.         return error
  20.     
  21.     try:
  22.         id = ObjectId(id)
  23.     except:
  24.         error = ERROR_ID
  25.         return error

  26.     do_something()
复制代码
这样的话如果参数一多的话,首先好几十行处理类型转换的代码,而且如果路由多了,那么这些冗余代码量是很可观的,因此这个函数演化诞生了第二代原型,如下:
  1. @app.route('/test', methods = ['GET', 'POST'])
  2. def test_route():
  3.     lat = request.form.get('LAT')
  4.     lon = request.form.get('LON')
  5.     id = request.form.get('OBJECT_ID')

  6.     #这种写法稍微好些,等于用check_format先把风险吃掉了,但是无法更新这里的lat,lon,id等值,所以还要下面再来一次
  7.     is_ok, error = check_format(lat=lat, lon=lon, id=id)
  8.     if not is_ok:
  9.         return error

  10.     lat = float(lat)
  11.     lon = float(lon)
  12.     id = ObjectId(id)

  13.     do_something()


  14. @classmethod
  15. def check_format(lat=None, lon=None, id=None):
  16.     ,,,
  17.     本方法因为是与处理一个数据类中的值相关,所以与上面的方法不在同一个py文件中。
  18.     如果我的项目是mvc架构的话,那么它是应该放在model层的,上面的函数放在view层。
  19.     ,,,
  20.     ret = True
  21.     error = ''
  22.     if lat is not None:
  23.         try:
  24.             lat = float(lat)
  25.         except:
  26.             error = ERROR_LAT
  27.             return False, error
  28.     if lon is not None:
  29.         try:
  30.             lon = float(lon)
  31.         except:
  32.             error = ERROR_LON
  33.             return False, error
  34.     if id is not None:
  35.         try:
  36.             id = ObjectId(id)
  37.         except:
  38.             error = ERROR_ID
  39.             return False, error

  40.     return ret, error
复制代码
第二代原型函数就是这样,不过你也看到了,在check_format()之后,还要自己再转一次,对于一个写代码喜欢追求美感的码农来说,这显然有点儿破坏美感,如果能够同时一次就改变了原函数中的值就好了,无奈精力和python造诣有限一时想不出好方法,因此特来求助于各位pythoner高手,希望解决这个需求,小弟先谢过了
待续。。。。

PS:
今日光棍节,愿天下程序猿光棍早日“脱光”


2楼:icymirror

我也是刚学习Python不久,最近刚刚在看metaclass相关的东西,也许下面的方法是你需要的。
使用的时候,只需要把新的句柄的开关加入到__routes__中,然后,定义下对应的方法就好了。
(风险:如果有几个检测是非常相似的,最好在检测时候仔细处理,免得被先检测的方法误"诊"了)

class MetaRouter(object):
    def __init__(self):
        self.__routes__ = ('lat', 'lon', 'id')    # define the head of handler

    def test_route(self, content):
        handle = None
        for index in xrange(len(self.__routes__)):
            methodName = self.__routes__[index] + "_handler"
            handler = getattr(self, methodName)
            if handler(content) == True:
                print("Current is %s", self.__routes__[index])
                break

        # below is the dosomething():
        print "all is done.\n", "=" * 80

    def lat_handler(self, content):    # handler for lat
        try:
            float(content)
            return True
        except:
            return False

    def lon_handler(self, content):    # handler for lon
        try:
            int(content)
            return True
        except:
            return False

    def id_handler(self, content):    # handler for id
        try:
            return True
        except:
            return False

def main():
    route = MetaRouter()
    route.test_route("33")     # verify lan
    route.test_route("3.14")  # verify lon
    route.test_route("id=9")  # verify id

if __name__ == '__main__':
    main()


3楼:Hadron74

本帖最后由 Hadron74 于 2013-11-11 14:31 编辑

回复 1# Sasoritattoo 

n
    你可以充分利用字典的功能,用维护一个字典,记录所需的format的格式和错误代码。另外再用一个字典保存返回值。这个程序就简洁多了,没有冗余代码了。
具体程序如下:
  1. class ObjectId(str):
  2.     def __init__(self,obj):
  3.         str.__init__(self,obj)

  4. # This is the dictionary you need to support
  5. key_format = {"LAT":(float,1)\
  6.                 ,"LON":(float,2)\
  7.                 ,"ObjectId":(ObjectId,3)\
  8.                 }

  9. def test_route(requestForm,key_format):
  10.     values = {}
  11.     for key in key_format:
  12.         value = requestForm[key]
  13.         #value = request.form.get(key)  # You can use the line in your real function
  14.         try:
  15.             values[key] = key_format[key][0](value)
  16.         except:
  17.             raise ValueError("DATA FORMAT ERROR NO:%d"% (key_format[key][1],))

  18.     print values # to store the converted data
  19.     #do_something()
  20. rf={"LAT":"12.0","LON":"2.0","ObjectId":"This is an example"}

  21. test_route(rf,key_format)

  22. rf={"LAT":"12.0","LON":"ERROR2.0","ObjectId":"This is an example"}

  23. test_route(rf,key_format)
复制代码
结果:
  1. $python test_route.py
  2. {'LAT': 12.0, 'LON': 2.0, 'ObjectId': 'This is an example'}
  3. Traceback (most recent call last):
  4.   File "test_route.py", line 33, in
  5.     test_route(rf,key_format)
  6.   File "test_route.py", line 20, in test_route
  7.     raise ValueError("DATA FORMAT ERROR NO:%d"% (key_format[key][1],))
  8. ValueError: DATA FORMAT ERROR NO:2
复制代码


4楼:Sasoritattoo

回复 3# Hadron74 

    其实,不只是做类型检查这么简单,类型检查只是一部分,我是希望通过check_format()方法实现预处理一下参数,不符合就返回错误,符合的话下面就直接用了。

5楼:Sasoritattoo

回复 2# icymirror 


    谢谢,不太能满足我的需求。

6楼:Hadron74

回复 4# Sasoritattoo 

    我写的只是一个思路,你可以用自定义函数的方法,处理任何你需要的预处理参数问题,如果你看懂了我的程序,不难实现。
在程序中只需要维护一个字典就能完成所有的处理。当然你也可以用一个try块把这部分程序封装在check_form函数中,用values做返回值,这样就能截获错误信息。代码我就不写了。


7楼:Sasoritattoo 

回复 6# Hadron74 

    你的代码虽然懂了,但是实际操作起来仍然不好操作,尝试了几次都没找到比较好的实际操作方法,而且那些lat,lon,id在下面的do_something()阶段还要接着使用的
    我想了一下,大概实现思路是这样的:
    第一阶段:
    lat, is_ok, error = check_format(lat='LAT')
    if not_is_ok:
        return error

    lon, is_ok, error = check_format(lon='LON')
    if not_is_ok:
        return error

    id, is_ok, error = check_format(id='OBJECT_ID')
    if not_is_ok:
        return error

    lon = 123
    lat = 321
    
这是分开写的,然后需要进化到把其变成
    for item in items:
        ?, is_ok, error = check_format(?) 
        if not is_ok:
            return error   

    lon = 123
    lat = 321

关键是?这里怎么填写才能给lon,lat,id赋上值?而且下面还要用到lon和lat,不知您知道怎么实现吗?
这里关于check_format()应该不难,只要传入对应参数,做相应处理即可。


8楼:yjphhw

你可以通过表单来解决。看看表单,建立一个表单,然后,直接用表单的验证。

9楼:Hadron74

回复 7# Sasoritattoo 

如果你非要直接用变量名,就把它们定义到globals里吧,不过该global变量不是好的编程,有崩溃的危险。(这里改了一版贴出来)
  1. class ObjectId(str):
  2.     def __init__(self,obj):
  3.         str.__init__(self,obj)

  4. # This is the dictionary you need to support
  5. key_format = {"LAT":(float,1,"lat")\
  6.                 ,"LON":(float,2,"lon")\
  7.                 ,"ObjectId":(ObjectId,3,"object")\
  8.                 }

  9. def test_route(requestForm,key_format):

  10.     def check_format(requestForm,key_format):
  11.         values = {}
  12.         for key in key_format:
  13.             value = requestForm[key]
  14.             #value = request.form(key)  # You can use the line in your real function
  15.             try:
  16.                 values[key_format[key][2]] = key_format[key][0](value)
  17.             except:
  18.                 values = (key,key_format[key][1])
  19.        return values

  20.     values = check_format(requestForm,key_format)

  21.     if type(values) != type({}):
  22.         key, err_number = values
  23.         # deal something for the error
  24.         print key,err_number, "heihei"
  25.         return

  26.     globals().update(values)


  27.     #do_something()
  28.     print lat, "xixixixi"
  29.     print object, "hahaha"



  30. rf={"LAT":"12.0","LON":"2.0","ObjectId":"This is an example"}

  31. test_route(rf,key_format)

  32. test_route(rf,key_format)

  33. rf={"LAT":"12.0","LON":"ERROR2.0","ObjectId":"This is an example"}

  34. test_route(rf,key_format)

  35.                                                         
复制代码
结果
  1. $python test_route.py
  2. 12.0 xixixixi
  3. This is an example hahaha
  4. LON 2 heihei
复制代码
还有一种方法是用类的属性函数,这里就不写了,但是lat前需要加个classname,用classname.lat
参考http://stackoverflow.com/questio ... -variable-in-python 
不过我觉得最好的方法,也是最简单就是在后续程序中用values["lat"]来替代lat;
你说呢?
   

10楼:timespace

回复 7# Sasoritattoo 
我觉得只要功能逻辑划分清楚,代码保持适当的冗余可以简化问题。把test_route的一堆“if”转移到check_format又能改变什么?反而让代码更难懂。大概意思如下

  1. @app.route('/test', methods = ['GET', 'POST'])
  2. def test_route():
  3.     lat, error = parse_float(request.form.get('LAT'))
  4.     if lat is None:
  5.          return error

  6.     lon, error = parse_float(request.form.get('LON'))
  7.     if lon is None:
  8.         return error

  9.     object_id, error = parse_object_id(request.form.get('OBJECT_ID'))
  10.     if object_id is None:
  11.         return error

  12.     do_something()

  13. def parse_float(param):
  14.     pass

  15. def parse_object_id(param):
  16.     pass
复制代码

11楼:Sasoritattoo

回复 8# yjphhw 

    我项目使用的是Flask,其有一个WTForms,不过我嫌建造太多Form太麻烦,而且有些限制,没有首先考虑。


12楼:Sasoritattoo

回复 9# Hadron74 
    这种方式也不符合,global变量是下策

13楼:Sasoritattoo

回复 10# timespace 

    太多的冗余代码会影响代码的观看,写程序不是堆砌代码,在我看来是思维逻辑的展现,尽可能的简洁,一下子就能看到逻辑,某一层有某一层的逻辑,不要混淆在一起。

14楼:Sasoritattoo

回复 10# timespace 
回复 9# Hadron74 
回复 8# yjphhw 

想来想去,每种思路都是比较麻烦,不能够很好的达到我想要的效果。不过我最后我采用了一种在我看来稍微好些的思路,各位可以看一下:

check_format()函数是这样定义的
  1. @classmethod
  2. def check_format(cls, forms, args):
  3.     ret = True
  4.     error = ''
  5.     ret_dict = {}

  6.     if len(args) == 0:
  7.         return ret, error, ret_dict

  8.     for arg in args:
  9.         is_ok, error, value = check_protocol(arg, forms)
  10.         if not is_ok:
  11.             return is_ok, error, ret_dict
  12.         ret_dict[arg] = value

  13.     return ret, error, ret_dict
复制代码
另外我新建了一个protocol类,里面放着那些协议参数名,如下:
  1. class Protocol(Object):
  2.     LON = 'lon'
  3.     LAT = 'lat'
  4.     ID   = 'id'


  5. def _LAT_LON():
  6.     try:
  7.         value = float(tmp)
  8.     except:
  9.         error = Error.ERROR_LAT_LON
  10.         return False, error, tmp
  11.     return True, '', value

  12. def _ID():
  13.     pass

  14. protocol_dict = {
  15.     Protocol.LON:'_LON()',
  16.     Protocol.LAT:'_LAT()',
  17.     Protocol.ID:'_ID()'
  18. }

  19. tmp = ''

  20. def check_protocol(arg, args):
  21.     value = args.get(arg)
  22.     global tmp
  23.     tmp = value
  24.     func = protocol_dict[arg]
  25.     if func is not '':
  26.         return eval(func)
  27.     else:
  28.         return True, '', value
复制代码
在视图View层,你只需这样即可:
  1. @app.route('/test/')
  2. def test():
  3.     forms = request.args #或者request.forms
  4.     args = [Protocol.LAT, Protocol.LAT, Protocol.ID]
  5.     is_ok, error, DICTS = check_format(forms, args)
  6.     if not is_ok:
  7.         return error
  8.     lon = DICTS[Protocol.LON]
  9.     lat  = DICTS[Protocol.LAT]
  10.     id   = DICTS[Protocol.ID]
复制代码
这样的好处是你在视图函数里只需要知道check_format()即可,按照这个流程来就行了,知一知百,里面怎么实现不用关心,这样对于将来代码的维护者来说也是轻巧很多,很容易懂,而且减少了很多代码量,非常容易就看清框架思路。
目前,在我看来,这是目前来讲我感觉比较好些的解决思路。
关于check_protocol部分,如果能够实现通过eval执行函数并传递参数就更好了,只可惜我不知道怎么操作,如果有懂的人还请告知一下,至于protocol_dict中value值使用的是字符串,是担心直接函数的话会被多次执行。
如果有不同意见,欢迎继续交流哈~~

15楼:Hadron74

回复 14# Sasoritattoo 

你这里用全局变量tmp不是一个鲁棒的办法,容易出问题。原因一样,全局变量可能导致崩溃。
我不理解你为什么说直接调用函数会多次执行,这是怎么回事呢?为什么哑员参数传递不成?
你可以试试传函数参数的方法。代码如下:

  1. class Protocol(Object):
  2.     LON = 'lon'
  3.     LAT = 'lat'
  4.     ID   = 'id'


  5. def _LAT_LON(tmp):
  6.     try:
  7.         value = float(tmp)
  8.     except:
  9.         error = Error.ERROR_LAT_LON
  10.         return False, error, tmp
  11.     return True, '', value

  12. def _ID(tmp,others="None"):
  13.     pass

  14. protocol_dict = {
  15.     Protocol.LON:(_LON_LAT,[])
  16.     Protocol.LAT:(_LON_LAT,[])
  17.     Protocol.ID:(_ID,["others"])                   # 定义函数,和哑员参数
  18. }


  19. def check_protocol(arg, args):
  20.     value = args.get(arg)
  21.     func = protocol_dict[arg][0]
  22.     others = protocol_dict[arg][1]
  23.     if func is not '':
  24.         return func(value,*others)            # 函数调用,加哑员
  25.     else:
  26.         return True, '', value
复制代码

16楼:Sasoritattoo

回复 15# Hadron74 
    使用tmp是不得已而用之,的确是很容易出问题的,也不符合OO原则,多亏了你提供的这个办法, 效果很好,多谢。
    至于你说“我不理解你为什么说直接调用函数会多次执行?”其实是我使用eval()方法执行,不转成str的话,我在函数后加了(),所以被执行了一遍。最根本原因还是我对python有些特性不熟悉。
    看了你的代码,也明白了,原来函数作为一个对象保存起来是这样保存的。再谢。

17楼:Sasoritattoo

回复 10# timespace 
回复 9# Hadron74 
回复 8# yjphhw 
上面那样感觉已经不错了,但是实际使用起来,还是比较麻烦,甚至还不如我最开始写的更简洁明了呢,后来想了一下,发现还有一个方法可以继续优化,并且看起来非常的让人舒畅,这就是我理想中的code了。现贴之如下:
check_format()函数是这样定义的:

  1. def check_format(arg, value):
  2.     func = protocol_dict[arg][0]
  3.     if func is '':
  4.         raise CheckFormatException(0)
  5.     is_ok, error, result = func(value)
  6.     if not is_ok:
  7.         raise CheckFormException(error)
  8.     return result
复制代码
如果format信息不符合要求,直接甩出CheckFormatException错误,此错误里面包含错误码,如果正常就返回转型后的数据。

新建了一个protocol类,里面放着那些协议参数名,如下:
  1. class Protocol(Object):
  2.     LON = 'lon'
  3.     LAT = 'lat'
  4.     ID   = 'id'

  5. class CheckFormatException(Exception)
  6.     def __init__(self, error_code):
  7.         Exception.__init__(self, error_code)
  8.         self.error = error_code


  9. def _LAT_LON(value):
  10.     try:
  11.         value = float(value)
  12.     except:
  13.         error = Error.ERROR_LAT_LON
  14.         return False, error, value
  15.     return True, '', value

  16. def _ID(id):
  17.     try:
  18.         id = ObjectId(id)
  19.     except:
  20.         error = Error.ERROR_ID
  21.         return False, error, id
  22.     return True, '', id

  23. protocol_dict = {
  24.     Protocol.LON: (_LAT_LON, []),   #一不留神又写错了,_LAT_LON写成_LAT_LON()了,多谢楼下斧正
  25.     Protocol.LAT: (_LAT_LON, []),
  26.     Protocol.ID: (_ID, [])
  27. }
复制代码
在视图View层,你只需这样即可:
  1. @app.route('/test/')
  2. def test():
  3.     forms = request.forms
  4.     try:
  5.         lon = check_format(LON, forms.get(LON))
  6.         lat  = check_format(LAT, forms.get(LAT))
  7.         id   = check_format(ID, forms.get(ID))
  8.     exception CheckFormatException, e:
  9.         return e.error
  10.     
  11.     #lon and lat尽情使用吧,类型已经转换过来了
复制代码
所有的都是这么简单,给check_format()传递一个参数名,一个value
这里要多谢大家给予的帮助,尤其是@Hadron74兄弟,谢谢。
本帖完,谢谢观赏。


18楼:Hadron74

本帖最后由 Hadron74 于 2013-11-12 23:26 编辑

回复 17# Sasoritattoo 

我也是理想主义者,看了楼主的代码,觉得有很多可取的地方,但是try块嵌套用得太多了,而且,像一个float函数都得重写一个函数,太冗余了。我试着改写了你的代码:
  1. def check_format(arg, value):
  2.     func = protocol_dict[arg][0]
  3.     others = protol_dict[arg][1]
  4.     try:
  5.         result = func(value,*others)
  6.     except:
  7.         raise CheckFormException(protocol_dict[arg][2])           # 在这里根据不同arg抛出错误码,在字典中定义。
  8.     return result
复制代码
  1. class Protocol(Object):                              #个人觉得你这一层类的设计没有必要,可以精简掉
  2.     LON = 'lon'
  3.     LAT = 'lat'
  4.     ID   = 'id'

  5. class CheckFormatException(Exception)
  6.     def __init__(self, error_code):
  7.         Exception.__init__(self, error_code)                                 
  8.         self.error = error_code                                  #这个设计不错,学习了


  9. protocol_dict = {
  10.     Protocol.LON: (float, [],ERROR_float),                 # 这里直接用转换函数,没有必要再嵌套一层,而且注意函数对象后面不能有(),你的代码有误     
  11.     Protocol.LAT: (float, [],ERROR_float),        
  12.     Protocol.ID: (ObjectID, [],ERROR_ID)
  13. }
复制代码
其他代码一样,是不是更简洁一点呢?


19楼:Sasoritattoo

回复 18# Hadron74 

的确更简洁,这一部分也确实有些冗余,不过,这毕竟是一个demo,实际项目中可不只是做类型转换,还有比如检测文本是否符合长度,不能太少也不能太多,而且每种文本的长度限制是不一样的。  
     比如,检查文本是否符合长度的函数:
def _check_text_length(t, min_len, max_len):
    if min_len <= ken(t) <= max_len:
        return True, '', t
    return False, '', t

关于协议类去掉的考虑,如果去掉的话,整个协议文本散落在整个项目中,到时候改起来非常不好改,必须全局唯一性,所以,存在是必要的。
其实,实际中我原本是每个协议对应一个check_format()方法,demo就是随便举的例子,但是你说这里有冗余,也的确,想了一下,现在改掉了冗余。
参考了你上面的做法,不过仍然是使用了自定义的函数,只不过分为_check_format_float(v), _check_format_int(v), _check_format_text(t, min_len, max_len),为其分了几个大类,错误码就找你说得写在了protocol_dict[]中了。我的协议挺多的,这样一精简,少了一大半代码,看起来也清爽多了。Hadron74兄,劳烦看看是否有比我的更好方法?欢迎交流斧正  


20楼:Hadron74

回复 19# Sasoritattoo 

如果只维护一个字典表,程序的代码量,会好很多。我也没有什么可说的了。有一个技巧可能提高你可变哑员的编程。

就是用一个列表(诸如前面说的)的可变哑员,用一个字典用于可改变顺序的哑员。

  1. def func(a,b,c,d=None,e=1):
  2.     print a,b,c,d,e

  3. arg=[1.5,"a",1]               # 定义一个哑员变量表
  4. kwarg={"d":None,"e":2}  #定义一个字典哑员表

  5. func_dict={"func":(func,arg,kwarg)}      #把函数对象放在字典里


  6. func_dict["func"][0](*func_dict["func"][1],**func_dict["func"][2])          #函数调用
复制代码
具体函数哑员及调用的例子,参考我翻译的文稿:http://bbs.sciencenet.cn/blog-565112-523776.html 

21楼:Sasoritattoo

回复 20# Hadron74 
    受教了,谢谢!
    已经加了哑元键参数作为默认值。
    如果没猜错的话,@Hadron74兄竟然是跨生物信息学界和编程界的博士后,而且自己业余时间翻译了一本python教材书,非常的令人倾佩!






你可能感兴趣的:(Flask,网站服务器)