bottle中文教程


本教程介绍了Bottle框架的概念和特性,涵盖了基本使用和进阶主题。您可以从头读到尾,或者以后作为使用参考。您或许对动态生成的 API Reference 也感兴趣,它包含了更多的细节,但是并没有太多的解释。在 Recipes 收集或者 Frequently Asked Questions 页面,可以找到更多的常见问题的解决方案. 如果您还需要其他帮助,可以加入 mailing list 或者通过 IRC channel来联系.

约定: route routes 翻译为 路由

安装

Bottle并不依赖其他库,您只需要下载 bottle.py 到项目目录中即可开始编码:

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

这条命令将从开发版本中获取最新代码,它包含了所有的最新特性。如果您希望获取更稳定的环境,您可以下载发行的稳定版本。稳定版本在 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, ...

无论哪种方式, 您都需要安装好 2.5以上 (包含3.x)来运行 bottle 应用. 如果您没有权限在整个系统中安装这些包或者根本不想这么做,那可以先创建 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 或者已经将它复制到您的项目目录,我们从最简单的 “Hello World” 开始:

from bottle import route, run

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

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

代码很简单,运行这个脚本,访问 http://localhost:8080/hello 您可以在浏览器中看到 “Hello World!” 。 它的运行原理如下:

 route() 装饰器为URL地址绑定了一段代码,在这个例子中,我们关联了/hello 地址到 hello() 函数上。 这个叫route的(装饰器的名字) 是Bottle框架的最重要的概念。您可以定义任意多个 route。当浏览器请求一个URL地址,它所关联的函数就会被调用并返回相应的值给浏览器,原理比较简单。

最后一行调用 run() 方法启动一个内置的开发环境用的服务器,运行在 localhost 8080 端口来为请求提供服务,可以按 Control-c来关闭它。以后你切换使用其他服务器,不过目前使用这个内置服务器就够用了,它不需要任何配置,并且以难以置信的无侵害方式使您的应用运行并且用于本地测试。

早期的开发中Debug Mode 很重要,但是要记得,生产环境应该关掉Debug模式.

上面的例子很简单,但是它展示了Bottle构建应用的基本概念。下面将更详细的讲解更多的内容。

默认应用

为了简单起见,本教程中的例子都使用module-level route()装饰器来定义route. 它会将这些routes添加到一个全局的 “默认应用”,它是一个Bottle 的实例,当第一次调用 route()时候会自动创建。有很多其他的 module-level 装饰器和函数与默认应用相关,但是如果您喜欢更面向对象的方法和不介意额外的输入,您可以创建一个单独的应用程序对象,而不是使用全局的默认应用:

from bottle import Bottle, run

app = Bottle()

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

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

面向对象的方法在Default Application 章节中有进一步的描述,现在只需要记得还有这样一个选择。

请求路由

上一章中我们创建了一个非常简单的web应用,它仅包含了一个路由,下面是 “Hello World”例子中的route部分:

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

route() 装饰器关联了一个URL 地址到一个回调函数,并添加了一个新的 route 到默认应用( 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添加通配符,并通过关键字参数和访问它们。

动态路由

包含通配符的Route称为动态路由 (对比于静态路由) ,可以同时匹配不止一个 URL地址。一个简单的通配符由一个名称和一对中括号组成  (例如 <name>) ,接受下一个斜杠(/)前一个多多个字母。例如  路由/hello/<name> 接受请求/hello/alice和 /hello/bob,但不接受 /hello/hello/ 或者/hello/mr/smith.

每一个通配符将覆盖URL的一部分作为一个关键字参数,用于请求的回调函数。您可以使用它们轻松地实现基于RESTful的、代码整洁友好的、有意义的url。这里有一些其他的例子,展示他们匹配的url:

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

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

 0.10 版本后的新特性.

使用过滤器是用来定义更具体的通配符,并且/或者 在传递给回调函数之前对参数进行转换。一个过滤通配符声明为<name:filter> or<name:filter:config>。可选配置部分的语法取决于所使用的过滤器。  

以下是默认实现的过滤器,以后可能会增加更多:

  • :int matches (signed) digits only and converts the value to integer.
  • :float similar to :int but for decimal numbers.
  • :path matches all characters including the slash character in a non-greedy way and can be used to match more than one path segment.
  • :re allows you to specify a custom regular expression in the config field. The matched value is not modified.

让我们来看看一些实际的例子:

@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, ...)

您可以实现定制的过滤器,细节请参考 Request Routing .

 0.10版本更新.

Bottle 0.10中引入的新规则语法,用来简化一些常见的用例,但是老语法仍然有效,你可以找到很多示例代码还使用它。下面例子描述了他们的差异:

Old Syntax New Syntax
:name <name>
:name#regexp# <name:re:regexp>
:#regexp# <:re:regexp>
:## <:re>

在未来的项目中尽量避免使用旧语法,它目前还能使用,但最终会被弃用。

HTTP请求方法

HTTP协议定义了多种请求方法(request methods ,有时被称作“verbs”) 来实现不同的任务。当没有指定方法时,所有的routes路由都默认使用GET方法,这些路由只能匹配GET请求。如果要处理其他的请求如 POST, PUT, DELETE 或者 PATCH, 给route() 增加一个 method 关键字参数,或者使用四个替代装饰器: 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 URL关联了两个截然不同的回调函数,一个用于GET请求,另一个用于POST请求。第一个请求为用户展示了一个HTML表单,当表单提交的时候调用第二个请求,用于验证用户所输入的登录凭证。  Request.forms 的详细使用方法可参考 Request Data 章节。

特殊方法: HEAD 和 ANY

HEAD方法用来发出和GET方法一样的请求,但是它不需要响应的BODY部分,只请求头部信息,当需要资源的元信息(meta-information)时候就很有用,它不需要下载全部的文档信息。 Bottle通过自动找到对应的GET路由(如果存在的话)并切掉请求体部分来处理这类请求,您不需要手动指定任何HEAD路由。

此外,非标准的ANY方法是一个低优先级的回调(fallback):ANY路由监听到任何没有明确定义的路由,这有助于proxy-routes将请求重定向到更具体的子应用。

概括来讲: HEAD请求会退回到GET 路由,如果没有匹配原始路由的请求方法时所有请求会退回到ANY 路由,就是这么简单。

路由静态文件

Bottle并不为CSS,图片等自动提供服务,需要添加路由和回调函数来控制哪些文件需要提供服务以及他们的位置:

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

static_file() 函数是一个辅助函数,用来以一种安全、便捷的方式为静态文件提供服务(参考Static Files). 这个例子中对于直接访问/path/to/your/static/files 目录下的文件是有限制的,因为 <filename> 通配符不能匹配一个包含斜杠的路径,如果要支持对子文件夹内容提供服务,则需要使用另一个通配符 pathfilter:

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

在指定相对路径时候例如 root='./static/files'时候要格外小心,工作目录 (./) 和项目的目录不一定是同一个。

错误(ERROR)页面

如果出现错误,Bottle 将展示一个包含信息但比较普通的错误页面,您可以根据指定的HTTP状态码来使用 error() 装饰器来替换默认的错误页面:

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

设置这个之后,404文件找不到的错误将展示一个定制的的错误页面给用户,传给error-handler 的唯一参数是一个 HTTPError的实例。除此之外,一个error-handler与常规请求回调并没有太多区别,您可以从 request中读取信息,可以写入response 并返回任意支持的数据类型( data-type)除了 HTTPError 实例。

错误处理仅仅在应用程序返回或者抛出一个HTTPError 异常的时候才会使用,(abort() does just that). 改变Request.status 或者返回HTTPResponse 并不会触发错误处理(error handler).

生成内容

在单纯的 WSGI 环境中,您的应用可以返回的类型是很有限的,应用必须返回一个可迭代的生成字节字符串(iterable yielding byte strings),您可以返回一个字符串 (因为字符串是可迭代的) ,但是这会造成许多服务器必须一个字符一个字符的去传输您的内容。不允许返回Unicode字符串,因此这种方式很不实用。

Bottle 则更加灵活,支持多种返回类型。如果可能的话它甚至增加了一个 Content-Length 头信息并对unicode 自动编码,这样就不需要手动去完成。下面列出了您的应用程序可能返回的数据类型以及一个简短的介绍来描述Bottle框架是如何对这种类型进行处理的:

Dictionaries
As mentioned above, Python dictionaries (or subclasses thereof) are automatically transformed into JSON strings and returned to the browser with the  Content-Typeheader set to  application/json. This makes it easy to implement json-based APIs. Data formats other than json are supported too. See the  tutorial-output-filter to learn more.
Empty Strings,  FalseNone or other non-true values:
These produce an empty output with the  Content-Length header set to 0.
Unicode strings
Unicode strings (or iterables yielding unicode strings) are automatically encoded with the codec specified in the  Content-Type header (utf8 by default) and then treated as normal byte strings (see below).
Byte strings
Bottle returns strings as a whole (instead of iterating over each char) and adds a Content-Length header based on the string length. Lists of byte strings are joined first. Other iterables yielding byte strings are not joined because they may grow too big to fit into memory. The  Content-Length header is not set in this case.
Instances of  HTTPError or  HTTPResponse
Returning these has the same effect as when raising them as an exception. In case of an  HTTPError, the error handler is applied. See  Error Pages for details.
File objects
Everything that has a  .read() method is treated as a file or file-like object and passed to the  wsgi.file_wrapper callable defined by the WSGI server framework. Some WSGI server implementations can make use of optimized system calls (sendfile) to transmit files more efficiently. In other cases this just iterates over chunks that fit into memory. Optional headers such as  Content-Length or  Content-Type are  not set automatically. Use  send_file() if possible. See  Static Files for details.
Iterables and generators
You are allowed to use  yield within your callbacks or return an iterable, as long as the iterable yields byte strings, unicode strings,  HTTPError or  HTTPResponse instances. Nested iterables are not supported, sorry. Please note that the HTTP status code and the headers are sent to the browser as soon as the iterable yields its first non-empty value. Changing these later has no effect.

The ordering of this list is significant. You may for example return a subclass of str with aread() method. It is still treated as a string instead of a file, because strings are handled first.

更改默认编码

Bottle 使用Content-Type 头信息的字符集参数来决定如何对  unicode 字符串进行编码。这个头信息默认是 text/html; charset=UTF8 ,可以通过改变 Response.content_type 属性或者直接设置 Response.charset 属性来更改编码。(Response 对象详细信息可以参考 The Response Object.)

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(which is sent to the client unchanged) 然后设置Response.charset属性 (which is used to encode unicode).

STATIC FILES静态文件

您可以直接返回文件对象,但是推荐使用static_file() 的方式来对静态文件提供服务。它会自动判断给出一个 mime-type, 增加一个 Last-Modified 头, 限制路径为设置的 root 目录提升安全性并且生成适当的错误响应(403 on permission errors, 404 on missing files). 它甚至支持 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() 返回值作为一个异常。

强制下载(Forced Download)

很多浏览器会试图打开一个下载文件如果它的 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 信息的303 See Other 响应来设置跳转到新的 URL. redirect() 为您实现这个功能:

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

您可以提供一个不同的HTTP 状态码作为第二个参数。

Note

Both functions will interrupt your callback code by raising an HTTPError exception.

其他异常

除了 HTTPResponse 或者HTTPError 以外的异常将返回一个 500 Internal ServerError 响应,所以它们不会造成WSGI服务崩溃。如果需要您可以通过设置中间件的 bottle.app().catchall 为False关闭这个处理异常的行为。

RESPONSE 对象

响应的元数据例如HTTP状态码、响应头、和cookies信息都存在一个叫  response 的对象中,通过这个对象传输给浏览器,您可以直接操纵这些元数据或者使用预先定义好的辅助方法。 You can manipulate these metadata directly or use the predefined helper methods to do so. 完整的API和功能列表参看(参考Response),,但最常见的用例和功能都囊括于此,。

状态码

HTTP status code 控制着浏览器的行为,默认是 200 OK. 大多数情况下,您不需要手动设置 Response.status 属性, 而是使用abort() 辅助类或者返回一个适当状态码的 HTTPResponse 实例。 状态码可以使用任意整数,但是使用 HTTP specification 规范定义以外的数字只会使浏览器混淆并且破坏标准。

Response Header

响应头例如 Cache-Control 或者Location 通过Response.set_header()来定义。 这个方法有两个参数,头信息的名称和值,名称部分不区分大小写:

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

大多数头信息是唯一的,意外着每个名字的头信息只会发送给客户端一个。有一些特殊的头信息是允许在一个响应中出现多次的,如果要对这个名字的头信息增加一个额外的值,使用 Response.add_header() 来代替 Response.set_header():

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

请注意,这只是一个例子。如果您想要使用 cookies, 继续阅读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: Maximum age in seconds. (default: None)
  • expires: A datetime object or UNIX timestamp. (default: None)
  • domain: The domain that is allowed to read the cookie. (default: current domain)
  • path: Limit the cookie to a given path (default: /)
  • secure: Limit the cookie to HTTPS connections (default: off).
  • httponly: Prevent client-side javascript to read this cookie (default: off, requires Python 2.6 or newer).

如果既不设置 expires 又不设置max_age ,cookie 将在浏览器回话结束或者关闭浏览器窗口时候失效。使用cookie的时候还有一些陷阱需要考虑:

  • 大多数浏览器限制Cookie 文本最大为 4 KB 
  • 一些用户设置浏览器禁用cookie,很多搜素引擎也忽略cookie,需要保证您的应用在不使用cookie时也能正常使用。
  • Cookies保存在客户端,而且不会以任何方式加密,不管您在cookie中保存了什么,用户都能读取它,更糟糕的是,攻击者可以通过XSS漏洞在您这端窃取用户的cookie。 已知一些病毒可以读取浏览器cookie,因此,千万不要在cookie中存需要的保密信息。 
  • Cookies 会被恶意的客户端伪造,因此不能相信它们。

签名Cookies

上面提到,cookie很容易被恶意的客户端篡改,Bottle 提供了加密签名机制来阻止这种操纵。当您读取或者设置一个cookie时候,您只需要通过secret 关键字提供一个用于签名的key,并确保这个key是保密的即可。其结果是,如果cookie没有签名或者签名的key不匹配, 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 可以自动pickles 和unpickles存储的签名 cookies. 这将允许您存储任何 pickle-able 对象(不仅仅是strings) 到cookie中, 只要数据不超过4KB的限制。

Warning

签名 cookies 并没有加密,(客户端仍然可以看到其内容),而且没有复制保护(客户端可以恢复一个旧的cookie)。签名的主要用途是使pickling 和unpickling 更加安全,防止被操纵。仍然不要在客户端保存保密信息。

REQUEST 数据

Cookies, HTTP header, HTML <form> 字段和其他请求数据都可以通过全局的 global request 对象来取得。这个特殊的对象总是指向当前请求,即便是在多线程环境中,在同时处理多个客户端连接的时候也仍是如此:

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 使用一个特殊的字典类型来保存form数据和cookie. 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多值: FormsDict 是 MultiDict 的子类,可以对每个key存储多个值。标准的字典读取方法只能返回一个值,但是 getall() 方法可以对指定的key返回一个值的list(可能为空):

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

WTForms 支持:  一些类库(e.g. WTForms) 需要all-unicode 字典作为输入.FormsDict.decode() 可以实现。 它解码所有的值并返回自身的一个副本,同时维护每个键多个值的对应和所有的其他功能。

Note

Python 2 中所有的键值对都是 byte-strings.如果您需要 unicode, 可以调用FormsDict.getunicode() 或者通过属性访问获取值。这两个方法都会解码字符串(default: utf8),并在失败的时候返回空字符串,而不需要捕获异常UnicodeError:

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

Python 3 中所有的字符串都是 unicode, 但是HTTP 是 byte-based wire 协议的. 服务器需要先解码 byte 字符串,然后将它们传给应用。 安全起见, WSGI 建议使用ISO-8859-1 (aka 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

Cookies 是保存在客户端浏览器里的小段文本,每次请求都会把他们返回到服务端。它们可以用来保存多个请求的一些状态信息(HTTP 本身是无状态的),但是不能用来保存安全相关的东西。它们很容易被客户端篡改。

所有客户端发送回来的cookie 可以通过BaseRequest.cookies (一个 FormsDict)来获取到。下面例子展示一个简单的基于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 HEADERS

所有的客户端发送的HTTP headers (例如 RefererAgent or Accept-Language) 都保存在一个 WSGIHeaderDict 中,可以通过 BaseRequest.headers 属性来获取。WSGIHeaderDict 是基于字典的,并且它的键不区分大小写:

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 方法(GET 或 POST). 使用method="get" 表单数据将被添加到URL上,可以通过上面讲到的 BaseRequest.query 来访问。 这种方式是不安全的,而且有很多其他限制,所以这里我们使用 method="post" 方法,如果存在疑问,请使用 POST方法提交表单。

通过 POST 传送的表单数据存储在 BaseRequest.forms 中,它是一个 FormsDict. 服务端的代码可能如下:

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>"

还有许多属性可以用来获取表单数据,其中一些能够从不同的源来组合值,使获取表单数据更加便捷。下表给出了一个像样的概述:

Attribute GET Form fields POST Form fields File Uploads
BaseRequest.query yes no no
BaseRequest.forms no yes no
BaseRequest.files no no yes
BaseRequest.params yes yes no
BaseRequest.GET yes no no
BaseRequest.POST no yes yes

文件上传

若要支持文件上传,我们需要对 <form> 标签进行一点儿改变。首先,添加一个给 <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将上传的文件存储在 BaseRequest.files 中,它是一个FileUpload 实例,还包含一些上传文件的元数据。假定您希望把上传的文件保存到磁盘:

@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 包含了上传的文件在客户端文件系统里的名称,但是已经被cleaned up 并且规范化了以防止文件名中的一些不支持的字符或者路径段造成bug,如果您需要客户端发送的未被修改的文件名,可以看一下 FileUpload.raw_filename.

如果您想要将上传文件存在磁盘,那么强烈推荐使用 FileUpload.save 方法。它能够避免一些常见错误 (例如它不会重写已存在的文件,除非你告诉它这么做) 并且能够以内存级效率的方式存储文件。您可以直接通过FileUpload.file来访问文件对象,小心使用。

JSON 内容

一些JavaScript 或者REST 客户端发送application/json 格式的内容到服务端,BaseRequest.json 属性包含了解析过的数据结构,如果存在的话。

THE RAW REQUEST BODY

可以通过BaseRequest.body.像读取文件对象一样获取raw body 数据, 它是一个 BytesIO buffer 或者一个临时文件,这取决于内容的长度和 BaseRequest.MEMFILE_MAX 的设置。这两种情况下,再您读取属性前,body 数据都会被完整的缓存。如果您期望得到大量的数据并且想直接读取未缓冲的流数据,可以参看request['wsgi.input'].

WSGI 环境

每个BaseRequest 实例包装了一个 WSGI 环境字典,原始数据保存在 BaseRequest.environ中,但 request 对象本身就也像一个字典,需要您感兴趣的数据都已经通过特殊的方法或者属性暴露出来,但如果您希望直接访问 WSGI 环境变量数据,您可以这样:

@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)

模板 (TEMPLATES)

Bottle有一个快速和强大的内置模板引擎,称作 SimpleTemplate Engine. 想要渲染一个模板您可以使用 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 Engine

下面是一个模板的例子:

%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 模式中禁用缓存.

插件

New in version 0.9.

Bottle的核心功能覆盖了大多数常见用户使用用例,但是作为一个微型框架它是有局限的,这时“插件”就发挥作用了。插件增加了框架的缺失功能,集成了第三方类库或者只是自动化一些重复工作。

我们拥有一个增长的 List of available Plugins 而且许多插件被设计为便捷的和可重用的应用程序。您所遇到的问题很可以已经被解决了,而且存在一个随时可用的插件。如果没有,那么 Plugin Development Guide 可能对您有帮助。

插件和api的效果是多种多样的,这取决于特定的插件。例子中的SQLitePlugin 插件能到探测到回调函数,需要一个 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对象的详细内容, 过滤回调函数返回的数据或者完全绕过回调函数。一个名为 “auth” 的插件可以检测合法的session并返回一个登录页面来代替调用原始的回调函数。能实现什么功能完全取决于插件本身。

应用全局安装插件

插件可以全应用范围安装也可以仅仅指定一些路由来添加功能。大多数的插件可以安全地安装到所有路由上并且足够灵巧不增加那些不需要这个插件的回调的开销。

我们以 SQLitePlugin 插件为例,它仅仅影响需要数据库连接的路由回调,其他路由则不受影响。因此,我们可以在应用全局安装此插件而不会增加开销。

要安装插件,只需要调用 install() ,并把插件名作为第一个参数即可:

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

这个插件还没有应用于路由回调,它会延迟以确保没有路由被遗漏。您可以先安装插件然后增加路由,安装插件的顺序很重要,如果某个插件需要一个数据库连接,那么您应该先安装数据库的插件。

卸载插件

您可以使用名字、类、或者实例来卸载一个之前已经安装的插件:

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

插件可以在任意时间安装或者卸载,甚至在服务请求运行时也可以。这里有一些巧妙的技巧,  (installing slow debugging or profiling plugins only when needed),但是不要过度使用。每次插件列表有修改,路由缓存就会被清空而且所有插件会被重新应用。

Note

The module-level install() and uninstall() functions affect the Default Application. To manage plugins for a specific application, use the corresponding methods on theBottle application object.

指定路由安装插件

如果您只为少量的路由安装插件,可以使用route() 装饰器的 apply 参数来完成:

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

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

插件黑名单

如果您想为一些路由禁用指定的插件, 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 参数接受一个单一的值或者一个值的列表。您可以使用名称、类、或者实例的名字来标识希望跳过的插件,设置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 都会在主应用上创建一个代理路由来转发请求到子应用,默认情况下,插件对于子应用是不生效的。因此,例子中的WTForms (虚构的)插件会影响 /contact 路由,但是并不会影响 /blog 子应用的路由。

这种行为的目的是作为一个健全的默认值( sane default),但是可以被覆盖。下面例子为指定的代理路由重新激活了所有插件:

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

不过存在一个障碍:插件把整个子应用看作一个单独的路由,即上述的proxy-route。如果想要影响到子应用的每一个路由,您必须显式地将插件安装到所加载的子应用。But there is a snag: The plugin sees the whole sub-application as a single route, namely the proxy-route mentioned above. In order to affect each individual route of the sub-application, you have to install the plugin to the mounted application explicitly.

开发

您已经学习了基本用法,是否想编写一个您自己的应用?这里有一些提示可能使您更有效率。

默认应用

Bottle 维护一个全局的Bottle 实例栈,而且使用栈顶的那个作为默认实例用于module-level的函数和装饰器。例如, route() 装饰器在默认应用中就是调用 Bottle.route() 的一个快捷方式:

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

run()

这对于小规模的应用是很方便的,而且可以减省您的输入,但也意味着一旦您的module 被导入,路由就会被安装到全局的默认应用里。为了避免这种导入方式的副作用, Bottle 提供了另一种,更加清晰的方式来构建应用:

app = Bottle()

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

app.run()

分离应用对象也很有利于代码重用,其他开发者可以安全地从您的模块中导入这个 app 对象并使用 Bottle.mount() 来与他的应用合并在一起。

New in version 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

DEBUG 模式

在初期的开发中,调试模式是很有用的:

bottle.debug(True)

这种模式下,Bottle是比较啰嗦的,但当发生错误时候能提供非常有用的调试信息。它也禁用一些优化,可能会妨碍您,还会增加一些检查,提醒你关于可能的错误配置。

这里是一个并不完整的列表,展示调试模式下的改变:

  • 默认的错误页面会显示一个 traceback.
  • 模板不被缓存
  • 插件会及时的应用

确保不要在生产环境的服务器端使用调试模式。

自动重新加载

在开发过程中,您经常需要重启服务来测试刚修改的代码。自动重新加载机制可以为您做这件事情,每次您编辑一个模块文件,重新加载机制会重启服务进程并加载最新版本的代码。

from bottle import run
run(reloader=True)

它的运行机制: 主进程并不启动服务器,而是使用启动主进程相同的命令行参数产生一个新的子进程。所有的模块级别代码会被执行至少两次,要小心。

子进程会设置 os.environ['BOTTLE_CHILD'] 为 True 并启动一个常规的非重新加载的app server。一旦某些模块发生变化,子进程就会被主进程终止和重新生成。模板的代码修改并不会触发重新加载,请使用 debug 模式来关闭模板缓存。

重新加载机制依赖于终止子进程的能力。如果您的系统运行在 Windows 或者其他一些不支持 signal.SIGINT (Python中可以触发KeyboardInterrupt )的操作系统上,则使用 signal.SIGTERM 来终止子进程。注意,退出处理程序和最终子句( exit handlers and finally clauses)等并不是在 SIGTERM后执行。

命令行接口

从版本 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.
  --debug               start server in debug mode.
  --reload              auto-reload on file changes.

ADDRESS 字段使用一个IP地址或者 IP:PORT 对,默认是localhost:8080. 其他的参数看名字便知其含义。

插件和应用都通过导入表达式来指定。 这些包括一个导入路径(例如:package.module)和一个该模块命名空间中要被评估(evaluated )的表达式,中间以冒号分割。详细可参考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 server 在开发过程和初期的生成环境下表现的优秀,但是当服务负载增加的时候可能会成为性能瓶颈。

最简单的增加性能的办法是安装一个多线程的服务器库,例如paste 或者cherrypy ,并告诉 Bottle 使用它来替代单线程的服务器:

bottle.run(server='paste')

这一点,和其他很多部署优化细节参看单独章节: Deployment

术语表

callback
Programmer code that is to be called when some external action happens. In the context of web frameworks, the mapping between URL paths and application code is often achieved by specifying a callback function for each URL.
decorator
A function returning another function, usually applied as a function transformation using the  @decorator syntax. See  python documentation for function definition for more about decorators.
environ
A structure where information about all documents under the root is saved, and used for cross-referencing. The environment is pickled after the parsing stage, so that successive runs only need to read and parse new and changed documents.
handler function
A function to handle some specific event or situation. In a web framework, the application is developed by attaching a handler function as callback for each specific URL comprising the application.
source directory
The directory which, including its subdirectories, contains all source files for one Sphinx project.
-_-基于0.13版本翻译,主要是为了仔细看一遍了解细节,顺便分享给大家,转载请提供出处。水平有限,错误之处欢迎批评指正!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值