廖雪峰Python教程实战篇-Day5

aiohttp是一个较底层的框架,当有HTTP请求进入,aiohttp会生成一个 request 对象,经处理后返回一个 Response 对象。
但是,中间的处理过程需要我们自行去完成,所以我们要 在aiohttp基础上自己封装一个框架
1. 编写视图函数(URL处理函数)
1.1 aiohttp编写视图函数
如果仅仅用aiohttp编写视图函数,我们需要以下几步:
第一步,编写一个由  async/await  装饰的函数:
[python]  view plain  copy
  1. async def handle_url_xxx(request):    
  2.     ...    
第二步,传入视图函数的参数要自己从 request 中获取:
[python]  view plain  copy
  1. url_param = request.match_info['key']    
  2. query_params = parse_qs(request.query_string)  
第三步,自行构造返回的 Response 对象:
[python]  view plain  copy
  1. text = render('template', data)    
  2. return web.Response(text.encode('utf-8'))  
以上这些重复的过程, 可以在新建的框架中封装起来。
1.2 新建框架编写视图函数
首先 编写一个构造视图函数的装饰器 ,其中传递、储存URL信息 (path,method)
[python]  view plain  copy
  1. # 建立视图函数装饰器,用来存储、附带URL信息  
  2. def Handler_decorator(path, *, method):  
  3.     def decorator(func):  
  4.         @functools.wraps(func)  
  5.         def warpper(*args, **kw):  
  6.             return func(*args, **kw)          
  7.         warpper.__route__ = path  
  8.         warpper.__method__ = method  
  9.         return warpper  
  10.     return decorator  
  11. # 偏函数。GET POST 方法的路由装饰器  
  12. get = functools.partial(Handler_decorator, method = 'GET')  
  13. post = functools.partial(Handler_decorator, method = 'POST')  
这样就可以 直接通过装饰器,直接将一个函数映射成视图函数。
[python]  view plain  copy
  1. @get    
  2. def View(request):    
  3.      return response    
1.3 编写RequestHandler:处理request对象
我们已经完成了视图函数装饰器的编写,但这远远不够。视图函数仍无法从 request 中获取参数。
所以我们还要从 request 对象中提取相应视图函数所需的参数,并且视图函数并非都是 coroutine
因此, 需要定义一个能处理 request 请求的类来对视图函数进行封装, RequestHandler
RequestHandler 是一个类,分析视图函数所需的参数,再从 request 对象中将参数提取,调用视图函数(URL处理函数),并返回 web.Response 对象。
由于其定义了 __call__() 方法,其实例对象可以看作函数。
用这样一个 RequestHandler 类,就能处理各类 request 向对应视图函数发起的请求了。
1.3.1 解析视图函数
使用python自带库的 inspect 模块, 解析视图函数的参数
参考资料:inspect
[python]  view plain  copy
  1. # 使用inspect模块,检查视图函数的参数  
  2.   
  3. # inspect.Parameter.kind 类型:  
  4. # POSITIONAL_ONLY          位置参数  
  5. # KEYWORD_ONLY             命名关键词参数  
  6. # VAR_POSITIONAL           可选参数 *args  
  7. # VAR_KEYWORD              关键词参数 **kw  
  8. # POSITIONAL_OR_KEYWORD    位置或必选参数  
  9.   
  10. def get_required_kw_args(fn):  # 获取无默认值的命名关键词参数  
  11.     args = []  
  12.     ''''' 
  13.     def foo(a, b = 10, *c, d,**kw): pass 
  14.     sig = inspect.signature(foo) ==> <Signature (a, b=10, *c, d, **kw)> 
  15.     sig.parameters ==>  mappingproxy(OrderedDict([('a', <Parameter "a">), ...])) 
  16.     sig.parameters.items() ==> odict_items([('a', <Parameter "a">), ...)]) 
  17.     sig.parameters.values() ==>  odict_values([<Parameter "a">, ...]) 
  18.     sig.parameters.keys() ==>  odict_keys(['a', 'b', 'c', 'd', 'kw']) 
  19.     '''  
  20.     params = inspect.signature(fn).parameters  
  21.     for name, param in params.items():  
  22.         # 如果视图函数存在命名关键字参数,且默认值为空,获取它的key(参数名)  
  23.         if param.kind == inspect.Parameter.KEYWORD_ONLY and param.default == inspect.Parameter.empty:  
  24.             args.append(name)  
  25.     return tuple(args)  
  26.   
  27. def get_named_kw_args(fn):  # 获取命名关键词参数  
  28.     args = []  
  29.     params = inspect.signature(fn).parameters  
  30.     for name, param in params.items():  
  31.         if param.kind == inspect.Parameter.KEYWORD_ONLY:  
  32.             args.append(name)  
  33.     return tuple(args)  
  34.   
  35. def has_named_kw_arg(fn):  # 判断是否有命名关键词参数  
  36.     params = inspect.signature(fn).parameters  
  37.     for name, param in params.items():  
  38.         if param.kind == inspect.Parameter.KEYWORD_ONLY:  
  39.             return True  
  40.   
  41. def has_var_kw_arg(fn):  # 判断是否有关键词参数  
  42.     params = inspect.signature(fn).parameters  
  43.     for name, param in params.items():  
  44.         if param.kind == inspect.Parameter.VAR_KEYWORD:  
  45.             return True  
  46.   
  47. def has_request_arg(fn):   # 判断是否含有名叫'request'的参数,且位置在最后  
  48.     params = inspect.signature(fn).parameters  
  49.     found = False  
  50.     for name, param in params.items():  
  51.         if name == 'request':  
  52.             found = True  
  53.             continue  
  54.         if found and (  
  55.             param.kind != inspect.Parameter.VAR_POSITIONAL and   
  56.             param.kind != inspect.Parameter.KEYWORD_ONLY and   
  57.             param.kind != inspect.Parameter.VAR_KEYWORD):  
  58.             # 若判断为True,表明param只能是位置参数。且该参数位于request之后,故不满足条件,报错。  
  59.             raise ValueError('request parameter must be the last named parameter in function:%s%s' % (fn.__name__, str(sig)))  
  60.     return found  
1.3.2 提取request中的参数
request 是经aiohttp包装后的对象。其本质是一个 HTTP请求
请求状态(status) 请求首部(header) 内容实体(body) 三部分组成。
我们需要的参数包含在内容实体以及请求状态URI中。
request 对象封装了HTTP请求,可以通过 request 的属性调取值。
参考资料:aiohttp.web.Request
RequestHandler 需要处理以下问题:
1、确定HTTP请求的方法(’ POST’or’GET ’)(用 request.method 获取)
2、根据HTTP请求的 content_type 字段,选用不同解析方法获取参数。(用 request.content_type 获取)
3、将获取的参数经处理,使其完全符合视图函数接收的参数形式
4、调用视图函数
[python]  view plain  copy
  1. # 定义RequestHandler从视图函数中分析其需要接受的参数,从web.Request中获取必要的参数  
  2. # 调用视图函数,然后把结果转换为web.Response对象,符合aiohttp框架要求  
  3. class RequestHandler(object):  
  4.     def __init__(self, app, fn):  
  5.         self._app = app  
  6.         self._func = fn  
  7.         self._required_kw_args = get_required_kw_args(fn)  
  8.         self._named_kw_args = get_named_kw_args(fn)  
  9.         self._has_request_arg = has_request_arg(fn)  
  10.         self._has_named_kw_arg = has_named_kw_arg(fn)  
  11.         self._has_var_kw_arg = has_var_kw_arg(fn)  
  12.   
  13.     # 1.定义kw,用于保存参数  
  14.     # 2.判断视图函数是否存在关键词参数,如果存在根据POST或者GET方法将request请求内容保存到kw  
  15.     # 3.如果kw为空(说明request无请求内容),则将match_info列表里的资源映射给kw;若不为空,把命名关键词参数内容给kw  
  16.     # 4.完善_has_request_arg和_required_kw_args属性  
  17.     async def __call__(self, request):  
  18.         kw = None # 定义kw,用于保存request中参数  
  19.         if self._has_named_kw_arg or self._has_var_kw_arg: # 若视图函数有命名关键词或关键词参数  
  20.             if request.method == 'POST':  
  21.                 # 根据request参数中的content_type使用不同解析方法:  
  22.                 if request.content_type == None# 如果content_type不存在,返回400错误  
  23.                     return web.HTTPBadRequest(text='Missing Content_Type.')  
  24.                 ct = request.content_type.lower() # 小写,便于检查  
  25.                 if ct.startwith('application/json'):  # json格式数据  
  26.                     params = await request.json() # 仅解析body字段的json数据  
  27.                     if not isinstance(params, dict): # request.json()返回dict对象  
  28.                         return web.HTTPBadRequest(text='JSON body must be object.')  
  29.                     kw = params  
  30.                 # form表单请求的编码形式  
  31.                 elif ct.startwith('application/x-www-form-urlencoded'or ct.startswith('multipart/form-data'):  
  32.                     params = await request.post() # 返回post的内容中解析后的数据。dict-like对象。  
  33.                     kw = dict(**params) # 组成dict,统一kw格式  
  34.                 else:  
  35.                     return web.HTTPBadRequest(text='Unsupported Content-Type: %s' % request.content_type)  
  36.             if request.method == 'GET':  
  37.                 qs = request.query_string # 返回URL查询语句,?后的键值。string形式。  
  38.                 if qs:  
  39.                     kw = dict()  
  40.                     ''''' 
  41.                     解析url中?后面的键值对的内容 
  42.                     qs = 'first=f,s&second=s' 
  43.                     parse.parse_qs(qs, True).items() 
  44.                     >>> dict([('first', ['f,s']), ('second', ['s'])]) 
  45.                     '''  
  46.                     for k, v in parse.parse_qs(qs, True).items(): # 返回查询变量和值的映射,dict对象。True表示不忽略空格。  
  47.                         kw[k] = v[0]  
  48.         if kw is None:  # 若request中无参数  
  49.             # request.match_info返回dict对象。可变路由中的可变字段{variable}为参数名,传入request请求的path为值  
  50.             # 若存在可变路由:/a/{name}/c,可匹配path为:/a/jack/c的request  
  51.             # 则reqwuest.match_info返回{name = jack}  
  52.             kw = dict(**request.match_info)  
  53.         else# request有参数  
  54.             if self._has_named_kw_arg and (not self._has_var_kw_arg): # 若视图函数只有命名关键词参数没有关键词参数                 
  55.                 copy = dict()  
  56.                 # 只保留命名关键词参数  
  57.                 for name in self._named_kw_arg:  
  58.                     if name in kw:  
  59.                         copy[name] = kw[name]  
  60.                 kw = copy # kw中只存在命名关键词参数  
  61.             # 将request.match_info中的参数传入kw  
  62.             for k, v in request.match_info.items():  
  63.                 # 检查kw中的参数是否和match_info中的重复  
  64.                 if k in kw:  
  65.                     logging.warn('Duplicate arg name in named arg and kw args: %s' % k)   
  66.                 kw[k] = v  
  67.         if self._has_request_arg: # 视图函数存在request参数  
  68.             kw['request'] = request  
  69.         if self._required_kw_args: # 视图函数存在无默认值的命名关键词参数  
  70.             for name in self._required_kw_args:  
  71.                 if not name in kw: # 若未传入必须参数值,报错。  
  72.                     return web.HTTPBadRequest('Missing argument: %s' % name)  
  73.         # 至此,kw为视图函数fn真正能调用的参数  
  74.         # request请求中的参数,终于传递给了视图函数  
  75.         logging.info('call with args: %s' % str(kw))  
  76.         #try:  
  77.         r = await self._func(**kw)  
  78.         return r  
  79.         #except APIerror as e:  
  80.             #return dict(error=e.error, data=e.data, message=e.message)  

2. 编写add_route函数及add_static函数
2.1 编写视图函数注册函数
完成了 RequestHandler 类的编写,我们需要在 app 注册视图函数 (添加路由)。
add_route 函数功能:
1、验证视图函数是否拥有 method path 参数
2、将视图函数转变为协程
参考资料:
[python]  view plain  copy
  1. # 编写一个add_route函数,用来注册一个视图函数  
  2. def add_route(app, fn):  
  3.     method = getattr(fn, '__method__'None)  
  4.     path = getattr(fn, '__route__'None)  
  5.     if method is None or path is None:  
  6.         raise ValueError('@get or @post not defined in %s.' % fn.__name__)  
  7.     # 判断URL处理函数是否协程并且是生成器  
  8.     if not asyncio.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):  
  9.         # 将fn转变成协程  
  10.         fn = asyncio.coroutine(fn)  
  11.     logging.info('add route %s %s => %s(%s)' % (method, path, fn.__name__, ','.join(inspect.signature(fn).parameters.keys())))  
  12.     # 在app中注册经RequestHandler类封装的视图函数  
  13.     app.router.add_route(method, path, RequestHandler(app, fn))  
add_route 函数每次只能注册一个视图函数。 若要 批量注册视图函 数,需要编写一个批注册函数 add_routes
希望只提供模块路径,批注册函数将自动导入其中的视图函数进行注册。
[python]  view plain  copy
  1. # 导入模块,批量注册视图函数  
  2. def add_routes(app, module_name):  
  3.     n = module_name.rfind('.'# 从右侧检索,返回索引。若无,返回-1。  
  4.     # 导入整个模块  
  5.     if n == -1:  
  6.         # __import__ 作用同import语句,但__import__是一个函数,并且只接收字符串作为参数  
  7.         # __import__('os',globals(),locals(),['path','pip'], 0) ,等价于from os import path, pip  
  8.         mod = __import__(module_name, globals(), locals, [], 0)  
  9.     else:  
  10.         name = module_name[(n+1):]  
  11.         # 只获取最终导入的模块,为后续调用dir()  
  12.         mod = getattr(__import__(module_name[:n], globals(), locals, [name], 0), name)  
  13.     for attr in dir(mod): # dir()迭代出mod模块中所有的类,实例及函数等对象,str形式  
  14.         if attr.startswith('_'):  
  15.             continue # 忽略'_'开头的对象,直接继续for循环  
  16.         fn = getattr(mod, attr)  
  17.         # 确保是函数  
  18.         if callable(fn):  
  19.             # 确保视图函数存在method和path  
  20.             method = getattr(fn, '__method__'None)  
  21.             path = getattr(fn, '__route__'None)  
  22.             if method and path:  
  23.                 # 注册  
  24.                 add_route(app, fn)  
2.2 编写静态文件注册函数
编写 add_static 函数用于 注册静态文件 ,只提供文件路径即可进行注册
[python]  view plain  copy
  1. # 添加静态文件,如image,css,javascript等  
  2. def add_static(app):  
  3.     # 拼接static文件目录  
  4.     path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')  
  5.     # path = os.path.join(os.path.abspath('.'), 'static')  
  6.   
  7.     app.router.add_static('/static/', path)  
  8.     logging.info('add static %s => %s' % ('/static/', path))  

3. 初始化jinja2模板
我们使用jinja2作为模板引擎,在新框架中对jinja2模板进行初始化设置。
参考资料:jinja2
初始化jinja2需要以下几步:
1、对Environment类的参数 options 进行配置。
2、使用jinja提供的模板加载器加载模板文件,程序中选用 FileSystemLoader 加载器直接从模板文件夹加载模板。
3、有了加载器和 options 参数,传递给 Environment 类,添加过滤器,完成初始化。
[python]  view plain  copy
  1. def init_jinja2(app, **kw):  
  2.     logging.info('init jinja2...')  
  3.     # class Environment(**options)  
  4.     # 配置options参数  
  5.     options = dict(  
  6.         # 自动转义xml/html的特殊字符  
  7.         autoescape = kw.get('autoescape'True),  
  8.         # 代码块的开始、结束标志  
  9.         block_start_string = kw.get('block_start_string''{%'),  
  10.         block_end_string = kw.get('block_end_string''%}'),  
  11.         # 变量的开始、结束标志  
  12.         variable_start_string = kw.get('variable_start_string''{{'),  
  13.         variable_end_string = kw.get('variable_end_string''}}'),  
  14.         # 自动加载修改后的模板文件  
  15.         auto_reload = kw.get('auto_reload'True)  
  16.     )  
  17.     # 获取模板文件夹路径  
  18.     path = kw.get('path'None)  
  19.     if not path:  
  20.         path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')  
  21.     # Environment类是jinja2的核心类,用来保存配置、全局对象以及模板文件的路径  
  22.     # FileSystemLoader类加载path路径中的模板文件  
  23.     env = Environment(loader = FileSystemLoader(path), **options)  
  24.     # 过滤器集合  
  25.     filters = kw.get('filters'None)  
  26.     if filters:  
  27.         for name, f in filters.items():  
  28.             # filters是Environment类的属性:过滤器字典  
  29.             env.filters[name] = f  
  30.     # 所有的一切是为了给app添加__templating__字段  
  31.     # 前面将jinja2的环境配置都赋值给env了,这里再把env存入app的dict中,这样app就知道要到哪儿去找模板,怎么解析模板。  
  32.     app['__template__'] = env # app是一个dict-like对象  
编写一个过滤器:
[python] view plain copy
  1. def datetime_filter(t):  
  2.     delta = int(time.time() - t)  
  3.     if delta < 60:  
  4.         return u'1分钟前'  
  5.     if delta < 3600:  
  6.         return u'%s分钟前' % (delta//60)  
  7.     if delta < 86400:  
  8.         return u'%s小时前' % (delta//3600)  
  9.     if delta < 604800:  
  10.         return u'%s天前' % (delta//86400)  
  11.     dt = datetime.fromtimestamp(t)  
  12.     return u'%s年%s月%s日' % (dt.year, dt.month, dt.day)  


4. 编写middleware
middlerware是符合WSGI定义的中间件 。位于 服务端 客户端 之间对数据进行拦截处理的一个桥梁。可以看过服务器端的数据,经 middleware 一层层封装,最终传递给客户端。
参考资料:WSGI
web框架正是由一层层middleware的封装,才具备各种完善的功能。所以我们 需要编写middlerware对视图函数返回的数据进行处理 。如:构造 Response 对象。
其实middleware装饰器类似,它会对视图函数返回的数据进行处理,达成想要的目的
编写一个简单的用于打印日志的 middleware
# 编写用于输出日志的middleware
[python]  view plain  copy
  1. # 编写用于输出日志的middleware  
  2. # handler是视图函数  
  3. async def logger_factory(app, handler):  
  4.     async def logger(request):  
  5.         logging.info('Request: %s %s' % (request.method, request.path))  
  6.         return await handler(request)  
  7.     return logger  
接下来,编写构造 Response对象的 middleware
[python]  view plain  copy
  1. # 处理视图函数返回值,制作response的middleware  
  2. # 请求对象request的处理工序:  
  3. #              logger_factory => response_factory => RequestHandler().__call__ => handler  
  4. # 响应对象response的处理工序:  
  5. # 1、由视图函数处理request后返回数据  
  6. # 2、@get@post装饰器在返回对象上附加'__method__'和'__route__'属性,使其附带URL信息  
  7. # 3、response_factory对处理后的对象,经过一系列类型判断,构造出真正的web.Response对象  
  8. async def response_factory(app, handler):  
  9.     async def response(request):  
  10.         logging.info('Response handler...')  
  11.         r = await handler(request)  
  12.         logging.info('response result = %s' % str(r))  
  13.         if isinstance(r, web.StreamResponse): # StreamResponse是所有Response对象的父类  
  14.             return r # 无需构造,直接返回  
  15.         if isinstance(r, bytes):  
  16.             logging.info('*'*10)  
  17.             resp = web.Response(body=r) # 继承自StreamResponse,接受body参数,构造HTTP响应内容  
  18.             # Response的content_type属性  
  19.             resp.content_type = 'application/octet-stream'  
  20.             return resp  
  21.         if isinstance(r, str):  
  22.             if r.startswith('redirect:'): # 若返回重定向字符串  
  23.                 return web.HTTPFound(r[9:]) # 重定向至目标URL  
  24.             resp = web.Response(body=r.encode('utf-8'))  
  25.             resp.content_type = 'text/html;charset=utf-8' # utf-8编码的text格式  
  26.             return resp  
  27.         # r为dict对象时  
  28.         if isinstance(r, dict):  
  29.             # 在后续构造视图函数返回值时,会加入__template__值,用以选择渲染的模板  
  30.             template = r.get('__template__'None)   
  31.             if template is None# 不带模板信息,返回json对象  
  32.                 resp = web.Response(body=json.dumps(r, ensure_ascii=False, default=lambda obj: obj.__dict__).encode('utf-8'))  
  33.                 # ensure_ascii:默认True,仅能输出ascii格式数据。故设置为False。  
  34.                 # default:r对象会先被传入default中的函数进行处理,然后才被序列化为json对象  
  35.                 # __dict__:以dict形式返回对象属性和值的映射  
  36.                 resp.content_type = 'application/json;charset=utf-8'  
  37.                 return resp  
  38.             else# 带模板信息,渲染模板  
  39.                 # app['__templating__']获取已初始化的Environment对象,调用get_template()方法返回Template对象  
  40.                 # 调用Template对象的render()方法,传入r渲染模板,返回unicode格式字符串,将其用utf-8编码  
  41.                 resp = web.Response(body=app['__template__'].get_template(template).render(**r))  
  42.                 resp.content_type = 'text/html;charset=utf-8' # utf-8编码的html格式  
  43.                 return resp  
  44.         # 返回响应码  
  45.         if isinstance(r, int) and (600>r>=100):  
  46.             resp = web.Response(status=r)  
  47.             return resp  
  48.         # 返回了一组响应代码和原因,如:(200, 'OK'), (404, 'Not Found')  
  49.         if isinstance(r, tuple) and len(r) == 2:  
  50.             status_code, message = r  
  51.             if isinstance(status_code, int) and (600>status_code>=100):  
  52.                 resp = web.Response(status=r, text=str(message))  
  53.         resp = web.Response(body=str(r).encode('utf-8')) # 均以上条件不满足,默认返回  
  54.         resp.content_type = 'text/plain;charset=utf-8' # utf-8纯文本  
  55.         return resp  
  56.     return response  

5. 测试
创建一个test_view.py文件在同一目录下,用来编写测试用的视图函数:
[python]  view plain  copy
  1. from coroweb import get  
  2. import asyncio  
  3.  
  4. @get('/')  
  5. async def index(request):  
  6.     return '<h1>Awesome</h1>'  
  7.  
  8. @get('/hello')  
  9. async def hello(request):  
  10.     return '<h1>hello!</h1>'  
初始化app实例,运行:
[python]  view plain  copy
  1. if __name__ == '__main__':  
  2.   
  3.     async def init(loop):  
  4.         app = web.Application(loop = loop, middlewares=[logger_factory, response_factory])  
  5.         init_jinja2(app, filters=dict(datetime = datetime_filter))  
  6.         add_routes(app, 'test_view')  
  7.         add_static(app)  
  8.         srv = await loop.create_server(app.make_handler(), 'localhost'9000)  
  9.         logging.info('server started at http://127.0.0.1:9000...')  
  10.         return srv  
  11.   
  12.     loop = asyncio.get_event_loop()  
  13.     loop.run_until_complete(init(loop))  
  14.     loop.run_forever()  
用浏览器访问地址:localhost:9000 ~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值