返回介绍

Werkzeug 的使用

发布于 2025-04-20 18:52:15 字数 10494 浏览 0 评论 0 收藏

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 前缀的,这一点比较隐晦。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。