廖雪峰Python教程 实战day05

1. Web程序工作流程

本文部分文字内容、图片摘自《Flask Web开发实战:入门、进阶与原理解析》,作者李辉。

在编写自己的Web框架之前,首先要理解常用的Web框架到底实现了什么功能。以Flask框架为例对应理解

向浏览器中输入如下网址并按下Enter

http://helloflask.com/hello

客户端向服务器端发送请求然后接收服务器返回的响应。

客户端通常指Web浏览器(简称浏览器),服务器端(Server Side)则指为用户提供服务的服务器,也是我们的程序运行的地方。

python教程廖雪峰 廖雪峰python基础教程_HTTP


图1-1 请求响应循环示意图

python教程廖雪峰 廖雪峰python基础教程_ci_02


图1-2 Flask Web程序工作流程

当用户访问一个URL,浏览器便生成对应的HTTP请求,经由互联网发送到对应的Web服务器。Web服务器接收请求,**通过WSGI将HTTP格式的请求数据转换成我们的Flask程序能够使用的Python数据。**在程序中,**Flask根据请求的URL执行对应的视图函数,获取返回值生成响应。**响应依次经过WSGI转换生成HTTP响应,再经由Web服务器传递,最终被发出请求的客户端接收。浏览器渲染响应中包含的HTML和CSS代码,并执行Java Script代码,最终把解析后的页面呈现在用户浏览器的窗口中。

2. 路由匹配(Route)

首先明确 URL的组成。

python教程廖雪峰 廖雪峰python基础教程_ci_03


图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这种形式。借由知乎大佬的解释

python教程廖雪峰 廖雪峰python基础教程_HTTP_04

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的要求。