教程

这份教程将向你介绍Bottle的开发理念和功能特性。既介绍Bottle的基本用法,也包含了进阶用法。你可以从头到尾通读一遍,也可当做开发时的参考。你也许对自动生成的 API参考 感兴趣。它包含了更多的细节,但解释没有这份教程详细。在 秘诀常见问题 可找到常见问题的解决办法。如果需要任何帮助,可加入我们的 邮件列表 或在 IRC频道 和我们交流。

安装

Bottle不依赖其他库,你需要做的仅是下载 bottle.py (开发版)到你的项目文件夹,然后开始写代码。

$ wget https://bottlepy.org/bottle.py

在终端运行以上命令,即可下载到Bottle的最新开发版,包含了所有新功能特性。如果更需要稳定性,你应该坚持使用Bottle的稳定版本。可在 PyPI 下载稳定版本,然后通过 pip (推荐), easy_install 或你的包管理软件安装。

$ sudo pip install bottle              # recommended
$ sudo easy_install bottle             # alternative without pip
$ sudo apt-get install python-bottle   # works for debian, ubuntu, ...

不管怎样,您都需要python 2.7或更高版本(包括3.4+)来运行瓶子应用程序。如果您没有在系统范围内安装软件包的权限,或者只是不想安装,请创建一个 virtualenv 第一:

$ virtualenv develop              # Create virtual environment
$ source develop/bin/activate     # Change default python to virtual one
(develop)$ pip install -U bottle  # Install bottle to virtual environment

如果还未安装virtualenv:

$ wget https://raw.github.com/pypa/virtualenv/master/virtualenv.py
$ python virtualenv.py develop    # Create virtual environment
$ source develop/bin/activate     # Change default python to virtual one
(develop)$ pip install -U bottle  # Install bottle to virtual environment

快速启动:"Hello World"

到目前为止,我假设你已经 安装 好了bottle或已将bottle.py拷贝到你的项目文件夹。接下来我们就可以写一个非常简单的"Hello World"了:

from bottle import route, run

@route('/hello')
def hello():
    return "Hello World!"

run(host='localhost', port=8080, debug=True)

就这么简单!保存为py文件并执行,用浏览器访问 http://localhost:8080/hello 就可以看到"Hello World!"。它的执行流程大致如下:

这个 route() decorator将一段代码绑定到URL路径。在本例中,我们将 /hello 路径到 hello() 功能。这叫A route (因此是装饰名)是这个框架最重要的概念。您可以定义任意数量的路由。每当浏览器请求URL时,都会调用关联函数,并将返回值发送回浏览器。就这么简单。

最后一行调用的 run() 函数启动了内置的开发服务器。它监听 localhost 的8080端口并响应请求, Control-c 可将其关闭。到目前为止,这个内置的开发服务器已经足够用于日常的开发测试了。它根本不需要安装,就可以让你的应用跑起来。在教程的后面,你将学会如何让你的应用跑在其他服务器上面(译者注:内置服务器不能满足生产环境的要求)

调试模式 在早期开发的时候非常有用,但请务必记得,在生产环境中将其关闭。

这只是一个关于如何用 Bottle 构建应用程序的基本概念的演示。继续阅读,你会发现还有什么可能。

默认应用

基于简单性考虑,这份教程中的大部分例子都使用一个模块层面的 route() 修饰器函数来定义route。这样的话,所有route都添加到了一个全局的“默认应用”里面,即是在第一次调用 route() 函数时,创建的一个 Bottle 类的实例。其他几个模块层面的修饰器函数都与这个“默认应用”有关,如果你偏向于面向对象的做法且不介意多打点字,你可以创建一个独立的应用对象,这样就可避免使用全局范围的“默认应用”。

from bottle import Bottle, run

app = Bottle()

@app.route('/hello')
def hello():
    return "Hello World!"

run(app, host='localhost', port=8080)

接下来的 默认应用 章节中将更详细地介绍这种做法。现在,你只需知道不止有一种选择就好了。

URL映射

在上一章中,我们实现了一个十分简单的web应用,只有一个URL映射(route)。让我们再来看一下“Hello World”中与routing有关的部分:

@route('/hello')
def hello():
    return "Hello World!"

这个 route() decorator将URL路径链接到回调函数,并向 default application . 但是,只有一条路径的应用程序有点无聊。我们再加一点(别忘了 from bottle import template ):

@route('/')
@route('/hello/<name>')
def greet(name='Stranger'):
    return template('Hello {{name}}, how are you?', name=name)

这个例子说明了两件事情,一个回调函数可绑定多个route,你也可以在URL中添加通配符,然后在回调函数中使用它们。

动态URL映射

包含通配符的route,我们称之为动态route(与之对应的是静态route),它能匹配多个URL地址。一个通配符包含在一对尖括号里面(像这样 <name> ),通配符之间用"/"分隔开来。如果我们将URL定义为 /hello/<name> 这样,那么它就能匹配 /hello/alice/hello/bob 这样的浏览器请求,但不能匹配 /hello , /hello//hello/mr/smith

URL中的通配符都会当作参数传给回调函数,直接在回调函数中使用。这样可以漂亮地实现RESTful形式的URL。例子如下:

@route('/wiki/<pagename>')            # matches /wiki/Learning_Python
def show_wiki_page(pagename):
    ...

@route('/<action>/<user>')            # matches /follow/defnull
def user_api(action, user):
    ...

过滤器可用于定义更具体的通配符,和/或在将URL传递到回调之前转换其覆盖部分。筛选的通配符声明为 <name:filter><name:filter:config> . 可选配置部分的语法取决于使用的筛选器。

已实现下面几种形式的过滤器,后续可能会继续添加:

  • :int 匹配一个数字,自动将其转换为int类型。

  • :float 与:int类似,用于浮点数。

  • :path 匹配一个路径(包含"/")

  • :re 匹配config部分的一个正则表达式,不更改被匹配到的值

让我们来看看具体的使用例子:

@route('/object/<id:int>')
def callback(id):
    assert isinstance(id, int)

@route('/show/<name:re:[a-z]+>')
def callback(name):
    assert name.isalpha()

@route('/static/<path:path>')
def callback(path):
    return static_file(path, ...)

您也可以添加自己的过滤器。见 URL映射 有关详细信息。

HTTP请求方法

HTTP协议定义了几个 `request methods`_ _(有时称为“动词”)用于不同的任务。GET是没有指定其他方法的所有路由的默认值。这些路由将只匹配GET请求。要处理其他方法,如post、put、delete或patch,请添加 method 关键字参数 route() 装饰师或使用五个替代装饰师之一: get()post()put()delete()patch() .

POST方法一般用于HTML表单的提交。下面是一个使用POST来实现用户登录的例子:

from bottle import get, post, request # or route

@get('/login') # or @route('/login')
def login():
    return '''
        <form action="/login" method="post">
            Username: <input name="username" type="text" />
            Password: <input name="password" type="password" />
            <input value="Login" type="submit" />
        </form>
    '''

@post('/login') # or @route('/login', method='POST')
def do_login():
    username = request.forms.get('username')
    password = request.forms.get('password')
    if check_login(username, password):
        return "<p>Your login information was correct.</p>"
    else:
        return "<p>Login failed.</p>"

在这个例子中, /login 绑定了两个回调函数,一个回调函数响应GET请求,一个回调函数响应POST请求。如果浏览器使用GET请求访问 /login ,则调用login_form()函数来返回登录页面,浏览器使用POST方法提交表单后,调用login_submit()函数来检查用户有效性,并返回登录结果。接下来的 请求数据 (Request Data) 章节中,会详细介绍 Request.forms 的用法。

特殊请求方法: HEAD 和 ANY

HEAD方法类似于GET方法,但服务器不会返回HTTP响应正文,一般用于获取HTTP原数据而不用下载整个页面。Bottle像处理GET请求那样处理HEAD请求,但是会自动去掉HTTP响应正文。你无需亲自处理HEAD请求。

另外,非标准的ANY方法做为一个低优先级的fallback:在没有其它route的时候,监听ANY方法的route会匹配所有请求,而不管请求的方法是什么。这对于用做代理的route很有用,可将所有请求都重定向给子应用。

总而言之:HEAD请求被响应GET请求的route来处理,响应ANY请求的route处理所有请求,但仅限于没有其它route来匹配原先的请求的情况。就这么简单。

静态文件映射

Bottle不会处理像图片或CSS文件的静态文件请求。你需要给静态文件提供一个route,一个回调函数(用于查找和控制静态文件的访问)。

from bottle import static_file
@route('/static/<filename>')
def server_static(filename):
    return static_file(filename, root='/path/to/your/static/files')

static_file() 函数用于响应静态文件的请求。 (详见 静态文件 )这个例子只能响应在 /path/to/your/static/files 目录下的文件请求,因为 <filename> 这样的通配符定义不能匹配一个路径(路径中包含"/")。 为了响应子目录下的文件请求,我们需要更改 path 过滤器的定义:

@route('/static/<filepath:path>')
def server_static(filepath):
    return static_file(filepath, root='/path/to/your/static/files')

使用 root='./static/files' 这样的相对路径的时候,请注意当前工作目录 (./) 不一定是项目文件夹。

错误页面

如果出错了,Bottle会显示一个默认的错误页面,提供足够的debug信息。你也可以使用 error() 函数来自定义你的错误页面:

from bottle import error
@error(404)
def error404(error):
    return 'Nothing here, sorry'

从现在开始,在遇到404错误的时候,将会返回你在上面自定义的页面。传给error404函数的唯一参数,是一个 HTTPError 对象的实例。除此之外,这个回调函数与我们用来响应普通请求的回调函数没有任何不同。你可以从 request 中读取数据, 往 response 中写入数据和返回所有支持的数据类型,除了 HTTPError 的实例。

只有在你的应用返回或raise一个 HTTPError 异常的时候(就像 abort() 函数那样),处理Error的函数才会被调用。更改 Request.status 或返回 HTTPResponse 不会触发错误处理函数。

生成内容

在纯WSGI环境里,你的应用能返回的内容类型相当有限。应用必须返回一个iterable的字节型字符串。你可以返回一个字符串(因为字符串是iterable的),但这会导致服务器按字符来传输你的内容。Unicode字符串根本不允许。这不是很实用。

Bottle支持返回更多的内容类型,更具弹性。它甚至能在合适的情况下,在HTTP头中添加 Content-Length 字段和自动转换unicode编码。下面列出了所有你能返回的内容类型,以及框架处理方式的一个简述。

辞典

上面已经提及,Python中的字典类型(或其子类)会被自动转换为JSON字符串。返回给浏览器的时候,HTTP头的 Content-Type 字段被自动设置为 `` application/json`` 。可十分简单地实现基于JSON的API。Bottle同时支持json之外的数据类型,详见 tutorial-output-filter

空字符串, FalseNone 或其他非真值:

输出为空, Content-Length 设为0。

Unicode的问题

Unicode字符串 (or iterables yielding unicode strings) 被自动转码, Content-Type 被默认设置为utf8,接着视之为普通字符串(见下文)。

字节串

Bottle将字符串当作一个整体来返回(而不是按字符来遍历),并根据字符串长度添加 Content-Length 字段。包含字节型字符串的列表先被合并。其它iterable的字节型字符串不会被合并,因为它们也许太大来,耗内存。在这种情况下, Content-Length 字段不会被设置。

实例 HTTPErrorHTTPResponse

返回它们和直接raise出来有一样的效果。对于 HTTPError 来说,会调用错误处理程序。详见 错误页面

文件对象

任何有 .read() 方法的对象都被当成一个file-like对象来对待,会被传给 WSGI Server 框架定义的 wsgi.file_wrapper callable对象来处理。一些WSGI Server实现会利用优化过的系统调用(sendfile)来更有效地传输文件,另外就是分块遍历。可选的HTTP头,例如 Content-LengthContent-Type 不会被自动设置。尽可能使用 send_file() 。详见 静态文件

iTerables和发电机

你可以在回调函数中使用 yield 语句,或返回一个iterable的对象,只要该对象返回的是字节型字符串,unicode字符串, HTTPErrorHTTPResponse 实例。不支持嵌套iterable对象,不好意思。注意,在iterable对象返回第一个非空值的时候,就会把HTTP状态码和HTTP头发送给浏览器。稍后再更改它们就起不到什么作用了。

以上列表的顺序非常重要。在你返回一个 str 类的子类的时候,即使它有 .read() 方法,它依然会被当成一个字符串对待,而不是文件,因为字符串先被处理。

改变默认编码

Bottle使用 Content-Typecharset 参数来决定编码unicode字符串的方式。默认的 Content-Typetext/html;charset=UTF8 ,可在 Response.content_type 属性中修改,或直接设置 Response.charset 的值。关于 Response 对象的介绍,详见 Response 对象

from bottle import response
@route('/iso')
def get_iso():
    response.charset = 'ISO-8859-15'
    return u'This will be sent with ISO-8859-15 encoding.'

@route('/latin9')
def get_latin():
    response.content_type = 'text/html; charset=latin9'
    return u'ISO-8859-15 is also known as latin9.'

在极少情况下,Python中定义的编码名字和HTTP标准中的定义不一样。这样,你就必须同时修改 Response.content_type` (发送给客户端的)和设置 Response.charset 属性 (用于编码unicode)。

静态文件

您可以直接返回文件对象,但是 static_file() 是服务静态文件的推荐方法。它自动猜测mime类型,并添加 Last-Modified 头,将路径限制为 root 出于安全原因,目录并生成适当的错误响应(权限错误为403,丢失文件为404)。它甚至支持 If-Modified-Since 并最终生成 304 Not Modified 反应。您可以通过自定义的mime类型来禁用猜测。

from bottle import static_file
@route('/images/<filename:re:.*\.png>')
def send_image(filename):
    return static_file(filename, root='/path/to/image/files', mimetype='image/png')

@route('/static/<filename:path>')
def send_static(filename):
    return static_file(filename, root='/path/to/static/files')

如果确实需要,你可将 static_file() 的返回值当作异常raise出来。

强制下载

大多数浏览器在知道MIME类型的时候,会尝试直接调用相关程序来打开文件(例如PDF文件)。如果你不想这样,你可强制浏览器只是下载该文件,甚至提供文件名。:

@route('/download/<filename:path>')
def download(filename):
    return static_file(filename, root='/path/to/static/files', download=filename)

如果 download 参数的值为 True ,会使用原始的文件名。

HTTP错误和重定向

abort() 函数是生成HTTP错误页面的一个捷径。

from bottle import route, abort
@route('/restricted')
def restricted():
    abort(401, "Sorry, access denied.")

为了将用户访问重定向到其他URL,你在 Location 中设置新的URL,接着返回一个 303 See Otherredirect() 函数可以帮你做这件事情。

from bottle import route, redirect
@route('/wrong/url')
def wrong():
    redirect("/right/url")

你可以在第二个参数中提供另外的HTTP状态码。

注解

两个函数都将通过引发 HTTPResponse 例外。

其他异常

除了 HTTPResponseHTTPError 以外的其他异常,都会导致500错误,所以不会造成WSGI服务器崩溃。你将 bottle.app().catchall 的值设为 False 来关闭这种行为,以便在你的中间件中处理异常。

Response 对象

诸如HTTP状态码,HTTP响应头,用户cookie等元数据都保存在一个名字为 response 的对象里面,接着被传输给浏览器。你可直接操作这些元数据或使用一些更方便的函数。在API章节可查到所有相关API(详见 Response ),这里主要介绍一些常用方法。

状态码

HTTP状态码 控制着浏览器的行为,默认为 200 OK 。多数情况下,你不必手动修改 Response.status 的值,可使用 abort() 函数或return一个 HTTPResponse 实例(带有合适的状态码)。虽然所有整数都可当作状态码返回,但浏览器不知道如何处理 HTTP标准 中定义的那些状态码之外的数字,你也破坏了大家约定的标准。

响应头

Cache-ControlLocation 之类的响应头通过 Response.set_header() 来定义。这个方法接受两个参数,一个是响应头的名字,一个是它的值,名字是大小写敏感的。

@route('/wiki/<page>')
def wiki(page):
    response.set_header('Content-Language', 'en')
    ...

大多数的响应头是唯一的,meaning that only one header per name is send to the client。一些特殊的响应头在一次response中允许出现多次。使用 Response.add_header() 来添加一个额外的响应头,而不是 Response.set_header()

response.set_header('Set-Cookie', 'name=value')
response.add_header('Set-Cookie', 'name2=value2')

请注意,这只是一个例子。如果你想使用cookie,详见 ahead

Cookies

Cookie是储存在浏览器配置文件里面的一小段文本。你可通过 Request.get_cookie() 来访问已存在的Cookie,或通过 Response.set_cookie() 来设置新的Cookie。

@route('/hello')
def hello_again():
    if request.get_cookie("visited"):
        return "Welcome back! Nice to see you again"
    else:
        response.set_cookie("visited", "yes")
        return "Hello there! Nice to meet you"

Response.set_cookie() 方法接受一系列额外的参数,来控制Cookie的生命周期及行为。一些常用的设置如下:

  • max_age: 最大有效时间,以秒为单位 (默认: None)

  • expires: 一个datetime对象或一个UNIX timestamp (默认: None)

  • domain: 可访问该Cookie的域名 (默认: 当前域名)

  • path: 限制cookie的访问路径 (默认: /)

  • secure: 只允许在HTTPS链接中访问cookie (默认: off)

  • 仅限于: 阻止客户端javascript读取此cookie(默认:关闭,需要python 2.7或更高版本)。

  • same_site: 禁止第三方使用cookie。允许的属性: laxstrict . 在严格模式下,永远不会发送cookie。在LAX模式下,cookie只与顶级GET请求一起发送。

如果 expiresmax_age 两个值都没设置,cookie会在当前的浏览器session失效或浏览器窗口关闭后失效。在使用cookie的时候,应该注意一下几个陷阱。

  • 在大多数浏览器中,cookie的最大容量为4KB。

  • 一些用户将浏览器设置为不接受任何cookie。大多数搜索引擎也忽略cookie。确保你的应用在无cookie的时候也能工作。

  • cookie被储存在客户端,也没被加密。你在cookie中储存的任何数据,用户都可以读取。更坏的情况下,cookie会被攻击者通过 XSS 偷走,一些已知病毒也会读取浏览器的cookie。既然如此,就不要在cookie中储存任何敏感信息。

  • cookie可以被伪造,不要信任cookie!

Cookie签名

上面提到,cookie容易被客户端伪造。Bottle可通过加密cookie来防止此类攻击。你只需在读取和设置cookie的时候,通过 secret 参数来提供一个密钥。如果cookie未签名或密钥不匹配, Request.get_cookie() 方法返回 None

@route('/login')
def do_login():
    username = request.forms.get('username')
    password = request.forms.get('password')
    if check_login(username, password):
        response.set_cookie("account", username, secret='some-secret-key')
        return template("<p>Welcome {{name}}! You are now logged in.</p>", name=username)
    else:
        return "<p>Login failed.</p>"

@route('/restricted')
def restricted_area():
    username = request.get_cookie("account", secret='some-secret-key')
    if username:
        return template("Hello {{name}}. Welcome back.", name=username)
    else:
        return "You are not logged in. Access denied."

例外,Bottle自动序列化储存在签名cookie里面的数据。你可在cookie中储存任何可序列化的对象(不仅仅是字符串),只要对象大小不超过4KB。

警告

签名cookie在客户端不加密(译者注:即在客户端没有经过二次加密),也没有写保护(客户端可使用之前的cookie)。给cookie签名的主要意义在于在cookie中存储序列化对象和防止伪造cookie,依然不要在cookie中存储敏感信息。

请求数据 (Request Data)

可通过全局的 request 对象来访问Cookies,HTTP头,HTML的 <form> 字段,以及其它的请求数据。这个特殊的对象总是指向 当前 的请求,即使在同时处理多个客户端连接的多线程情况下。

from bottle import request, route, template

@route('/hello')
def hello():
    name = request.cookies.username or 'Guest'
    return template('Hello {{name}}', name=name)

request 对象继承自 BaseRequest ,提供了丰富的API来访问数据。虽然我们只介绍最常用的特性,也足够入门了。

介绍 FormsDict

Bottle使用了一个特殊的字典来储存表单数据和cookies。 FormsDict 表现得像一个普通的字典,但提供了更方便的额外功能。

属性访问 :字典中所有的值都可以当做属性来访问。这些虚拟的属性返回unicode字符串。在字典中缺少对应的值,或unicode解码失败的情况下,属性返回的字符串为空。

name = request.cookies.name

# is a shortcut for:

name = request.cookies.getunicode('name') # encoding='utf-8' (default)

# which basically does this:

try:
    name = request.cookies.get('name', '').decode('utf-8')
except UnicodeError:
    name = u''

一个key对应多个value: FormsDictMutilDict 的子类,一个key可存储多个value。标准的字典访问方法只返回一个值,但 getall() 方法会返回一个包含了所有value的一个list(也许为空)。

for choice in request.forms.getall('multiple_choice'):
    do_something(choice)

WTForms支持: 一些第三方库(例如 WTForms )希望输入中的所有字典都是unicode的。 FormsDict.decode() 帮你做了这件事情。它将所有value重新编码,并返回原字典的一个拷贝,同时保留所有特性,例如一个key对应多个value。

注解

Python2 中,所有的key和value都是byte-string。如果你需要unicode,可使用 FormsDict.getunicode() 方法或像访问属性那样访问。这两种方法都试着将字符串转码(默认: utf8),如果失败,将返回一个空字符串。无需捕获 UnicodeError 异常。

>>> request.query['city']
'G\xc3\xb6ttingen'  # A utf8 byte string
>>> request.query.city
u'Göttingen'        # The same string as unicode

Python3 中,所有的字符串都是unicode。但HTTP是基于字节的协议,在byte-string被传给应用之前,服务器必须将其转码。安全起见,WSGI协议建议使用ISO-8859-1 (即是latin1),一个可反转的单字节编码,可被转换为其他编码。Bottle通过 FormsDict.getunicode() 和属性访问实现了转码,但不支持字典形式的访问。通过字典形式的访问,将直接返回服务器返回的字符串,未经处理,这或许不是你想要的。

>>> request.query['city']
'Göttingen' # An utf8 string provisionally decoded as ISO-8859-1 by the server
>>> request.query.city
'Göttingen'  # The same string correctly re-encoded as utf8 by bottle

如果你整个字典包含正确编码后的值(e.g. for WTForms),可通过 FormsDict.decode() 方法来获取一个转码后的拷贝(译者注:一个新的实例)。

Cookies

Cookie是客户端浏览器存储的一些文本数据,在每次请求的时候发送回给服务器。Cookie被用于在多次请求间保留状态信息(HTTP本身是无状态的),但不应该用于保存安全相关信息。因为客户端很容易伪造Cookie。

可通过 BaseRequest.cookies (一个 FormsDict) 来访问所有客户端发来的Cookie。下面的是一个基于Cookie的访问计数。

from bottle import route, request, response
@route('/counter')
def counter():
    count = int( request.cookies.get('counter', '0') )
    count += 1
    response.set_cookie('counter', str(count))
    return 'You visited this page %d times' % count

BaseRequest.get_cookie() 是访问cookie的另一种方法。它支持解析 signed cookies

HTTP头

所有客户端发送过来的HTTP头(例如 Referer, AgentAccept-Language)存储在一个 WSGIHeaderDict 中,可通过 BaseRequest.headers 访问。 WSGIHeaderDict 基本上是一个字典,其key大小写敏感。

from bottle import route, request
@route('/is_ajax')
def is_ajax():
    if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
        return 'This is an AJAX request'
    else:
        return 'This is a normal request'

查询变量

查询字符串(例如 /forum?id=1&page=5 )一般用于向服务器传输键值对。你可通过 BaseRequest.query ( FormsDict 类的实例) 来访问,和通过 BaseRequest.query_string 来获取整个字符串。

from bottle import route, request, response, template
@route('/forum')
def display_forum():
    forum_id = request.query.id
    page = request.query.page or '1'
    return template('Forum ID: {{id}} (page {{page}})', id=forum_id, page=page)

处理HTML的 <form> 标签

让我们从头开始。在HTML中,一个典型的 <form> 标签看起来是这样的。

<form action="/login" method="post">
    Username: <input name="username" type="text" />
    Password: <input name="password" type="password" />
    <input value="Login" type="submit" />
</form>

action 属性指定了用于接收表单数据的URL, method 定义了使用的HTTP方法( GETPOST )。如果使用GET方法,表单中的数据会附加到URL后面,可通过 BaseRequest.query 来访问。这被认为是不安全的,且有其它限制。所以这里我们使用POST方法。如果有疑惑,就使用 POST 吧。

通过POST方法传输的表单字段,作为一个 FormsDict 存储在 BaseRequest.forms 中。服务器端的代码看起来是这样的。

from bottle import route, request

@route('/login')
def login():
    return '''
        <form action="/login" method="post">
            Username: <input name="username" type="text" />
            Password: <input name="password" type="password" />
            <input value="Login" type="submit" />
        </form>
    '''

@route('/login', method='POST')
def do_login():
    username = request.forms.get('username')
    password = request.forms.get('password')
    if check_login(username, password):
        return "<p>Your login information was correct.</p>"
    else:
        return "<p>Login failed.</p>"

有其它一些属性也可以用来访问表单数据。为了方便,一些属性包含了多个来源的数据。下面的表格可给你一个直观的印象。

属性

获取表单字段

POST表单数据

文件上传

BaseRequest.query

BaseRequest.forms

BaseRequest.files

BaseRequest.params

BaseRequest.GET

BaseRequest.POST

文件上传

为了支持文件上传,我们需要小改一下上面 <form> 标签,加上 enctype="multipart/form-data" 属性,告诉浏览器用另一种方式编码表单数据。接下来,我们添加 <input type="file" /> 标签,让用户可以选择需要上传的文件。例子如下。

<form action="/upload" method="post" enctype="multipart/form-data">
  Category:      <input type="text" name="category" />
  Select a file: <input type="file" name="upload" />
  <input type="submit" value="Start upload" />
</form>

Bottle将上传的文件当做一个 FileUpload 实例存储在 BaseRequest.files 中,伴随着一些这次上传的元数据。我们假设你仅是想把上传的文件保存到磁盘中。

@route('/upload', method='POST')
def do_upload():
    category   = request.forms.get('category')
    upload     = request.files.get('upload')
    name, ext = os.path.splitext(upload.filename)
    if ext not in ('.png','.jpg','.jpeg'):
        return 'File extension not allowed.'

    save_path = get_save_path_for_category(category)
    upload.save(save_path) # appends upload.filename automatically
    return 'OK'

FileUpload.filename 包含客户端传上来的文件的文件名,但为了防止异常字符带来的bug,这里的文件名已经被处理过。如果你需要未经改动的文件名,看看 FileUpload.raw_filename

如果你想将文件保存到磁盘,强烈建议你使用 FileUpload.save 方法。它避免了一些常见的错误(例如,它不会覆盖已经存在的文件,除非你告诉它可以覆盖),并且更有效地使用内存。你可以通过 FileUpload.file 来直接访问文件对象,但是要谨慎。

JSON内容

一些JavaScript或支持REST的客户端会发送 application/json 内容给服务器。如果可用(合法的JSON), BaseRequest.json 会包含解析后的数据结构。

原始请求正文

你可以把 BaseRequest.body 当做一个file-like 对象来访问。根据内容的长度,以及 BaseRequest.MEMFILE_MAX 中的设置,它可以是一个 BytesIO 缓存或一个磁盘上的临时文件。无论如何,它都是被缓存的。如果你无需缓存,想直接访问文件流,可看看 request['wsgi.input']

WSGI环境

BaseRequest 实例包装WSGi环境字典。原件存储在 BaseRequest.environ 但请求对象本身的行为也类似于字典。大多数有趣的数据是通过特殊的方法或属性公开的,但是如果您想访问 WSGI environ variables 直接,你可以这样做:

@route('/my_ip')
def show_ip():
    ip = request.environ.get('REMOTE_ADDR')
    # or ip = request.get('REMOTE_ADDR')
    # or ip = request['REMOTE_ADDR']
    return template("Your IP is: {{ip}}", ip=ip)

模板

Bottle内置了一个快速的,强大的模板引擎,称为 SimpleTemplate 模板引擎 。可通过 template() 函数或 view() 修饰器来渲染一个模板。只需提供模板的名字和传递给模板的变量。下面是一个渲染模板的简单例子:

@route('/hello')
@route('/hello/<name>')
def hello(name='World'):
    return template('hello_template', name=name)

这会加载 hello_template.tpl 模板文件,并提供 name 变量。默认情况,Bottle会在 ./views/ 目录查找模板文件(译者注:或当前目录)。可在 bottle.TEMPLATE_PATH 这个列表中添加更多的模板路径。

view() 修饰器允许你在回调函数中返回一个字典,并将其传递给模板,和 template() 函数做同样的事情。

@route('/hello')
@route('/hello/<name>')
@view('hello_template')
def hello(name='World'):
    return dict(name=name)

语法

模板语法类似于Python的语法。它要确保语句块的正确缩进,所以你在写模板的时候无需担心会出现缩进问题。详细的语法描述可看 SimpleTemplate 模板引擎

简单的模板例子:

%if name == 'World':
    <h1>Hello {{name}}!</h1>
    <p>This is a test.</p>
%else:
    <h1>Hello {{name.title()}}!</h1>
    <p>How are you?</p>
%end

缓存

模板在经过编译后被缓存在内存里。你在修改模板文件后,要调用 bottle.TEMPLATES.clear() 函数清除缓存才能看到效果。在debug模式下,缓存被禁用了,无需手动清除缓存。

插件

0.9 新版功能.

Bottle的核心功能覆盖了常见的使用情况,但是作为一个迷你框架,它有它的局限性。所以我们引入了插件机制,插件可以给框架添加其缺少的功能,集成第三方的库,或是自动化一些重复性的工作。

我们有一个不断增长的 可用插件列表 插件列表,大多数插件都被设计为可插拔的。有很大可能,你的问题已经被解决,而且已经有现成的插件可以使用了。如果没有现成的插件, 插件开发指南 有介绍如何开发一个插件。

插件扮演着各种各样的角色。例如, SQLitePlugin 插件给每个route的回调函数都添加了一个 db 参数,在回调函数被调用的时候,会新建一个数据库连接。这样,使用数据库就非常简单了。

from bottle import route, install, template
from bottle_sqlite import SQLitePlugin

install(SQLitePlugin(dbfile='/tmp/test.db'))

@route('/show/<post_id:int>')
def show(db, post_id):
    c = db.execute('SELECT title, content FROM posts WHERE id = ?', (post_id,))
    row = c.fetchone()
    return template('show_post', title=row['title'], text=row['content'])

@route('/contact')
def contact_page():
    ''' This callback does not need a db connection. Because the 'db'
        keyword argument is missing, the sqlite plugin ignores this callback
        completely. '''
    return template('contact')

其它插件或许在线程安全的 local 对象里面发挥作用,改变 request 对象的细节,过滤回调函数返回的数据或完全绕开回调函数。举个例子,一个用于登录验证的插件会在调用原先的回调函数响应请求之前,验证用户的合法性,如果是非法访问,则返回登录页面而不是调用回调函数。具体的做法要看插件是如何实现的。

整个应用的范围内安装插件

可以在整个应用的范围内安装插件,也可以只是安装给某些route。大多数插件都可安全地安装给所有route,也足够智能,可忽略那些并不需要它们的route。

让我们拿 SQLitePlugin 插件举例,它只会影响到那些需要数据库连接的route,其它route都被忽略了。正因为如此,我们可以放心地在整个应用的范围内安装这个插件。

调用 install() 函数来安装一个插件:

from bottle_sqlite import SQLitePlugin
install(SQLitePlugin(dbfile='/tmp/test.db'))

插件没有马上应用到所有route上面,它被延迟执行来确保没有遗漏任何route。你可以先安装插件,再添加route。有时,插件的安装顺序很重要,如果另外一个插件需要连接数据库,那么你就需要先安装操作数据库的插件。

卸载插件

调用 uninstall() 函数来卸载已经安装的插件

sqlite_plugin = SQLitePlugin(dbfile='/tmp/test.db')
install(sqlite_plugin)

uninstall(sqlite_plugin) # uninstall a specific plugin
uninstall(SQLitePlugin)  # uninstall all plugins of that type
uninstall('sqlite')      # uninstall all plugins with that name
uninstall(True)          # uninstall all plugins at once

在任何时候,插件都可以被安装或卸载,即使是在服务器正在运行的时候。一些小技巧应用到了这个特征,例如在需要的时候安装一些供debug和性能测试的插件,但不可滥用这个特性。每一次安装或卸载插件的时候,route缓存都会被刷新,所有插件被重新加载。

注解

模块层面的 install()unistall() 函数会影响 默认应用 。针对应用来管理插件,可使用 Bottle 应用对象的相应方法。

安装给特定的route

route() 修饰器的 apply 参数可以给指定的route安装插件

sqlite_plugin = SQLitePlugin(dbfile='/tmp/test.db')

@route('/create', apply=[sqlite_plugin])
def create(db):
    db.execute('INSERT INTO ...')

插件黑名单

如果你想显式地在一些route上面禁用某些插件,可使用 route() 修饰器的 skip 参数:

sqlite_plugin = SQLitePlugin(dbfile='/tmp/test1.db')
install(sqlite_plugin)

dbfile1 = '/tmp/test1.db'
dbfile2 = '/tmp/test2.db'

@route('/open/<db>', skip=[sqlite_plugin])
def open_db(db):
    # The 'db' keyword argument is not touched by the plugin this time.

    # The plugin handle can be used for runtime configuration, too.
    if db == 'test1':
        sqlite_plugin.dbfile = dbfile1
    elif db == 'test2':
        sqlite_plugin.dbfile = dbfile2
    else:
        abort(404, "No such database.")

    return "Database File switched to: " + sqlite_plugin.dbfile

skip 参数接受单一的值或是一个list。你可使用插件的名字,类,实例来指定你想要禁用的插件。如果 skip 的值为True,则禁用所有插件。

插件和子应用

大多数插件只会影响到安装了它们的应用。因此,它们不应该影响通过 Bottle.mount() 方法挂载上来的子应用。这里有一个例子。

root = Bottle()
root.mount('/blog', apps.blog)

@root.route('/contact', template='contact')
def contact():
    return {'email': 'contact@example.com'}

root.install(plugins.WTForms())

在你挂载一个应用的时候,Bottle在主应用上面创建一个代理route,将所有请求转接给子应用。在代理route上,默认禁用了插件。如上所示,我们的 WTForms 插件影响了 /contact route,但不会影响挂载在root上面的 /blog

这个是一个合理的行为,但可被改写。下面的例子,在指定的代理route上面应用了插件。

root.mount('/blog', apps.blog, skip=None)

这里存在一个小难题: 插件会整个子应用当作一个route看待,即是上面提及的代理route。如果想在子应用的每个route上面应用插件,你必须显式地在子应用上面安装插件。

开发

所以,您已经学习了基本知识,并且想编写自己的应用程序?以下是一些可以帮助您提高工作效率的提示。

默认应用

Bottle维护一个全局的 Bottle 实例的栈,模块层面的函数和修饰器使用栈顶实例作为默认应用。例如 route() 修饰器,相当于在默认应用上面调用了 Bottle.route() 方法。

@route('/')
def hello():
    return 'Hello World'

run()

这对于小型应用程序非常方便,并为您节省了一些输入,但也意味着,一旦导入模块,就会将路由安装到全局默认应用程序。为了避免这种导入副作用,Bottle提供了第二种更明确的方法来构建应用程序:

app = Bottle()

@app.route('/')
def hello():
    return 'Hello World'

app.run()

分离应用对象,大大提高了可重用性。其他开发者可安全地从你的应用中导入 app 对象,然后通过 Bottle.mount() 方法来合并到其它应用中。

0.13 新版功能.

从 Bottle-0.13 开始,您可以使用 Bottle 作为上下文管理器的实例:

app = Bottle()

with app:

    # Our application object is now the default
    # for all shortcut functions and decorators

    assert my_app is default_app()

    @route('/')
    def hello():
        return 'Hello World'

    # Also useful to capture routes defined in other modules
    import some_package.more_routes

调试模式

在开发的早期阶段,调试模式非常有用。

bottle.debug(True)

在调试模式下,当错误发生的时候,Bottle会提供更多的调试信息。同时禁用一些可能妨碍你的优化措施,检查你的错误设置。

下面是调试模式下会发生改变的东西,但这份列表不完整:

  • 默认的错误页面会打印出运行栈。

  • 模板不会被缓存。

  • 插件马上生效。

请确保不要在生产环境中使用调试模式。

自动加载

在开发的时候,你需要不断地重启服务器来验证你最新的改动。自动加载功能可以替你做这件事情。在你编辑完一个模块文件后,它会自动重启服务器进程,加载最新版本的代码。

from bottle import run
run(reloader=True)

它的工作原理,主进程不会启动服务器,它使用相同的命令行参数,创建一个子进程来启动服务器。请注意,所有模块级别的代码都被执行了至少两次。

子进程中 os.environ['BOOTLE_CHILD'] 变量的值被设为 True ,它运行一个不会自动加载的服务器。在代码改变后,主进程会终止掉子进程,并创建一个新的子进程。更改模板文件不会触发自动重载,请使用debug模式来禁用模板缓存。

自动加载需要终止子进程。如果你运行在Windows等不支持 signal.SIGINT (会在Python中raise KeyboardInterrupt 异常)的系统上,会使用 signal.SIGTERM 来杀掉子进程。在子进程被 SIGTERM 杀掉的时候,exit handlers和finally等语句不会被执行。

命令行接口

从0.10版本开始,你可像一个命令行工具那样使用Bottle:

$ python -m bottle

Usage: bottle.py [options] package.module:app

Options:
  -h, --help            show this help message and exit
  --version             show version number.
  -b ADDRESS, --bind=ADDRESS
                        bind socket to ADDRESS.
  -s SERVER, --server=SERVER
                        use SERVER as backend.
  -p PLUGIN, --plugin=PLUGIN
                        install additional plugin/s.
  -c FILE, --conf=FILE  load config values from FILE.
  -C NAME=VALUE, --param=NAME=VALUE
                        override config values.
  --debug               start server in debug mode.
  --reload              auto-reload on file changes.

ADDRESS 参数接受一个IP地址或IP:端口,其默认为 localhost:8080 。其它参数都很好地自我解释了。

插件和应用都通过一个导入表达式来指定。包含了导入的路径(例如: package.module )和模块命名空间内的一个表达式,两者用":"分开。下面是一个简单例子,详见 load()

# Grab the 'app' object from the 'myapp.controller' module and
# start a paste server on port 80 on all interfaces.
python -m bottle -server paste -bind 0.0.0.0:80 myapp.controller:app

# Start a self-reloading development server and serve the global
# default application. The routes are defined in 'test.py'
python -m bottle --debug --reload test

# Install a custom debug plugin with some parameters
python -m bottle --debug --reload --plugin 'utils:DebugPlugin(exc=True)'' test

# Serve an application that is created with 'myapp.controller.make_app()'
# on demand.
python -m bottle 'myapp.controller:make_app()''

部署

Bottle 是内置的 wsgiref WSGIServer 默认情况下。这种非线程HTTP服务器对于开发来说非常好,但是当服务器负载增加时,它可能会成为性能瓶颈。

最早的解决办法是让Bottle使用 pastecherrypy 等多线程的服务器。

bottle.run(server='paste')

部署 章节中,会介绍更多部署的选择。

词汇表

回调

当一些外部动作发生时要调用的程序员代码。在Web框架的上下文中,通常通过为每个URL指定回调函数来实现URL路径和应用程序代码之间的映射。

装饰者

返回另一个函数的函数,通常作为函数转换使用 @decorator 语法。参见 python documentation for function definition 关于装饰工的更多信息。

环境

保存根目录下所有文档信息并用于交叉引用的结构。环境在解析阶段之后被pickle,因此连续运行只需要读取和解析新的和已更改的文档。

处理函数

处理某些特定事件或情况的函数。在Web框架中,通过附加一个处理程序函数作为组成应用程序的每个特定URL的回调来开发应用程序。

源目录

包含其子目录的目录,其中包含一个sphinx项目的所有源文件。