用户指南

本指南介绍 Bottle Web 框架的概念和功能,涵盖基础和高级主题。你可以从头到尾阅读,也可以稍后将其用作参考。自动生成的 API 参考 可能也对你有所帮助。它涵盖更多细节,但解释不如本教程详细。常见问题的解答可在我们的 常见问题解答 (F.A.Q.) 集合或 常见问题解答 (F.A.Q.) 页面找到。如果你需要任何帮助,请加入我们的邮件列表或访问我们的IRC 频道

安装

Bottle 不依赖任何外部库。你可以直接将 bottle.py 下载到你的项目目录中并开始编码

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

这将获取包含所有新功能的最新开发快照。如果你更喜欢稳定的环境,应使用稳定版本。这些版本可在 PyPI 上获取,并且可以通过 pip(推荐)或你的包管理器安装

$ pip install --user bottle            # better use a venv, see below
$ sudo apt-get install python-bottle   # works for debian, ubuntu, ...

通常更好的做法是为每个项目创建一个 virtualenv 并使用它来安装包

$ python3 -m venv venv         # Create virtual environment
$ source venv/bin/activate     # Change default python to virtual one
(venv)$ 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!"

if __name__ == '__main__':
    run(host='localhost', port=8080, debug=True)

就是这样。运行此脚本,访问 http://localhost:8080/hello,你将在浏览器中看到“Hello World!”。下面是它的工作原理

route() 装饰器将一段代码绑定到 URL 路径。在本例中,我们将 /hello 路径链接到 hello() 函数。这被称为路由(因此得名装饰器),是本框架最重要的概念。你可以定义任意数量的路由。每当浏览器请求一个 URL 时,相应的函数就会被调用,并且返回值会被发送回浏览器。就是这么简单。

最后一行中的 run() 调用会启动一个内置的开发服务器。它运行在 localhost8080 端口,并处理请求直到你按下 Control-c。你稍后可以切换服务器后端(参见 部署),但目前我们只需要一个开发服务器。它完全不需要设置,是一种非常轻松的方式,让你的应用程序可以启动并运行进行本地测试。

调试模式在早期开发阶段非常有用,但在面向公众的应用程序中应关闭。请记住这一点。

这只是演示了使用 Bottle 构建应用程序的基本概念。继续阅读,你将看到更多可能性。

应用对象

为了简化,本教程中的大多数示例使用模块级别的 route() 和其他装饰器来定义路由。这些引用一个全局的“默认应用”,它是 Bottle 的一个实例,在你第一次调用 route() 或其相关函数时自动创建。如果你更喜欢更明确的方法并且不介意额外输入一些代码,你可以创建一个单独的应用对象并使用它,而不是全局的应用对象

from bottle import Bottle, run

app = Bottle()

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

if __name__ == '__main__':
    app.run(host='localhost', port=8080)

面向对象的方法在 应用结构 一节中有进一步描述。只需记住你有一个选择。

调试模式

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

# Enable debug at runtime
bottle.debug(True)
# or during startup
run(..., debug=True)

在此模式下,Bottle 会输出更多信息,并在发生错误时提供有用的调试信息。它还会禁用一些可能妨碍你的优化,并添加一些检查来警告你可能的配置错误。以下是调试模式下变化的一些事项的不完整列表

  • 默认错误页面显示堆栈跟踪。

  • 模板不会被缓存。

  • 插件立即生效。

请确保不在生产服务器上使用调试模式。

自动重载

厌倦了每次更改代码后都重新启动服务器吗?自动重载器可以为你完成这项工作。每次你编辑模块文件时,重载器都会重新启动服务器进程并加载最新版本的代码。

run(..., debug=True, reloader=True)

工作原理:主进程不会启动服务器,而是使用与启动主进程相同的命令行参数生成一个新的子进程。所有模块级代码至少执行两次!请务必小心。

子进程的 os.environ['BOTTLE_CHILD'] 将被设置为 True,并作为一个正常的非重载应用服务器启动。一旦任何已加载的模块发生变化,子进程就会终止,并由主进程重新生成。模板文件的更改不会触发重载。请使用调试模式来禁用模板缓存。

命令行接口

Bottle 不仅是一个模块,也是一个命令行可执行文件,可以用来启动你的应用,而不是通过编程方式调用 run()。如果你通过 pip 或类似工具安装了 Bottle,你的路径中也会有一个方便的 bottle 命令。尝试以下其中一种方式

bottle --help
python3 -m bottle --help
./path/to/bottle.py --help

下面是一个快速示例

$ bottle --debug --reload mymodule
Bottle v0.13-dev server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Hit Ctrl-C to quit.

0.13 版更改: 安装到(虚拟)环境中的可执行脚本曾命名为 bottle.py,这可能导致循环导入。旧名称现已弃用,新的可执行文件仅命名为 bottle

请求路由

在上一章中,我们构建了一个非常简单的 Web 应用程序,只有一个路由。这里再次展示“Hello World”示例的路由部分

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

route() 装饰器将 URL 路径链接到回调函数,并为 默认应用 添加一个新路由。然而,只有一个路由的应用有点无聊。让我们添加一些更多(别忘了 from bottle import template

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

这个示例演示了两件事:你可以将多个路由绑定到一个回调,并且可以将通配符添加到 URL 并通过关键字参数访问它们。

动态路由

包含通配符的路由称为动态路由(与静态路由相对),并且同时匹配多个 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):
    ...

可以使用过滤器来定义更具体的通配符,和/或在将 URL 的匹配部分传递给回调之前对其进行转换。带过滤器的通配符声明为 <name:filter><name:filter:config>。可选的配置部分的语法取决于使用的过滤器。

默认实现了以下过滤器,后续可能会添加更多

  • :int 仅匹配(带符号)数字,并将值转换为整数。

  • :float 类似于 :int,但用于十进制数。

  • :path 以非贪婪方式匹配所有字符,包括斜杠字符,可用于匹配多个路径段。

  • :re 允许你在配置字段中指定自定义正则表达式。匹配的值不会被修改。

让我们看一些实际示例

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

你也可以添加自己的过滤器。详情请参见 请求路由

HTTP 请求方法

HTTP 协议定义了几种请求方法(有时也称为“动词”)用于不同的任务。对于未指定其他方法的路由,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.username
    password = request.forms.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 <BaseRequest.forms> 的用法在 :ref:`tutorial-request 一节中有进一步描述。

特殊方法:HEAD 和 ANY

HEAD 方法用于请求与 GET 请求对应的响应,但不包含响应正文。这对于检索资源的元信息而无需下载整个文档非常有用。Bottle 会自动处理这些请求,通过回退到相应的 GET 路由并截断请求正文(如果存在)来实现。你无需自己指定任何 HEAD 路由。

此外,非标准的 ANY 方法作为低优先级回退:监听 ANY 的路由将匹配请求,无论其 HTTP 方法是什么,但仅在没有定义其他更具体的路由时才会匹配。这对于将请求重定向到更具体的子应用的代理路由很有帮助。

总而言之:HEAD 请求回退到 GET 路由,所有请求回退到 ANY 路由,但前提是没有针对原始请求方法的匹配路由。就这么简单。

提供静态资源

静态文件或资源,例如图像或 CSS 文件,不会自动提供。你必须添加一个路由和回调来控制哪些文件被提供以及在哪里找到它们。Bottle 提供了一个方便的 static_file() 函数,可以完成大部分工作,并以安全便捷的方式提供文件

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

请注意,我们在这里使用了 :path 路由过滤器,以允许 filepath 中包含斜杠字符,并同时提供子目录中的文件。

static_file() 助手函数与手动处理文件相比有很多优势。最重要的是,它通过限制文件访问到指定的 root 目录来防止目录遍历攻击(例如 GET /static/../../../../etc/secrets)。不过,请确保为 root 使用绝对路径。相对路径(以 ./ 开头)会相对于当前工作目录解析,而当前工作目录可能并非总是你的项目目录。

static_file() 函数返回 HTTPResponseHTTPError,如果需要,可以将其作为异常引发。

文件下载

大多数浏览器在知道 MIME 类型的情况下(例如 PDF 文件)会尝试使用关联的应用程序打开下载的文件。如果这不是你想要的行为,你可以强制弹出下载对话框,甚至向用户建议文件名

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

如果 download 参数仅为 True,则使用原始文件名。

生成内容

我们已经知道路由回调可以返回字符串作为内容,但还有更多:Bottle 支持多种其他数据类型,甚至可能添加 Content-Length 等其他头信息,这样你就不必自己操作了。以下是你可能从应用回调中返回的数据类型列表,以及框架如何处理这些数据类型的简要说明

字典

Python 字典(或其子类)会自动转换为 JSON 字符串并返回给浏览器,同时 Content-Type 头被设置为 application/json。这使得实现基于 JSON 的 API 变得容易,并且实际上是由 JsonPlugin 实现的,该插件会自动应用于所有路由。如果需要,你可以配置和禁用 JSON 处理。

空字符串、FalseNone 或其他非真值

这些将生成空响应。

字符串列表

字节或 Unicode 字符串列表会连接成一个字符串,然后像往常一样处理(见下文)。

Unicode 字符串

Unicode 字符串会使用 Content-Type 头中指定的编码(默认为 utf8)自动编码,然后作为字节字符串处理(见下文)。

字节字符串

原始字节字符串按原样写入响应。

HTTPErrorHTTPResponse 的实例

抛出或返回 HTTPResponse 的实例将覆盖对全局 request 对象所做的任何更改,然后照常继续。如果是 HTTPError,将首先应用错误处理程序。详情请参见 错误处理

文件或类文件对象

任何具有 .read() 方法的对象都被视为文件或类文件对象,并传递给 WSGI 服务器框架定义的 wsgi.file_wrapper 可调用对象。一些 WSGI 服务器实现可以利用优化的系统调用(例如 sendfile)更高效地传输文件。在其他情况下,这只是迭代适合内存的块。可选的头信息,如 Content-LengthContent-Type 不会自动设置。出于安全和其他原因,你应该总是优先使用 static_file(),而不是直接返回原始文件。详情请参见 提供静态资源

可迭代对象或生成器

你可以从路由回调中 yield 字节或 Unicode 字符串(不能同时),Bottle 将以流式方式将其写入响应。在这种情况下,不会设置 Content-Length 头,因为最终响应大小未知。不支持嵌套的可迭代对象,抱歉。请注意,一旦可迭代对象 yield 第一个非空值,HTTP 状态码和头信息就会发送到浏览器。之后更改这些信息无效。如果可迭代对象的第一个元素是 HTTPErrorHTTPResponse,则忽略迭代器的其余部分。

此列表的顺序很重要。例如,你可以返回一个带有 read() 方法的 str 子类。它仍然被视为字符串而不是文件,因为字符串首先被处理。

更改默认编码

Bottle 使用 Content-Type 头中的 charset 参数来决定如何编码 Unicode 字符串。该头默认为 text/html; charset=UTF8,可以通过 Response.content_type 属性或直接设置 Response.charset 属性来更改。(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)。

错误处理

如果出现问题,Bottle 会显示一个信息丰富但相当朴素的错误页面。如果启用了 debug() 模式,该页面会包含堆栈跟踪。你可以使用 error() 装饰器覆盖默认错误页面并为特定的 HTTP 状态码注册错误处理程序

from bottle import error

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

从现在起,404(文件未找到)错误将向用户显示一个自定义错误页面。传递给错误处理程序的唯一参数是 HTTPError 的实例。除此之外,错误处理程序与常规请求回调非常相似。你可以从 request 读取,写入 response,并返回任何支持的数据类型,除了 HTTPError 实例。

错误处理程序仅在你应用返回或抛出 HTTPError 异常时才会使用(abort() 就实现了这一点)。将 Response.status 设置为错误代码或返回 HTTPResponse 不会触发错误处理程序。

使用 abort() 触发错误

abort() 函数是触发 HTTP 错误的快捷方式。

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

使用 abort() 时无需返回任何内容。它会引发 HTTPError 异常。

其他异常

所有 HTTPResponseHTTPError 之外的异常都将导致 500 Internal Server Error 响应,因此它们不会使你的 WSGI 服务器崩溃。你可以通过将 bottle.app().catchall 设置为 False 来关闭此行为,以便在你的中间件中处理异常。

response 对象

响应元数据,例如 HTTP 状态码、响应头和 cookie,存储在一个名为 response 的对象中,直到它们被传输到浏览器。你可以直接操作这些元数据,或使用预定义的助手方法。完整的 API 和功能列表在 API 部分描述(参见 BaseResponse),但这里也涵盖了最常见的用例和功能。

状态码

HTTP 状态码控制浏览器的行为,默认为 200 OK。在大多数情况下,你无需手动设置 Response.status 属性,而是使用 abort() 助手或返回带有适当状态码的 HTTPResponse 实例。允许使用任何整数,但 HTTP 规范定义的代码之外的代码只会让浏览器感到困惑并违反标准。

响应头

响应头,例如 Cache-ControlLocation,通过 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', 'name=value2')

请注意,这只是一个示例。如果你想处理 cookie,请阅读后续内容

重定向

要将客户端重定向到不同的 URL,你可以发送一个 303 See Other 响应,并将 Location 头设置为新的 URL。redirect() 会为你完成这项工作

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

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 时间戳。(默认值:None

  • domain: 允许读取 cookie 的域名。(默认值:当前域名)

  • path: 将 cookie 限制在给定的路径下(默认值:/

  • secure: 将 cookie 限制在 HTTPS 连接下(默认值:关闭)。

  • httponly: 防止客户端 JavaScript 读取此 cookie(默认值:关闭,需要 Python 2.7 或更高版本)。

  • samesite: 禁用第三方使用 cookie。允许的属性值:laxstrict。在 strict 模式下,cookie 永远不会发送。在 lax 模式下,cookie 仅在顶级 GET 请求时发送。

如果既没有设置 expires 也没有设置 max_age,cookie 将在浏览器会话结束或浏览器窗口关闭时过期。使用 cookie 时还需要考虑一些其他问题

  • 在大多数浏览器中,cookie 的文本限制为 4 KB。

  • 有些用户将其浏览器配置为完全不接受 cookie。大多数搜索引擎也忽略 cookie。请确保你的应用程序在没有 cookie 的情况下也能正常工作。

  • cookie 存储在客户端,并且没有任何加密。无论你存储在 cookie 中的是什么,用户都可以读取。更糟糕的是,攻击者可能会通过你端的 XSS 漏洞窃取用户的 cookie。已知有些病毒也会读取浏览器 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 会自动序列化 (pickle) 并反序列化 (unpickle) 存储到签名 cookie 中的任何数据。这允许你将任何可序列化的对象(不仅是字符串)存储到 cookie 中,只要序列化后的数据不超过 4 KB 的限制。

警告

签名 cookie 未加密(客户端仍然可以看到内容),也未受复制保护(客户端可以恢复旧的 cookie)。其主要目的是使序列化和反序列化安全并防止篡改,而不是在客户端存储秘密信息。

请求数据

Cookie、HTTP 头、表单数据及其他请求数据均可通过全局 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 来访问数据。我们在这里只介绍最常用的功能,但这应该足以入门。

HTTP 头

客户端发送的所有 HTTP 头(例如 RefererAgentAccept-Language)都存储在 WSGIHeaderDict 中,可通过 Request.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'

Cookies

Cookie 是存储在客户端浏览器中的一小段文本,每次请求都会发送回服务器。它们有助于在多个请求之间保持一些状态(HTTP 本身是无状态的),但不应用于安全相关的事务。它们很容易被客户端伪造。

客户端发送的所有 cookie 都可通过 Request.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

Request.get_cookie 方法是访问 cookie 的另一种方式。它支持解码在单独章节中描述的签名 cookie

查询参数、表单和文件上传

查询和表单数据会按需解析,并通过 request 属性和方法访问。其中一些属性组合了来自不同来源的值,以便于访问。请看下表快速概览。

属性

数据源

Request.GET

查询参数

Request.query

Request.GET 的别名

Request.POST

表单字段和文件上传组合

Request.forms

表单字段

Request.files

文件上传或非常大的表单字段

Request.params

查询参数和表单字段组合

介绍 FormsDict

Bottle 使用一种特殊类型的字典来存储这些参数。FormsDict 的行为类似于普通字典,但有一些附加功能,使你的工作更轻松。

首先,FormsDictMultiDict 的子类,并且可以为每个键存储多个值。默认情况下只返回第一个值,但可以使用 MultiDict.getall() 获取特定键的所有值的列表(可能为空)

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

也支持类似属性的访问方式,对于缺失的值返回空字符串。这在处理大量可选属性时极大地简化了代码

name = request.query.name    # may be an empty string

关于 Unicode 和字符编码的说明

请求路径、查询参数或 cookie 中的 Unicode 字符有点棘手。HTTP 是一个非常老的基于字节的协议,它早于 Unicode,并且缺乏明确的编码信息。这就是为什么 WSGI 服务器必须回退到 ISO-8859-1(也称为 latin1,一种可逆的输入编码)来处理这些字符串。不过,现代浏览器默认使用 utf8。要求应用开发者手动将每一个用户输入字符串转换为正确的编码,这有点过于繁重。Bottle 使这一切变得容易,并简单地假设一切都是 utf8 编码。Bottle API 返回的所有字符串都支持完整的 Unicode 字符范围,前提是网页或 HTTP 客户端遵循最佳实践并且不违反既定标准。

查询参数

查询字符串(如 /forum?id=1&page=5)常用于向服务器传输少量键/值对。你可以使用 Request.query 属性(一个 FormsDict)访问这些值,使用 Request.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)。当 method="get" 时,表单值会被附加到 URL,并通过上面描述的 Request.query 访问。这有时被认为不安全且有其他限制,因此我们在这里使用 method="post"。如果不确定,请使用 POST 表单。

通过 POST 传输的表单字段存储在 Request.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.username
    password = request.forms.password
    if check_login(username, password):
        return "<p>Your login information was correct.</p>"
    else:
        return "<p>Login failed.</p>"

文件上传

为了支持文件上传,我们需要稍微修改 <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 将文件上传存储在 Request.files 中,作为 FileUpload 实例,并包含一些关于上传的元数据。假设你只是想将文件保存到磁盘

@route('/upload', method='POST')
def do_upload():
    category   = request.forms.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 包含客户端文件系统上的文件名,但已清理和规范化,以防止文件名中包含不受支持的字符或路径段导致错误。如果你需要客户端发送的未经修改的文件名,请查看 FileUpload.raw_filename

如果你想将文件存储到磁盘,强烈推荐使用 FileUpload.save 方法。它能防止一些常见错误(例如,除非你明确告知,否则不会覆盖现有文件),并以内存高效的方式存储文件。你可以通过 FileUpload.file 直接访问文件对象,但请务必小心。

JSON

对于 JavaScript 和 REST API,向服务器发送 application/json 而不是表单数据非常常见。Request.json 属性包含解析后的数据结构(如果可用),对于空请求或不包含 application/json 数据的请求,则为 None。解析错误会触发相应的 HTTPError

原始请求数据

你可以通过 Request.body 将原始主体数据作为类文件对象访问。根据内容长度和 Request.MEMFILE_MAX 设置,这可能是一个 io.BytesIO 缓冲区或一个临时文件。在这两种情况下,在你访问该属性之前,主体都已完全缓冲。如果你期望处理大量数据并希望直接无缓冲地访问流,请查看 request['wsgi.input']

WSGI 环境

每个 BaseRequest 实例都封装了一个 WSGI 环境字典,该字典存储在 Request.environ 中。大多数有趣的信息也通过特殊方法或属性暴露,但如果你想直接访问原始 WSGI environ,你可以这样做

@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() 来执行此操作。在调试模式下,缓存是禁用的。

应用结构

Bottle 维护一个全局的 Bottle 实例栈,并将栈顶作为一些模块级函数和装饰器的默认应用。例如,route() 装饰器或 run() 函数是默认应用上 Bottle.route()Bottle.run() 的快捷方式

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

if __name__ == '__main__':
    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

术语表

callback (回调)

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

decorator (装饰器)

一个返回另一个函数的函数,通常使用 @decorator 语法作为函数转换应用。有关装饰器的更多信息,请参见Python 函数定义文档

environ (环境)

一个结构,其中保存了根目录下所有文档的信息,并用于交叉引用。环境在解析阶段后被序列化 (pickled),以便后续运行只需读取和解析新的和更改的文档。

handler function (处理函数)

用于处理特定事件或情况的函数。在 Web 框架中,通过将处理函数作为每个组成应用的特定 URL 的回调来开发应用。

source directory (源目录)

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