Werkzeug 的使用
Werkzeug 是 WSGI 协议层工具集,本节我们来挖掘 Werkzeug 中有用的函数和类,帮助我们进行 Web 开发。它可以独立安装:
> pip install Werkzeug
DebuggedApplication
werkzeug.debug.DebuggedApplication 实现了杀手级的 Debug 调试器,可以用如下方式启用调试器:
from werkzeug.debug import DebuggedApplication from myapp import app app=DebuggedApplication(app, evalex=True)
如果在 Django 中使用它,可以通过 django-extensions 的 runserver_plus(http://bit.ly/28Rpopa )看一下 Tornado 框架集成 DebuggedApplication 的例子(app_tornado.py)。
首先安装 Tornado:
> pip install tornado
然后自定义请求处理类:
import tornado.web
from werkzeug.debug import DebuggedApplication
class Handler(tornado.web.RequestHandler):
def initialize(self, debug):
if debug:
self.write_error=self.write_debugger_error
def write_debugger_error(self, status_code,**kwargs):
assert isinstance(self.application, DebugApplication)
html=self.application.render_exception()
self.write(html.encode('utf-8', 'replace'))
如果是开启 DEBUG 模式,则使用 DebugApplication 的 render_exception 方法生成 HTML。基于 Handler 创建一个使用 GET 就会报错的 BadHandler:
class BadHandler(Handler):
def get(self):
raise Exception('This is a test')
self.write('You will never see this text.')
DebugApplication 类的内容如下:
import tornado.wsgi
from tornado.web import Application
from werkzeug.debug.tbtools import get_current_traceback
class RequestDispatcher(tornado.web._RequestDispatcher):
def set_request(self, request):
super(RequestDispatcher, self).set_request(request)
if '__debugger__' in request.uri:
return self.application.debug_container(request)
class DebugApplication(Application):
def__init__(self,*args,**kwargs):
super(DebugApplication, self).__init__(*args,**kwargs)
self.debug_app=DebuggedApplication(self, evalex=True)
self.debug_container=tornado.wsgi.WSGIContainer(self.debug_app)
def start_request(self, server_conn, request_conn):
return RequestDispatcher(self, request_conn)
def render_exception(self):
traceback=get_current_traceback()
for frame in traceback.frames:
self.debug_app.frames[frame.id]=frame
self.debug_app.tracebacks[traceback.id]=traceback
return traceback.render_full(evalex=True,
secret=self.debug_app.secret)
创建的路由统一放在函数中,虽然仅有/error/这一个可用地址,但是更利于独立管理:
def create_application(debug=False):
handlers=[
('/error/', BadHandler,{'debug':debug}),
]
if debug:
return DebugApplication(handlers, debug=True)
return Application(handlers, debug=debug)
主函数如下:
import tornado.ioloop
from tornado.options import define, options, parse_command_line
define('debug', default=False, type=bool, help='Run in debug mode.')
define('port', default=9000, type=int, help='Port on which to listen.')
parse_command_line()
logger=logging.getLogger()
port=options.port
application=create_application(debug=options.debug)
logger.info('Running tornado on port{}.'.format(port))
application.listen(port)
tornado.ioloop.IOLoop.instance().start()
使用 define 可以定义命令行参数的名字、类型和默认值。create_application 中的逻辑是,当 debug 为 False,也就是不指定 debug 参数时:
> python chapter4/section3/app_tornado.py
则使用默认的 Application。
如果开启 debug 模式:
> python chapter4/section3/app_tornado.py--debug
则使用 DebugApplication,即可以使用更友好的 werkzeug.debug.DebuggedApplication 了。
数据结构
Werkzeug 中提供了多种定制的数据结构,在工作中有时也会需要这样的数据结构,本节列出常用的到数据结构。
1.TypeConversionDict:它继承于 dict,执行 get 方法的可以指定值的类型。
In:from werkzeug.datastructures import TypeConversionDict
In:d=TypeConversionDict(foo='42', bar='blub')
In:d.get('foo', type=int)
Out:42
In:d.get('bar',-1, type=int)
Out:-1
2.ImmutableTypeConversionDict:不可变的 TypeConversionDict。
3.MultiDict:它继承于 TypeConversionDict,可以对相同的键传入多个值,会把这些值都保留下来。
In:from werkzeug.datastructures import MultiDict
In:d=MultiDict([('a', 'b'), ('a', 'c')])
In:d.getlist('a')
Out:['b', 'c']
In:list(d.iterlists())
Out:[('a', ['b', 'c'])]
In:d.setlist('d', ['e', 'f'])
In:d
Out:MultiDict([('a', 'b'), ('a', 'c'), ('d', 'e'), ('d', 'f')])
In:d.poplist('d')
Out:['e', 'f']
In:d.get('a')
Out:'b'
4.ImmutableMultiDict:不可变的 MultiDict。
5.OrderedMultiDict:它继承于 MultiDict,但是保留了字典的顺序。
6.ImmutableOrderedMultiDict:不可变的 OrderedMultiDict。
功能函数
Werkzeug 中提供了多个有用的函数。
1.cached_property:非常知名的装饰器,它通过描述符把方法执行的结果作为一个属性(property)缓存下来。
In:class Foo(object): ....: @cached_property ....: def bar(self): ....: print 'calculate something' ....: return 1 ....: In:foo=Foo() In:foo.bar calculate something Out:1 In:foo.bar #方法只执行了一次,此后都直接返回结果 Out:1
2.import_string:一个帮助我们直接通过字符串找到对应模块的功能函数。
In:import_string('os')
Out:<module 'os' from '/home/ubuntu/web_develop/venv/lib/python2.7/os.pyc'>
In:import_string('werkzeug.utils')
Out:<module 'werkzeug.utils' from '/home/ubuntu/web_develop/venv/local/lib/
python2.7/site-packages/werkzeug/utils.pyc'>
3.secure_filename:返回一个安全版本的文件名。
In:secure_filename('My cool movie.mov')
Out:'My_cool_movie.mov'
In:secure_filename('../../../etc/passwd')
Out:'etc_passwd'
In:secure_filename(u'i contain cool\xfcml\xe4uts.txt')
Out:'i_contain_cool_umlauts.txt'
密码加密
数据库中的重要字段(如密码)不能明文存储,需要加密之后存储。Web 开发常用到的方法是加盐哈希加密,也就是在加密时混入一段随机字符串(盐值,salt)再进行哈希加密(如 MD5、SHA1 等),这样即使密码相同,如果混入的盐值不同,那么哈希值也是不一样的。Werkzeug 中提供了密码加盐的哈希函数:
In:from werkzeug.security import generate_password_hash
In:pw_1=generate_password_hash('xiaoming')
In:pw_2=generate_password_hash('xiaoming')
In:pw_1
Out:'pbkdf2:sha1:1000$ynvwQwYK$1854dfb3b61124972d68bc8d7291d333bcf583de'
In:pw_2
Out:'pbkdf2:sha1:1000$SVT1AYa8$fdb43e5afc70f6751b11c5479e56632eab0cdf8f'
哈希之后的哈希字符串格式是“method$salt$hash”,其中 1000 表示迭代次数,默认是 1000。由于盐值是随机的,同一个密码生成的哈希值不一样,因而不容易被暴力破解。
经过哈希之后的字符串是不能获取原密码的,只能使用 check_password_hash 来验证:
In:check_password_hash(pw_1, 'xiaoming') Out:True In:check_password_hash(pw_2, 'xiaoming') Out:True In:check_password_hash(pw_1[:-1]+'a', 'xiaoming') Out:False
我们看一下 SQLAlchemy 记录密码的哈希值的方法:
from sqlalchemy.ext.hybrid import hybrid_property
class User(db.Model):
__tablename__='hashed_users'
id=db.Column(db.Integer, primary_key=True)
name=db.Column(db.String(128), nullable=False)
_password=db.Column(db.String(256), nullable=False)
def__init__(self, name, password):
self.name=name
self.password=password
@hybrid_property
def password(self):
return self._password
@password.setter
def_set_password(self, plaintext):
self._password=generate_password_hash(plaintext)
def verify_password(self, password):
return check_password_hash(self.password, password)
其中装饰器 hybrid_property 把 password 变成了一个混合属性,可以通过 user.password 属性来访问哈希的密码,也会在给 user.password 赋值的时候触发 password.setter。
看一下效果:
In:user=User(name='xiaoming', password='123')
In:db.session.add(user)
In:db.session.commit()
In:user.password
Out:u'pbkdf2:sha1:1000$ekbscfaZ$7473514367759bc2746532db99d82598a10bf97b'
In:user.verify_password('223')
Out:False
In:user.verify_password('123')
Out:True
中间件
中间件(Middleware)会对每次请求添加额外的处理。可以用来记录日志、会话管理、请求验证、性能分析等工作。Werkzeug 中提供了 10 个中间件,之前提到的 werkzeug.debug.Debugged Application 也是一个中间件。下面介绍 5 个常用的中间件。
SharedDataMiddleware
一般而言,静态文件都应该使用 Nginx 来服务,但是在测试环境中或者对资源响应要求不高时,也可以使用 SharedDataMiddleware 来提供这样的服务,之前实现的文件托管服务也使用了它。我们看一个例子(app_share_middleware.py):
import os
from flask import Flask
from werkzeug.wsgi import SharedDataMiddleware
app=Flask(__name__)
app.wsgi_app=SharedDataMiddleware(app.wsgi_app,{
'/static/':os.path.join(os.path.dirname(__file__), 'static')
})
if__name__=='__main__':
app.run(host='0.0.0.0', port=9000)
假设程序所在目录的 static 子目录下有一个叫作 main.js 的文件,可以使用 http://localhost:9000/static/main.js 访问。
ProfilerMiddleware
可以很方便地使用 ProfilerMiddleware 添加性能分析。当请求页面的时候,就可以获得分析的结果(app_profiler_middleware.py):
from flask import Flask
from werkzeug.contrib.profiler import ProfilerMiddleware
app=Flask(__name__)
app.wsgi_app=ProfilerMiddleware(app.wsgi_app, profile_dir='/tmp')
@app.route('/')
def hello():
return 'Hello'
if__name__=='__main__':
app.run(host='0.0.0.0', port=9000)
访问页面之后,结果被保存下来:
> ls/tmp/*.prof /tmp/GET.root.000000ms.1464519481.prof
如果不指定 profile_dir,会在终端输出分析结果。我们来看一部分终端输出:
> python chapter4/section3/app_profiler_middleware.py
*Running on http://0.0.0.0:9000/(Press CTRL+C to quit)
--------------------------------------------------------------------------------
PATH:'/'
319 function calls in 0.001 seconds
Ordered by:internal time, call count
ncalls tottime percall cumtime percall filename:lineno(function)
6 0.000 0.000 0.000 0.000/home/ubuntu/.virtualenvs/local/lib/
python2.7/site-packages/werkzeug/local.py:160(top)
2 0.000 0.000 0.000 0.000/home/ubuntu/.virtualenvs/local/lib/
python2.7/site-packages/werkzeug/datastructures.py:860(
_unicodify_header_value)
10 0.000 0.000 0.000 0.000/home/ubuntu/.virtualenvs/local/lib/
python2.7/site-packages/werkzeug/local.py:68(__getattr__)
DispatcherMiddleware
DispatcherMiddleware 是可以调度多个应用的中间件。在第 3 章 3.1 节的时候我们实现了一个 JSONResponse,现在利用 JSONResponse 实现如下功能:
- 当访问以/json 开头的地址时都默认自动用 jsonify 格式化。
- 访问其他地址不受影响。
代码如下(app_dispatcher_middleware.py):
from collections import OrderedDict
from flask import Flask, jsonify
from werkzeug.wrappers import Response
from werkzeug.wsgi import DispatcherMiddleware
app=Flask(__name__)
json_page=Flask(__name__)
class JSONResponse(Response):
@classmethod
def force_type(cls, rv, environ=None):
if isinstance(rv, dict):
rv=jsonify(rv)
return super(JSONResponse, cls).force_type(rv, environ)
json_page.response_class=JSONResponse
@json_page.route('/hello/')
def hello():
return{'message':'Hello World!'}
@app.route('/')
def index():
return 'The index page'
app.wsgi_app=DispatcherMiddleware(app.wsgi_app, OrderedDict((
('/json', json_page),
)))
if__name__=='__main__':
app.run(host='0.0.0.0', port=9000)
使用这个中间件对访问地址是有副作用的,“@json_page.route('/')”等价于“@app.route('/json/')”,也就是子路径的地址前面是有/json 前缀的,这一点比较隐晦。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论