廖雪峰Python教程 实战day05
1. Web程序工作流程
本文部分文字内容、图片摘自《Flask Web开发实战:入门、进阶与原理解析》,作者李辉。
在编写自己的Web框架之前,首先要理解常用的Web框架到底实现了什么功能。以Flask框架为例对应理解
向浏览器中输入如下网址并按下Enter
http://helloflask.com/hello
客户端向服务器端发送请求然后接收服务器返回的响应。
客户端通常指Web浏览器(简称浏览器),服务器端(Server Side)则指为用户提供服务的服务器,也是我们的程序运行的地方。
图1-1 请求响应循环示意图
图1-2 Flask Web程序工作流程
当用户访问一个URL,浏览器便生成对应的HTTP请求,经由互联网发送到对应的Web服务器。Web服务器接收请求,**通过WSGI将HTTP格式的请求数据转换成我们的Flask程序能够使用的Python数据。**在程序中,**Flask根据请求的URL执行对应的视图函数,获取返回值生成响应。**响应依次经过WSGI转换生成HTTP响应,再经由Web服务器传递,最终被发出请求的客户端接收。浏览器渲染响应中包含的HTML和CSS代码,并执行Java Script代码,最终把解析后的页面呈现在用户浏览器的窗口中。
2. 路由匹配(Route)
首先明确 URL的组成。
图2-1 URL组成
这一步是建立不同的URL和视图函数的映射关系。根据GET,POST等HTTP方法类型决定返回给客户端的具体页面。当请求发来后,Flask会根据请求报文中的URL(path部分)来尝试与这个表中的所有URL规则进行匹配,调用匹配成功的视图函数。如果没有找到匹配的URL规则,说明程序中没有处理这个URL的视图函数。
这样廖雪峰教程中注册路由部分代码段就可以理解,获取对应的HTTP方法,path并和URL规则建立关系。以装饰器的形式添加了HTTP方法和path路径。
def get(path):#通过@get()就附带了URL信息。
'''
Define decorator @get('/path')
'''
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
return func(*args, **kw)
wrapper.__method__ = 'GET'
wrapper.__route__ = path
return wrapper
return decorator
def add_route(app, fn):#单个注册
method = getattr(fn, '__method__', None)
path = getattr(fn, '__route__', None)
if path is None or method is None:
raise ValueError('@get or @post not defined in %s.' % str(fn))
if not asyncio.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):
fn = asyncio.coroutine(fn)
logging.info('add route %s %s => %s(%s)' % (method, path, fn.__name__, ', '.join(inspect.signature(fn).parameters.keys())))
app.router.add_route(method, path, RequestHandler(app, fn))
def add_routes(app, module_name):#批量注册路由
n = module_name.rfind('.')#对应python文件存为handles,使用rfind找到handles.py文件
if n == (-1): #如果没有匹配项,返回(-1)
mod = __import__(module_name, globals(), locals())
else:#找到了,返回字符串出现的最后一次位置。
name = module_name[n+1:]
mod = getattr(__import__(module_name[:n], globals(), locals(), [name]), name)
for attr in dir(mod):
if attr.startswith('_'):
continue
fn = getattr(mod, attr)
if callable(fn):#如果找到了对应的处理方法。
method = getattr(fn, '__method__', None)
path = getattr(fn, '__route__', None)
if method and path:
add_route(app, fn)
为了以上代码方便理解,把分别对关键参数打印,代码如下所示。
n = 'handlers'.rfind('.')
if n == (-1):
mod = __import__('handlers', globals(), locals())
else:
name = 'handlers'[n+1:]
mod = getattr(__import__('handlers'[:n], globals(), locals(), [name]), name)
print(dir(mod))
for attr in dir(mod):
if attr.startswith('_'):
continue
fn = getattr(mod, attr)
#print(fn)
if callable(fn):
method = getattr(fn, '__method__', None)
path = getattr(fn, '__route__', None)
#print(method,path)
结果如下,分别获取不同方法并切进行注册:
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'asyncio', 'base64', 'hashlib', 'json', 'logging', 're', 'time']
['Blog', 'Comment', 'User', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'asyncio', 'base64', 'get', 'hashlib', 'index', 'json', 'logging', 'next_id', 'post', 're', 'time']
注意:在rfind
的注释文档中,使用了 S.rfind(sub[, start[, end]]) ->int
这种形式。借由知乎大佬的解释
3. 函数参数分析
该部分涉及提取正确的参数。
def get_named_kw_args(fn):#参数处理函数。这里重点要理解inspect模块用法。
args = []
params = inspect.signature(fn).parameters
for name, param in params.items():
if param.kind == inspect.Parameter.KEYWORD_ONLY:
args.append(name)
return tuple(args)
举例表示inspect.signature(fn)
的用途
def f(a,b,c=1,*var,city,game='lol',**kw):pass
a = inspect.signature(f)
t = a.parameters
print(t)
for name, param in t.items():
print(name,param,param.kind,param.default)
OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">), ('c', <Parameter "c=1">), ('var', <Parameter "*var">), ('city', <Parameter "city">), ('game', <Parameter "game='lol'">), ('kw', <Parameter "**kw">)])
a a POSITIONAL_OR_KEYWORD <class 'inspect._empty'>
b b POSITIONAL_OR_KEYWORD <class 'inspect._empty'>
c c=1 POSITIONAL_OR_KEYWORD 1
var *var VAR_POSITIONAL <class 'inspect._empty'>
city city KEYWORD_ONLY <class 'inspect._empty'>
game game='lol' KEYWORD_ONLY lol
kw **kw VAR_KEYWORD <class 'inspect._empty'>
inspect.Parameter.KEYWORD_ONLY
代表函数的参数类型。
查阅文档可知:
class _ParameterKind(enum.IntEnum):
POSITIONAL_ONLY = 0
POSITIONAL_OR_KEYWORD = 1
VAR_POSITIONAL = 2
KEYWORD_ONLY = 3
VAR_KEYWORD = 4
由此可得,这一系列代码是用来获取不同类型的函数参数。
def get_named_kw_args(fn):#获取命名关键字参数
def has_named_kw_args(fn):#检查是否有命名关键字参数,有了返回True。
def has_var_kw_arg(fn):#检查是否有随机参数,有了返回True
def get_required_kw_args(fn):#检查 是否有命名关键字参数并且该参数没有默认值。
def has_request_arg(fn):#如果参数名为request并且 参数不是随机参数,不是命名关键字参数,不是关键字参数就报错。
在做完以上铺垫以后,就可以最后理解RequestHandler
类了。
class RequestHandler(object):
def __init__(self, app, fn):
self._app = app
self._func = fn
self._has_request_arg = has_request_arg(fn)
self._has_var_kw_arg = has_var_kw_arg(fn)
self._has_named_kw_args = has_named_kw_args(fn)
self._named_kw_args = get_named_kw_args(fn)
self._required_kw_args = get_required_kw_args(fn)
async def __call__(self, request):
kw = None
if self._has_var_kw_arg or self._has_named_kw_args or self._required_kw_args:
if request.method == 'POST':
if not request.content_type:
return web.HTTPBadRequest('Missing Content-Type.')
ct = request.content_type.lower()
if ct.startswith('application/json'):
params = await request.json()
if not isinstance(params, dict):
return web.HTTPBadRequest('JSON body must be object.')
kw = params
elif ct.startswith('application/x-www-form-urlencoded') or ct.startswith('multipart/form-data'):
params = await request.post()
kw = dict(**params)
else:
return web.HTTPBadRequest('Unsupported Content-Type: %s' % request.content_type)
if request.method == 'GET':
qs = request.query_string
if qs:
kw = dict()
for k, v in parse.parse_qs(qs, True).items():
kw[k] = v[0]
if kw is None:
kw = dict(**request.match_info)
else:
if not self._has_var_kw_arg and self._named_kw_args:
# remove all unamed kw:
copy = dict()
for name in self._named_kw_args:
if name in kw:
copy[name] = kw[name]
kw = copy
# check named arg:
for k, v in request.match_info.items():
if k in kw:
logging.warning('Duplicate arg name in named arg and kw args: %s' % k)
kw[k] = v
if self._has_request_arg:
kw['request'] = request
# check required kw:
if self._required_kw_args:
for name in self._required_kw_args:
if not name in kw:
return web.HTTPBadRequest('Missing argument: %s' % name)
logging.info('call with args: %s' % str(kw))
try:
r = await self._func(**kw)
return r
except APIError as e:
return dict(error=e.error, data=e.data, message=e.message)
4.中间件
middleware
是一种拦截器,一个URL在被某个函数处理前,可以经过一系列的middleware
的处理。
一个middleware
可以改变URL的输入、输出,甚至可以决定不继续处理而直接返回。middleware的用处就在于把通用的功能从每个URL处理函数中拿出来,集中放到一个地方。例如,一个记录URL日志的logger
可以简单定义如下:
async def logger_factory(app, handler):
async def logger(request):
logging.info('Request: %s %s' % (request.method, request.path))
# await asyncio.sleep(0.3)
return (await handler(request))
return logger
async def response_factory(app, handler):
async def response(request):
logging.info('Response handler...')
r = await handler(request)
if isinstance(r, web.StreamResponse):
return r
if isinstance(r, bytes):
resp = web.Response(body=r)
resp.content_type = 'application/octet-stream'
return resp
if isinstance(r, str):
if r.startswith('redirect:'):
return web.HTTPFound(r[9:])
resp = web.Response(body=r.encode('utf-8'))
resp.content_type = 'text/html;charset=utf-8'
return resp
if isinstance(r, dict):
template = r.get('__template__')
if template is None:
resp = web.Response(body=json.dumps(r, ensure_ascii=False, default=lambda o: o.__dict__).encode('utf-8'))
resp.content_type = 'application/json;charset=utf-8'
return resp
else:
resp = web.Response(body=app['__templating__'].get_template(template).render(**r).encode('utf-8'))
resp.content_type = 'text/html;charset=utf-8'
return resp
if isinstance(r, int) and r >= 100 and r < 600:
return web.Response(r)
if isinstance(r, tuple) and len(r) == 2:
t, m = r
if isinstance(t, int) and t >= 100 and t < 600:
return web.Response(t, str(m))
# default:
resp = web.Response(body=str(r).encode('utf-8'))
resp.content_type = 'text/plain;charset=utf-8'
return resp
return response
而response
这个middleware
把返回值转换为web.Response
对象再返回,以保证满足aiohttp
的要求。