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里面初始化(https://github.com/dongweiming/commentbox/blob/master/app.py#L18):
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支持了很多数据结构,对我来说,其实是可以满足的,但是为了演示如何存储复杂对象,单个文档对象缓存的是序列化之后的结果,也就是一个字符串(https://github.com/dongweiming/commentbox/blob/master/models.py#L42):
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之美」):