首发于Python之美
Flask最佳实践

Flask最佳实践

这是

写个抓取网易云音乐精彩评论的爬虫 - Python之美 - 知乎专栏

的续篇。本节将主要分享

GitHub - dongweiming/commentbox: 网易云音乐精彩评论

中我使用Flask的考虑以及延伸出来的问题。

1. 怎么用扩展

Application Factories提到了如何用扩展,也就是在create_app内才用init_app初始化对应的扩展。但是我推荐如下方式来组织扩展。首先 创建一个ext.py(叫extensions.py或者其他我也不反对)文件,用来管理全部的扩展:

from flask_mongoengine import MongoEngine
from flask_mako import MakoTemplates, render_template  # noqa

db = MongoEngine()
mako = MakoTemplates()

同样在create_app里面初始化(github.com/dongweiming/):

from ext import db, mako


def create_app():
    app = Flask(__name__, template_folder='templates',
                static_folder='static')
    app.config.from_object(config)
    mako.init_app(app)
    db.init_app(app)

    ...

    return app

举个例子,注意其中的db,我没有用 「from yourapplication.model import db」,而是使用了第三方的ext中import进来的。

而在model里面怎么用呢(commentbox/models.py at master · dongweiming/commentbox · GitHub):

from ext import db


class BaseModel(db.Document):
   ...

这样就解耦了扩展的使用,也就是不会有相互依赖的问题了,这就是init_app存在的意义。

2. 自定义RESTAPI的处理

现存的框架比较知名的有django-rest-framework和flask-restapi,但是这些框架我都不太满意,而对于我这个项目用它们还太重了。好吧,手动写一个实现。首先是借用 DispatcherMiddleware 实现对/j这样的路径特殊处理(commentbox/app.py at master · dongweiming/commentbox · GitHub):

from werkzeug.wsgi import DispatcherMiddleware                                                                     


app.wsgi_app = DispatcherMiddleware(app.wsgi_app, OrderedDict((                                                
        ('/j', json_api),                                                                                          
)))  

我希望/j开头的返回的响应都是json格式的内容:

from flask import Flask


class ApiFlask(Flask):                                                                                             
    def make_response(self, rv):                                                                                   
        if isinstance(rv, dict):                                                                                   
            if 'r' not in rv:                                                                                      
                rv['r'] = 1                                                                                        
            rv = ApiResult(rv)                                                                                     
        if isinstance(rv, ApiResult):                                                                              
            return rv.to_response()                                                                                
        return Flask.make_response(self, rv)                                                                       
                                                                                                                   
                                                                                                                   
json_api = ApiFlask(__name__)         

其中返回了一个额外的字段r, 如果是0表示响应的结果是正确的,为1表示响应的内容有问题。

接着我们自定义错误处理的方式,比如404返回这样:

{
    message: "Not Found"
}

怎么实现呢:

from flask import json                                                                                             
from werkzeug.wrappers import Response                                                                                                                                                                                               
                                                                                                              
                                                                                                                   
class ApiResult(object):                                                                                           
    def __init__(self, value, status=200):                                                                         
        self.value = value                                                                                         
        self.status = status                                                                                       
    def to_response(self):                                                                                         
        return Response(json.dumps(self.value),                                                                    
                        status=self.status,                                                                        
                        mimetype='application/json')                                                               
                                                                                                                                                               
                                                                                                                   
class ApiException(Exception):                                                                                     
    def __init__(self, message, status=400):                                                                       
        self.message = message                                                                                     
        self.status = status                                                                                       
    def to_result(self):                                                                                           
        return ApiResult({'message': self.message, 'r': 1},                                                        
                         status=self.status)


@json_api.errorhandler(ApiException)                                                                               
def api_error_handler(error):                                                                                      
    return error.to_result()                                                                                       
                                                                                                                   
                                                                                                                   
@json_api.errorhandler(403)                                                                                        
@json_api.errorhandler(404)                                                                                        
@json_api.errorhandler(500)                                                                                        
def error_handler(error):                                                                                          
    if hasattr(error, 'name'):                                                                                     
        msg = error.name                                                                                           
        code = error.code                                                                                          
    else:                                                                                                          
        msg = error.message                                                                                        
        code = 500                                                                                                 
    return ApiResult({'message': msg}, status=code)  

而且响应也被封装了:

def success(res=None, status_code=200):                                                                            
    res = res or {}                                                                                                
                                                                                                                   
    dct = {                                                                                                        
        'r': 1                                                                                                     
    }                                                                                                              
                                                                                                                   
    if res and isinstance(res, dict):                                                                              
        dct.update(res)                                                                                            
                                                                                                                   
    return ApiResult(dct, status_code)                                                                             
                                                                                                                   
                                                                                                                   
def failure(message, status_code):                                                                                 
    dct = {                                                                                                        
        'r': 0,                                                                                                    
        'status': status_code,                                                                                     
        'message': message                                                                                         
    }                                                                                                              
    return dct                                                                                                     
                                                                                                                   
                                                                                                                   
def updated(res=None):                                                                                             
    return success(res=res, status_code=204)                                                                       
                                                                                                                   
                                                                                                                   
def bad_request(message, res=None):                                                                                
    return failure(message, 400)

使用的时候可以让返回的正确和错误结果的格式都保持统一。

3. Redis序列化

我使用了mongoengine处理model,但是为了给后端减少压力,所以使用Redis缓存结果。大家知道Redis支持了很多数据结构,对我来说,其实是可以满足的,但是为了演示如何存储复杂对象,单个文档对象缓存的是序列化之后的结果,也就是一个字符串(github.com/dongweiming/):

cache.set(key, rs.to_json())

取的时候这样(commentbox/models.py at master · dongweiming/commentbox · GitHub):

rs = cache.get(key)
if rs:
    return cls.from_json(rs)

其中from_json和to_json都是mongoengine自带的,希望对大家在业务中的使用有启发。

4. local_settings.py

local_settings.py在豆瓣被广泛的使用,一般的Flask应用都会有一个config.py文件,包含一些配置,它会被放进版本库。但是线上和测试环境其中的一些设置是不一样的,比如DEBUG在线上一定是False,但是在测试环境就是True。 那么可以在config.py这么用(commentbox/config.py at master · dongweiming/commentbox · GitHub):

DEBUG = False

try:
    from local_settings import *  # noqa
except ImportError:
    pass

假如存在local_settings.py,那么配置就被会覆盖了,而local_settings.py就是在特定环境下才存在的。

5. 使用Mako

豆瓣在我印象里面好像都是没人使用Jinja2的。Mako是另一个知名模板语言,它有如下优点:

1. 性能和Jinja2相近,这一点[Jinja2也承认](Frequently Asked Questions)。

2. 有大型网站在使用,有质量保证。Reddit在2011年的月PV就达到10亿,豆瓣几乎全部用户产品都使用Mako模板,所以不需要担心没有大公司使用的案例。

3. 有知名Web框架支持。Pylons和Pyramid这两个Web框架内置Mako,而且把它作为默认模板。

4. 支持在模板中写几乎原生的Python语法的代码,对Python工程师友好,我已经见过多个人来了豆瓣爱上Mako而抛弃Jinja2的例子了。

5. 自带完整的缓存系统。Mako提供非常好的扩展接口,很容易切换成其他的缓存系统。

Jinja2和Mako的设计哲学有一点不同:Jinja2认为应该尽可能把逻辑从模板中移除,界限清晰,不允许在模板内写Python代码,也不支持全部的Python内置函数(只提供了很有限、最常用的一部分);而Mako正好相反,它最后会编译成Python代码以达到性能最优,在模板里面可以自由写后端逻辑,不需要传递就可以使用Python自带的数据结构和内置类。Jinja2带来的好处是模板引擎易于维护,并且模板有更好的可读性;而Mako是一个对Python工程师非常友好的语言,限制很少,完成模板开发工作时更有效率,整个项目的代码可维护性更好。

6. 合理使用Flask提供的资源。

这并不是这个项目中用到的实践。我在豆瓣东西的一个后台项目看到过这样用蓝图:


import views.story.user_story
import views.story.rec_pool
import views.story.export
import views.story.similar
... 省略了剩下的几十个import


def create_app():
    ...
    app.register_blueprint(
        views.story.user_story.bp, url_prefix="/story/user_story")
    app.register_blueprint(
        views.story.rec_pool.bp, url_prefix="/story/rec_pool")
    app.register_blueprint(
        views.story.export.bp, url_prefix="/story/export")
    app.register_blueprint(
        views.story.similar.bp, url_prefix="/story/similar")
    ... 继续省略剩下的那几十个register_blueprint

其实好的做法是什么呢:

from werkzeug.utils import find_modules, import_string


def register_blueprints(root, app):
    for name in find_modules(root, recursive=True):
        mod = import_string(name)
        if hasattr(mod, 'bp'):
            urls = name.split('.')
            prefix = '/{}/{}'.format(urls[-2], urls[-1])
            app.register_blueprint(
                mod.bp, url_prefix=prefix)


def create_app():
    ...
    register_blueprints('views', app)
    ...

是否懂了呢?

无耻的广告:《Python Web开发实战》上市了!

欢迎关注本人的微信公众号获取更多Python相关的内容(也可以直接搜索「Python之美」):



编辑于 2017-04-17 23:07