分类目录归档:计算机与编程

《Flask 入门教程》第 5 章:数据库

大部分程序都需要保存数据,所以不可避免要使用数据库。用来操作数据库的数据库管理系统(DBMS)有很多选择,对于不同类型的程序,不同的使用场景,都会有不同的选择。在这个教程中,我们选择了属于关系型数据库管理系统(RDBMS)的 SQLite,它基于文件,不需要单独启动数据库服务器,适合在开发时使用,或是在数据库操作简单、访问量低的程序中使用。

使用 SQLAlchemy 操作数据库

为了简化数据库操作,我们将使用 SQLAlchemy——一个 Python 数据库工具(ORM,即对象关系映射)。借助 SQLAlchemy,你可以通过定义 Python 类来表示数据库里的一张表(类属性表示表中的字段 / 列),通过对这个类进行各种操作来代替写 SQL 语句。这个类我们称之为模型类,类中的属性我们将称之为字段

Flask 有大量的第三方扩展,这些扩展可以简化和第三方库的集成工作。我们下面将使用一个叫做 Flask-SQLAlchemy 的官方扩展来集成 SQLAlchemy。

首先使用 Pipenv 安装它:

$ pipenv install flask-sqlalchemy

大部分扩展都需要执行一个“初始化”操作。你需要导入扩展类,实例化并传入 Flask 程序实例:

from flask_sqlalchemy import SQLAlchemy  # 导入扩展类

app = Flask(__name__)

db = SQLAlchemy(app)  # 初始化扩展,传入程序实例 app

设置数据库 URI

为了设置 Flask、扩展或是我们程序本身的一些行为,我们需要设置和定义一些配置变量。Flask 提供了一个统一的接口来写入和获取这些配置变量:Flask.config 字典。配置变量的名称必须使用大写,写入配置的语句一般会放到扩展类实例化语句之前。

下面写入了一个 SQLALCHEMY_DATABASE_URI 变量来告诉 SQLAlchemy 数据库连接地址:

import os

# ...

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + os.path.join(app.root_path, 'data.db')

注意 这个配置变量的最后一个单词是 URI,而不是 URL。

对于这个变量值,不同的 DBMS 有不同的格式,对于 SQLite 来说,这个值的格式如下:

sqlite:////数据库文件的绝对地址

数据库文件一般放到项目根目录即可,app.root_path 返回程序实例所在模块的路径(目前来说,即项目根目录),我们使用它来构建文件路径。数据库文件的名称和后缀你可以自由定义,一般会使用 .db、.sqlite 和 .sqlite3 作为后缀。

另外,如果你使用 Windows 系统,上面的 URI 前缀部分需要写入三个斜线(即 sqlite:///)。在本书的示例程序代码里,做了一些兼容性处理,另外还新设置了一个配置变量,实际的代码如下:

import os
import sys

from flask import Flask

WIN = sys.platform.startswith('win')
if WIN:  # 如果是 Windows 系统,使用三个斜线
    prefix = 'sqlite:///'
else:  # 否则使用四个斜线
    prefix = 'sqlite:////'

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(app.root_path, 'data.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False  # 关闭对模型修改的监控

如果你固定在某一个操作系统上进行开发,部署时也使用相同的操作系统,那么可以不用这么做,直接根据你的需要写出前缀即可。

提示 你可以访问 Flask 文档的配置页面查看 Flask 内置的配置变量;同样的,在 Flask-SQLAlchemy 文档的配置页面可以看到 Flask-SQLAlchemy 提供的配置变量。

创建数据库模型

在 Watchlist 程序里,目前我们有两类数据要保存:用户信息和电影条目信息。下面分别创建了两个模型类来表示这两张表:

app.py:创建数据库模型

class User(db.Model):  # 表名将会是 user(自动生成,小写处理)
    id = db.Column(db.Integer, primary_key=True)  # 主键
    name = db.Column(db.String(20))  # 名字


class Movie(db.Model):  # 表名将会是 movie
    id = db.Column(db.Integer, primary_key=True)  # 主键
    title = db.Column(db.String(60))  # 电影标题
    year = db.Column(db.String(4))  # 电影年份

模型类的编写有一些限制:

  • 模型类要声明继承 db.Model
  • 每一个类属性(字段)要实例化 db.Column,传入的参数为字段的类型,下面的表格列出了常用的字段类。
  • 在 db.Column() 中添加额外的选项(参数)可以对字段进行设置。比如,primary_key 设置当前字段是否为主键。除此之外,常用的选项还有 nullable(布尔值,是否允许为空值)、index(布尔值,是否设置索引)、unique(布尔值,是否允许重复值)、default(设置默认值)等。

常用的字段类型如下表所示:

字段类 说明
db.Integer 整型
db.String (size) 字符串,size 为最大长度,比如 db.String(20)
db.Text 长文本
db.DateTime 时间日期,Python datetime 对象
db.Float 浮点数
db.Boolean 布尔值

创建数据库表

模型类创建后,还不能对数据库进行操作,因为我们还没有创建表和数据库文件。下面在 Python Shell 中创建了它们:

$ flask shell
>>> from app import db
>>> db.create_all()

打开文件管理器,你会发现项目根目录下出现了新创建的数据库文件 data.db。这个文件不需要提交到 Git 仓库,我们在 .gitignore 文件最后添加一行新规则:

*.db

如果你改动了模型类,想重新生成表模式,那么需要先使用 db.drop_all() 删除表,然后重新创建:

>>> db.drop_all()
>>> db.create_all()

注意这会一并删除所有数据,如果你想在不破坏数据库内的数据的前提下变更表的结构,需要使用数据库迁移工具,比如集成了 Alembic 的 Flask-Migrate 扩展。

提示 上面打开 Python Shell 使用的是 flask shell命令,而不是 python。使用这个命令启动的 Python Shell 激活了“程序上下文”,它包含一些特殊变量,这对于某些操作是必须的(比如上面的 db.create_all()调用)。请记住,后续的 Python Shell 都会使用这个命令打开。

和 flask shell类似,我们可以编写一个自定义命令来自动执行创建数据库表操作:

import click

@app.cli.command()  # 注册为命令
@click.option('--drop', is_flag=True, help='Create after drop.')  # 设置选项
def initdb(drop):
    """Initialize the database."""
    if drop:  # 判断是否输入了选项
        db.drop_all()
    db.create_all()
    click.echo('Initialized database.')  # 输出提示信息

默认情况下,函数名称就是命令的名字,现在执行 flask initdb 命令就可以创建数据库表:

$ flask initdb

使用 --drop 选项可以删除表后重新创建:

$ flask initdb --drop

创建、读取、更新、删除

在前面打开的 Python Shell 里,我们来测试一下常见的数据库操作。你可以跟着示例代码来操作,也可以自由练习。

创建

下面的操作演示了如何向数据库中添加记录:

>>> from app import User, Movie  # 导入模型类
>>> user = User(name='Grey Li')  # 创建一个 User 记录
>>> m1 = Movie(title='Leon', year='1994')  # 创建一个 Movie 记录
>>> m2 = Movie(title='Mahjong', year='1996')  # 再创建一个 Movie 记录
>>> db.session.add(user)  # 把新创建的记录添加到数据库会话
>>> db.session.add(m1)
>>> db.session.add(m2)
>>> db.session.commit()  # 提交数据库会话,只需要在最后调用一次即可

提示 在实例化模型类的时候,我们并没有传入 id 字段(主键),因为 SQLAlchemy 会自动处理这个字段。

最后一行 db.session.commit() 很重要,只有调用了这一行才会真正把记录提交进数据库,前面的 db.session.add() 调用是将改动添加进数据库会话(一个临时区域)中。

读取

通过对模型类的 query 属性调用可选的过滤方法和查询方法,我们就可以获取到对应的单个或多个记录(记录以模型类实例的形式表示)。查询语句的格式如下:

<模型类>.query.<过滤方法(可选)>.<查询方法>

下面是一些常用的过滤方法:

过滤方法 说明
filter() 使用指定的规则过滤记录,返回新产生的查询对象
filter_by() 使用指定规则过滤记录(以关键字表达式的形式),返回新产生的查询对象
order_by() 根据指定条件对记录进行排序,返回新产生的查询对象
group_by() 根据指定条件对记录进行分组,返回新产生的查询对象

下面是一些常用的查询方法:

查询方法 说明
all() 返回包含所有查询记录的列表
first() 返回查询的第一条记录,如果未找到,则返回None
get(id) 传入主键值作为参数,返回指定主键值的记录,如果未找到,则返回None
count() 返回查询结果的数量
first_or_404() 返回查询的第一条记录,如果未找到,则返回404错误响应
get_or_404(id) 传入主键值作为参数,返回指定主键值的记录,如果未找到,则返回404错误响应
paginate() 返回一个Pagination对象,可以对记录进行分页处理

下面的操作演示了如何从数据库中读取记录,并进行简单的查询:

>>> from app import Movie  # 导入模型类
>>> movie = Movie.query.first()  # 获取 Movie 模型的第一个记录(返回模型类实例)
>>> movie.title  # 对返回的模型类实例调用属性即可获取记录的各字段数据
'Leon'
>>> movie.year
'1994'
>>> Movie.query.all()  # 获取 Movie 模型的所有记录,返回包含多个模型类实例的列表
[, ]
>>> Movie.query.count()  # 获取 Movie 模型所有记录的数量
2
>>> Movie.query.get(1)  # 获取主键值为 1 的记录
<Movie 1>
>>> Movie.query.filter_by(title='Mahjong').first()  # 获取 title 字段值为 Mahjong 的记录
<Movie 2>
>>> Movie.query.filter(Movie.title=='Mahjong').first()  # 等同于上面的查询,但使用不同的过滤方法
<Movie 2>

提示 我们在说 Movie 模型的时候,实际指的是数据库中的 movie 表。表的实际名称是模型类的小写形式(自动生成),如果你想自己指定表名,可以定义 __tablename__ 属性。

对于最基础的 filter() 过滤方法,SQLAlchemy 支持丰富的查询操作符,具体可以访问文档相关页面查看。除此之外,还有更多的查询方法、过滤方法和数据库函数可以使用,具体可以访问文档的 Query API 部分查看。

更新

下面的操作更新了 Movie 模型中主键为 2 的记录:

>>> movie = Movie.query.get(2)
>>> movie.title = 'WALL-E'  # 直接对实例属性赋予新的值即可
>>> movie.year = '2008'
>>> db.session.commit()  # 注意仍然需要调用这一行来提交改动

删除

下面的操作删除了 Movie 模型中主键为 1 的记录:

>>> movie = Movie.query.get(1)
>>> db.session.delete(movie)  # 使用 db.session.delete() 方法删除记录,传入模型实例
>>> db.session.commit()  # 提交改动

在程序里操作数据库

经过上面的一番练习,我们可以在 Watchlist 里进行实际的数据库操作了。

在主页视图读取数据库记录

因为设置了数据库,负责显示主页的 index 可以从数据库里读取真实的数据:

@app.route('/')
def index():
    user = User.query.first()  # 读取用户记录
    movies = Movie.query.all()  # 读取所有电影记录
    return render_template('index.html', user=user, movies=movies)

在 index 视图中,原来传入模板的 name 变量被 user 实例取代,模板 index.html 中的两处 name 变量也要相应的更新为 user.name 属性:

{{ user.name }}'s Watchlist

生成虚拟数据

因为有了数据库,我们可以编写一个命令函数把虚拟数据添加到数据库里。下面是用来生成虚拟数据的命令函数:

import click

@app.cli.command()
def forge():
    """Generate fake data."""
    db.create_all()
    
    # 全局的两个变量移动到这个函数内
    name = 'Grey Li'
    movies = [
        {'title': 'My Neighbor Totoro', 'year': '1988'},
        {'title': 'Dead Poets Society', 'year': '1989'},
        {'title': 'A Perfect World', 'year': '1993'},
        {'title': 'Leon', 'year': '1994'},
        {'title': 'Mahjong', 'year': '1996'},
        {'title': 'Swallowtail Butterfly', 'year': '1996'},
        {'title': 'King of Comedy', 'year': '1999'},
        {'title': 'Devils on the Doorstep', 'year': '1999'},
        {'title': 'WALL-E', 'year': '2008'},
        {'title': 'The Pork of Music', 'year': '2012'},
    ]
    
    user = User(name=name)
    db.session.add(user)
    for m in movies:
        movie = Movie(title=m['title'], year=m['year'])
        db.session.add(movie)
    
    db.session.commit()
    click.echo('Done.')

现在执行 flask forge 命令就会把所有虚拟数据添加到数据库里:

$ flask forge

本章小结

本章我们学习了使用 SQLAlchemy 操作数据库,后面你会慢慢熟悉相关的操作。结束前,让我们提交代码:

$ git add .
$ git commit -m "Add database support with Flask-SQLAlchemy"
$ git push

提示 你可以在 GitHub 上查看本书示例程序的对应 commit:4d2442a

进阶提示

  • 在生产环境,你可以更换更合适的 DBMS,因为 SQLAlchemy 支持多种 SQL 数据库引擎,通常只需要改动非常少的代码。
  • 我们的程序只有一个用户,所以没有将 User 表和 Movie 表建立关联。访问 Flask-SQLAlchemy 文档的”声明模型“章节可以看到相关内容
  • 《Flask Web 开发实战》第 5 章详细介绍了 SQLAlchemy 和 Flask-Migrate 的使用,第 8 章和第 9 章引入了更复杂的模型关系和查询方法。
  • 阅读 SQLAlchemy 官方文档和教程详细了解它的用法。注意我们在这里使用 Flask-SQLAlchemy 来集成它,所以用法和单独使用 SQLAlchemy 有一些不同。作为参考,你可以同时阅读 Flask-SQLAlchemy 官方文档 。
  • 本书主页 & 相关资源索引:http://helloflask.com/tutorial

《Flask 入门教程》第 4 章:使用静态文件

静态文件(static files)和我们的模板概念相反,指的是内容不需要动态生成的文件。比如图片、CSS 文件和 JavaScript 脚本等。

在 Flask 中,我们需要创建一个 static 文件夹来保存静态文件,它应该和程序模块、templates 文件夹在同一目录层级,所以我们在项目根目录创建它:

$ mkdir static

生成静态文件 URL

在 HTML 文件里,引入这些静态文件需要给出资源所在的 URL。为了更加灵活,这些文件的 URL 可以通过 Flask 提供的 url_for() 函数来生成。

在第 2 章的最后,我们学习过 url_for() 函数的用法,传入端点值(视图函数的名称)和参数,它会返回对应的 URL。对于静态文件,需要传入的端点值是 static,同时使用filename 参数来传入相对于 static 文件夹的文件路径。

假如我们在 static 文件夹的根目录下面放了一个 foo.jpg 文件,下面的调用可以获取它的 URL:

<img src="{{ url_for('static', filename='foo.jpg') }}">

花括号部分的调用会返回 /static/foo.jpg

提示 在 Python 脚本里,url_for() 函数需要从 flask 包中导入,而在模板中则可以直接使用,因为 Flask 把一些常用的函数和对象添加到了模板上下文(环境)里。

添加 Favicon

Favicon(favourite icon) 是显示在标签页和书签栏的网站头像。你需要准备一个 ICO、PNG 或 GIF 格式的图片,大小一般为 16×16、32×32、48×48 或 64×64 像素。把这个图片放到 static 目录下,然后像下面这样在 HTML 模板里引入它:

templates/index.html:引入 Favicon

<head>
    ...
    <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
</head>

保存后刷新页面,即可在浏览器标签页上看到这个图片。

添加图片

为了让页面不那么单调,我们来添加两个图片:一个是显示在页面标题旁边的头像,另一个是显示在页面底部的龙猫动图。我们在 static 目录下面创建一个子文件夹 images,把这两个图片都放到这个文件夹里:

$ cd static
$ mkdir images

创建子文件夹并不是必须的,这里只是为了更好的组织同类文件。同样的,如果你有多个 CSS 文件,也可以创建一个 css 文件夹来组织他们。下面我们在页面模板中添加这两个图片,注意填写正确的文件路径:

templates/index.html:添加图片

<h2>
    <img alt="Avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
    {{ name }}'s Watchlist
</h2>
...
<img alt="Walking Totoro" src="{{ url_for('static', filename='images/totoro.gif') }}">

提示 这两张图片你可以自己替换为任意的图片(注意更新文件名),也可以在示例程序的 GitHub 仓库下载。

添加 CSS

虽然添加了图片,但页面还是非常简陋,因为我们还没有添加 CSS 定义。下面在 static 目录下创建一个 CSS 文件 style.css,内容如下:

static/style.css:定义页面样式

/* 页面整体 */
body {
    margin: auto;
    max-width: 580px;
    font-size: 14px;
    font-family: Helvetica, Arial, sans-serif;
}

/* 页脚 */
footer {
    color: #888;
    margin-top: 15px;
    text-align: center;
    padding: 10px;
}

/* 头像 */
.avatar {
    width: 40px;
}

/* 电影列表 */
.movie-list {
    list-style-type: none;
    padding: 0;
    margin-bottom: 10px;
    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}

.movie-list li {
    padding: 12px 24px;
    border-bottom: 1px solid #ddd;
}

.movie-list li:last-child {
    border-bottom:none;
}

.movie-list li:hover {
    background-color: #f8f9fa;
}

/* 龙猫图片 */
.totoro {
    display: block;
    margin: 0 auto;
    height: 100px;
}

接着在页面的 <head> 标签内引入这个 CSS 文件:

templates/index.html:引入 CSS 文件

<head>
    ...
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
</head>

最后要为对应的元素设置 class 属性值,以便和对应的 CSS 定义关联起来:

templates/index.html:添加 class 属性

<h2>
    <img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
    {{ name }}'s Watchlist
</h2>
...
<ul class="movie-list">
    ...
</ul>
<img alt="Walking Totoro" class="totoro" src="{{ url_for('static', filename='images/totoro.gif') }}">

最终的页面如下图所示(你可以自由修改 CSS 定义,我已经尽力了):

4-1

本章小结

主页现在基本成型了,接下来我们会慢慢完成程序的功能。结束前,让我们提交代码:

$ git add .
$ git commit -m "Add static files"
$ git push

提示 你可以在 GitHub 上查看本书示例程序的对应 commit:e51c579

进阶提示

《Flask 入门教程》第 3 章:模板

在一般的 Web 程序里,访问一个地址通常会返回一个包含各类信息的 HTML 页面。因为我们的程序是动态的,页面中的某些信息需要根据不同的情况来进行调整,比如对登录和未登录用户显示不同的信息,所以页面需要在用户访问时根据程序逻辑动态生成。

我们把包含变量和运算逻辑的 HTML 或其他格式的文本叫做模板,执行这些变量替换和逻辑计算工作的过程被称为渲染,这个工作由我们这一章要学习使用的模板渲染引擎——Jinja2 来完成。

按照默认的设置,Flask 会从程序实例所在模块同级目录的 templates 文件夹中寻找模板,我们的程序目前存储在项目根目录的 app.py 文件里,所以我们要在项目根目录创建这个文件夹:

$ mkdir templates

模板基本语法

在社交网站上,每个人都有一个主页,借助 Jinja2 就可以写出一个通用的模板:

<h1>{{ username }}的个人主页</h1>
{% if bio %}
    <p>{{ bio }}</p>  {# 这里的缩进只是为了可读性,不是必须的 #}
{% else %}
    <p>自我介绍为空。</p>
{% endif %}  {# 大部分 Jinja 语句都需要声明关闭 #}

Jinja2 的语法和 Python 大致相同,你在后面会陆续接触到一些常见的用法。在模板里,你需要添加特定的定界符将 Jinja2 语句和变量标记出来,下面是三种常用的定界符:

  • {{ ... }} 用来标记变量。
  • {% ... %} 用来标记语句,比如 if 语句,for 语句等。
  • {# ... #} 用来写注释。

模板中使用的变量需要在渲染的时候传递进去,具体我们后面会了解。

编写主页模板

我们先在 templates 目录下创建一个 index.html 文件,作为主页模板。主页需要显示电影条目列表和个人信息,代码如下所示:

templates/index.html:主页模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>{{ name }}'s Watchlist</title>
</head>
<body>
    <h2>{{ name }}'s Watchlist</h2>
    {# 使用 length 过滤器获取 movies 变量的长度 #}
    <p>{{ movies|length }} Titles</p>
    <ul>
        {% for movie in movies %}  {# 迭代 movies 变量 #}
        <li>{{ movie.title }} - {{ movie.year }}</li>  {# 等同于 movie['title'] #}
        {% endfor %}  {# 使用 endfor 标签结束 for 语句 #}
    </ul>
    <footer>
        <small>&copy; 2018 <a href="http://helloflask.com/tutorial">HelloFlask</a></small>
	</footer>
</body>
</html>

为了方便对变量进行处理,Jinja2 提供了一些过滤器,语法形式如下:

{{ 变量|过滤器 }}

左侧是变量,右侧是过滤器名。比如,上面的模板里使用 length 过滤器来获取 movies 的长度,类似 Python 里的 len() 函数。

提示 访问 http://jinja.pocoo.org/docs/2.10/templates/#list-of-builtin-filters 查看所有可用的过滤器。

准备虚拟数据

为了模拟页面渲染,我们需要先创建一些虚拟数据,用来填充页面内容:

app.py:定义虚拟数据

name = 'Grey Li'
movies = [
    {'title': 'My Neighbor Totoro', 'year': '1988'},
    {'title': 'Dead Poets Society', 'year': '1989'},
    {'title': 'A Perfect World', 'year': '1993'},
    {'title': 'Leon', 'year': '1994'},
    {'title': 'Mahjong', 'year': '1996'},
    {'title': 'Swallowtail Butterfly', 'year': '1996'},
    {'title': 'King of Comedy', 'year': '1999'},
    {'title': 'Devils on the Doorstep', 'year': '1999'},
    {'title': 'WALL-E', 'year': '2008'},
    {'title': 'The Pork of Music', 'year': '2012'},
]

渲染主页模板

使用 render_template() 函数可以把模板渲染出来,必须传入的参数为模板文件名(相对于 templates 根目录的文件路径),这里即 'index.html'。为了让模板正确渲染,我们还要把模板内部使用的变量通过关键字参数传入这个函数,如下所示:

app.py:返回渲染好的模板作为响应

from flask import Flask, render_template

# ...

@app.route('/')
def index():
    return render_template('index.html', name=name, movies=movies)

为了更好的表示这个视图函数的作用,我们把原来的函数名 hello 改为 index,意思是“索引”,即主页。

在传入 render_template() 函数的关键字参数中,左边的 movies 是模板中使用的变量名称,右边的 movies 则是该变量指向的实际对象。这里传入模板的 name 是字符串,movies 是列表,但能够在模板里使用的不只这两种 Python 数据结构,你也可以传入元组、字典、函数等。

render_template() 函数在调用时会识别并执行 index.html 里所有的 Jinja2 语句,返回渲染好的模板内容。在返回的页面中,变量会被替换为实际的值(包括定界符),语句(及定界符)则会在执行后被移除(注释也会一并移除)。

现在访问 http://localhost:5000/ 看到的程序主页如下图所示:

3-1

本章小结

这一章我们编写了一个简单的主页。结束前,让我们提交代码:

$ git add .
$ git commit -m "Add index page"
$ git push

提示 你可以在 GitHub 上查看本书示例程序的对应 commit:8537d98

进阶提示

  • 使用 Faker 可以实现自动生成虚拟数据,它支持丰富的数据类型,比如时间、人名、地名、随机字符等等……
  • 除了过滤器,Jinja2 还在模板中提供了一些测试器、全局函数可以使用;除此之外,还有更丰富的控制结构支持,有一些我们会在后面学习到,更多的内容则可以访问 Jinja2 文档学习。
  • 如果你是《Flask Web 开发实战》的读者,模板相关内容可以在第 3 章《模板》找到,Faker 相关内容可以在第 7 章找到。
  • 本书主页 & 相关资源索引:http://helloflask.com/tutorial

Flask 全局错误处理(一个函数处理所有 HTTP 错误)

在 Flask 程序中,使用 app.errorhandler() 装饰器可以注册错误处理函数,传入 HTTP 错误状态码或是特定的异常类:

@app.errorhandler(404)
def error_404(e):
    return '404 Error', 404

如果发生 404 错误,就会触发这个函数获取返回值作为响应主体。

通常我们会为不同的 HTTP 错误编写各自的的错误处理函数,以便返回不同的响应。如果你愿意的话,我们也可以编写一个统一的错误处理函数,这个函数会处理所有的 HTTP 错误和一般异常,只需要在装饰器内传入 Exception 类即可:

@app.errorhandler(Exception)
def all_exception_handler(e):
    return 'Error', 500

现在所有的 HTTP 错误都会触发这个函数。你也可以在函数中对错误进行分类处理:

@app.errorhandler(Exception)
def all_exception_handler(e):
    # 对于 HTTP 异常,返回自带的错误描述和状态码
    # 这些异常类在 Werkzeug 中定义,均继承 HTTPException 类
    if isinstance(e, HTTPException):
        return e.desciption, e.code
    return 'Error', 500  # 一般异常

如果你使用 Flask 0.12 版本,则需要参考这个 SO 回答重写相关方法。

附注一些关于错误处理的小知识:

  • 对于一般的程序异常(比如 NameError),如果没有特定的异常处理函数,默认都会触发 500 错误处理函数。
  • 开启调试模式的时候,500 错误会显示错误调试页面。
  • 500 错误发生时传入错误处理函数的是真正的异常对象,不是 Werkzeug 内置的 HTTP 异常类。
  • 内置的 HTTP 异常类的 description 和 code 属性分别返回错误描述和状态码。

让 CKEditor 图片响应式(responsive)

通过 CKEditor 上传并插入图片后,CKEditor 的图片部件(widget)会在图片的 <img> 元素里插入行内样式定义来设置图片的宽高,这会导致响应式布局失效:图片因为被固定了宽高,在窗口缩小后会超出外层元素。生成的 HTML 代码示例如下:

<img alt="hello" src="/images/hello.jpg" style="height:300px; width:500px" />

对于这个问题,有三种解决方法:

方法 1:设置 CSS

最简单的解决方法是在 CSS 文件里加入下面的代码:

img {
    max-width: 100%;
    height: auto !important;
}

这会重写 CKEditor 生成的行内 CSS。

方法 2:使用插件 Enhanced Image

你可以使用加强版的图片部件插件 Enhanced Image(Image2)来替代原有的图片部件,同时确保 CKEditor 的版本大于 4.3。

(未测试。)

方法 3:使用 JavaScript

如果你使用 Bootstrap,这里还有另一种方法:

CKEDITOR.on('instanceReady', function (ev) {
    ev.editor.dataProcessor.htmlFilter.addRules( {
        elements : {
            img: function( el ) {
                // Add bootstrap "img-responsive" class to each inserted image
                el.addClass('img-responsive');
        
                // Remove inline "height" and "width" styles and
                // replace them with their attribute counterparts.
                // This ensures that the 'img-responsive' class works
                var style = el.attributes.style;
        
                if (style) {
                    // Get the width from the style.
                    var match = /(?:^|\s)width\s*:\s*(\d+)px/i.exec(style),
                        width = match && match[1];
        
                    // Get the height from the style.
                    match = /(?:^|\s)height\s*:\s*(\d+)px/i.exec(style);
                    var height = match && match[1];
        
                    // Replace the width
                    if (width) {
                        el.attributes.style = el.attributes.style.replace(/(?:^|\s)width\s*:\s*(\d+)px;?/i, '');
                        el.attributes.width = width;
                    }
        
                    // Replace the height
                    if (height) {
                        el.attributes.style = el.attributes.style.replace(/(?:^|\s)height\s*:\s*(\d+)px;?/i, '');
                        el.attributes.height = height;
                    }
                }
        
                // Remove the style tag if it is empty
                if (!el.attributes.style)
                    delete el.attributes.style;
            }
        }
    });
});

出处地址

需要注意的是,如果你使用 Bootstrap 4,需要把第 6 行的 img-responsive 换成 img-fluid。

(未测试。)

在 Flask 程序中实现 CKEditor 图片上传和 CSRF 保护

《Flask Web 开发实战》第 2 个实战项目是一个博客(Bluelog),这个项目本来没有添加图片上传支持,很多人想要自己实现,结果因为项目中同时使用了 CSRFProtect 扩展,它会默认验证所有 POST 请求,进而导致上传图片的请求出错。反馈的人多了,我就做了一些事情来改善这个问题:

  • 发布了 Flask-CKEditor 0.4.3,内置了对 CSRFProtect 扩展的支持。
  • 在 Bluelog 中实现了图片上传(7537416)。

如果你想实际体验,需要更新本地的 Bluelog 仓库,并更新 Flask-CKEditor 的版本。这篇文章会单独介绍使用 Flask-CKEditor 实现图片上传的方法,顺便说一下新的 CSRF 保护支持。

基础工作

使用扩展并不是必须的,但是可以简化大量的工作。这里将通过 Flask-CKEditor 来实现相关功能,首先安装(或更新)它:

$ pip install -U Flask-CKEditor

关于创建编辑器输入框等基础内容,可以参考这篇文章

实现图片上传

在使用文本编辑器写文章时,上传图片是一个很常见的需求。在CKEditor中,图片上传可以通过File Browser插件实现。在服务器端的Flask程序中,你需要做三件事:

  1. 创建一个视图函数来处理并保存上传文件
  2. 创建一个视图函数来获取图片文件,类似Flask内置的 static 端点
  3. 将配置变量 `CKEDITOR_FILE_UPLOADER` 设为这个视图函数的 URL 或端点值

完整的代码示例如下所示:

from flask_ckeditor import upload_success, upload_fail

app.config['CKEDITOR_FILE_UPLOADER'] = 'upload'

@app.route('/files/<path:filename>')
def uploaded_files(filename):
    path = '/the/uploaded/directory'
    return send_from_directory(path, filename)

@app.route('/upload', methods=['POST'])
def upload():
    f = request.files.get('upload')  # 获取上传图片文件对象,键必须为'upload'
    # Add more validations here
    extension = f.filename.split('.')[1].lower()
    if extension not in ['jpg', 'gif', 'png', 'jpeg']:  # 验证文件类型示例
        return upload_fail(message='Image only!')  # 返回upload_fail调用
    f.save(os.path.join('/the/uploaded/directory', f.filename))
    url = url_for('uploaded_files', filename=f.filename)
    return upload_success(url=url) # 返回upload_success调用

在处理上传文件的视图函数中,你必须返回 `upload_success()` 调用,每将 `url` 参数设置为获取上传文件的 URL。通常情况下,除了保存文件,你还需要对上传的图片进行验证和处理(大小、格式、文件名处理等等,具体可以访问这篇《Flask文件上传(一):原生实现》了解),在验证未通过时,你需要返回 `upload_fail()` 调用,并使用 `message` 参数传入错误消息。

当设置了 `CKEDITOR_FILE_UPLOADER` 配置变量后,你可以在编辑区域点开图片按钮打开的弹窗中看到一个新的上传标签。另外,你也可以直接将图片文件拖拽到编辑区域进行上传,或复制文件并粘贴到文本区域进行上传(CKEditor >= 4.5)。

提示 对应的示例程序在examples/image-upload/目录下。

如果你使用的 CKEditor 版本小于 4.5,则使用下面的方式实现:

from flask import send_from_directory

app.config['CKEDITOR_FILE_UPLOADER'] = 'upload'  # this value can be endpoint or url

@app.route('/files/<filename>')
def uploaded_files(filename):
    path = '/the/uploaded/directory'
    return send_from_directory(path, filename)

@app.route('/upload', methods=['POST'])
@ckeditor.uploader
def upload():
    f = request.files.get('upload')
    f.save(os.path.join('/the/uploaded/directory', f.filename))
    url = url_for('uploaded_files', filename=f.filename)
    return url

CSRF 保护

如果你想为图片上传的请求添加 CSRF 保护,可以通过 CSRFProtect 实现(Flask-WTF 内置),首先安装 Flask-WTF:

$ pip install flask-wtf

同时要确保 Flask-CKEditor 使用了最新版本(0.4.3)

然后初始化扩展:

from flask_wtf import CSRFProtect

csrf = CSRFProtect(app) 

Flask-CKEditor 0.4.3 版本内置了对 CSRFProtect 的支持,当使用 CSRFProtect 时,只需要把配置变量 `CKEDITOR_ENABLE_CSRF` 设为 `True` 即可开启 CSRF 保护:

app.config['CKEDITOR_ENABLE_CSRF'] = True

顺便说一下 Flask-CKEditor 内部的实现方式, Flask-CKEditor 需要把 CSRF 令牌放到上传图片的 AJAX 请求首部,这通过 CKEditor 4.9.0 版本新添加的一个配置选项 fileTools_requestHeaders 实现,这个配置可以用来想文件上传请求插入自定义的首部字段 。所以,如果想要实现 CSRF 保护,CKEditor 的版本需要大于或等于 4.9.0。

《Flask 入门教程》第 2 章:Hello, Flask!

追溯到最初,Flask 诞生于 Armin Ronacher 在 2010 年愚人节开的一个玩笑。后来,它逐渐发展成为一个成熟的 Python Web 框架,越来越受到开发者的喜爱。目前它在 GitHub 上是 Star 数量最多的 Python Web 框架,没有之一。

Flask 是典型的微框架,作为 Web 框架来说,它仅保留了核心功能:请求响应处理模板渲染。这两类功能分别由 Werkzeug(WSGI 工具库)完成和 Jinja(模板渲染库)完成,因为 Flask 包装了这两个依赖,我们暂时不用深入了解它们。

主页

这一章的主要任务就是为我们的程序编写一个简单的主页。主页的 URL 一般就是根地址,即 /。当用户访问根地址的时候,我们需要返回一行欢迎文字。这个任务只需要下面几行代码就可以完成:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return 'Welcome to My Watchlist!'

按照惯例,我们把程序保存为 app.py,确保当前目录是项目的根目录,然后在命令行窗口执行 flask run 命令启动程序(按下 Control + C 可以退出):

$ flask run
* Serving Flask app "app.py"
* Environment: production
  WARNING: Do not use the development server in a production environment.
  Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

现在打开浏览器,访问 http://localhost:5000 即可访问我们的程序主页,并看到我们在程序里返回的问候语,如下图所示:

2-1

执行 flask run 命令时,Flask 会使用内置的开发服务器来运行程序。这个服务器默认监听本地机的 5000 端口,也就是说,我们可以通过在地址栏输入 http://127.0.0.1:5000 或是 http://localhost:5000 访问程序。

注意 内置的开发服务器只能用于开发时使用,部署上线的时候要换用性能更好的服务器,我们会在最后一章学习。

解剖时间

下面我们来分解这个 Flask 程序,了解它的基本构成。

首先我们从 flask 包导入 Flask 类,通过实例化这个类,创建一个程序对象 app

from flask import Flask
app = Flask(__name__)

接下来,我们要注册一个处理函数,这个函数是处理某个请求的处理函数,Flask 官方把它叫做视图函数(view funciton),你可以理解为“请求处理函数”。

所谓的“注册”,就是给这个函数戴上一个装饰器帽子。我们使用 app.route() 装饰器来为这个函数绑定对应的 URL,当用户在浏览器访问这个 URL 的时候,就会触发这个函数,获取返回值,并把返回值显示到浏览器窗口:

@app.route('/')
def hello():
    return 'Welcome to My Watchlist!'

填入 app.route() 装饰器的第一个参数是 URL 规则字符串,这里的 /指的是根地址。

我们只需要写出相对地址,主机地址、端口号等都不需要写出。所以说,这里的 / 对应的是主机名后面的路径部分,完整 URL 就是 http://localhost:5000/。如果我们这里定义的 URL 规则是 /hello,那么完整 URL 就是 http://localhost:5000/hello 。

整个请求的处理过程如下所示:

  1. 当用户在浏览器地址栏访问这个地址,在这里即 http://localhost:5000/
  2. 服务器解析请求,发现请求 URL 匹配的 URL 规则是 /,因此调用对应的处理函数 hello()
  3. 获取 hello() 函数的返回值,处理后返回给客户端(浏览器)
  4. 浏览器接受响应,将其显示在窗口上

提示 在 Web 程序的语境下,虽然客户端可能有多种类型,但在本书里通常是指浏览器。

程序发现机制

如果你把上面的程序保存成其他的名字,比如 hello.py,接着执行 flask run 命令会返回一个错误提示。这是因为 Flask 默认会假设你把程序存储在名为 app.py 或 wsgi.py 的文件中。如果你使用了其他名称,就要设置系统环境变量 FLASK_APP 来告诉 Flask 你要启动哪个程序。

Flask 通过读取这个文件对应的模块寻找要运行的程序实例,你可以把它设置成下面这些值:

  • 模块名
  • Python 导入路径
  • 文件目录路径

管理环境变量

现在在启动 Flask 程序的时候,我们通常要和两个环境变量打交道:FLASK_APP 和 FLASK_ENV。因为我们的程序现在的名字是 app.py,暂时不需要设置 FLASK_APPFLASK_ENV 用来设置程序运行的环境,默认为 production。在开发时,我们需要开启调试模式(debug mode)。调试模式可以通过将系统环境变量 FLASK_ENV 设为 development 来开启。调试模式开启后,当程序出错,浏览器页面上会显示错误信息;代码出现变动后,程序会自动重载。

为了不用每次打开新的终端会话都要设置环境变量,我们安装用来管理系统环境变量的 python-dotenv:

$ pipenv install python-dotenv

当 python-dotenv 安装后,Flask 会从项目根目录的 .flaskenv 和 .env 文件读取环境变量并设置。我们分别使用文本编辑器创建这两个文件,或是使用更方便的 touch 命令创建:

$ touch .env .flaskenv

.flaskenv 用来存储 Flask 命令行系统相关的公开环境变量;而 .env 则用来存储敏感数据,不应该提交进Git仓库,我们把 .env 添加到 .gitignore 文件的结尾(新建一行)来让 Git 忽略它。你可以使用编辑器执行这个操作:

.env

在新创建的 .flaskenv 文件里,我们写入一行 FLASK_ENV=development ,将环境变量 FLASK_ENV 的值设为 development,以便开启调试模式:

FLASK_ENV=development

实验时间

在这个小节,我们可以通过做一些实验,来扩展和加深对本节内容的理解。

修改视图函数返回值

首先,你可以自由修改视图函数的返回值,比如:

@app.route('/')
def hello():
    return u'欢迎来到我的 Watchlist!'

返回值作为响应的主体,默认会被浏览器作为 HTML 格式解析,所以我们可以添加一个 HTML 元素标记:

@app.route('/')
def hello():
    return '<h1>Hello Totoro!</h1><img src="http://helloflask.com/totoro.gif">'

保存修改后,只需要在浏览器里刷新页面,你就会看到页面上的内容也会随之变化。

2-2

修改 URL 规则

另外,你也可以自由修改传入 app.route 装饰器里的 URL 规则字符串,但要注意以斜线 / 作为开头。比如:

@app.route('/home')
def hello():
    return 'Welcome to My Watchlist!'

保存修改,这时刷新浏览器,则会看到一个 404 错误提示,提示页面未找到(Page Not Found)。这是因为视图函数的 URL 改成了 /home,而我们刷新后访问的地址仍然是旧的 /。如果我们把访问地址改成 http://localhost:5000/home,就会正确看到返回值。

一个视图函数也可以绑定多个 URL,这通过附加多个装饰器实现,比如:

@app.route('/')
@app.route('/index')
@app.route('/home')
def hello():
    return 'Welcome to My Watchlist!'

现在无论是访问 http://localhost:5000/、http://localhost:5000/home 还是 http://localhost:5000/index 都可以看到返回值。

在前面,我们之所以把传入 app.route 装饰器的参数称为 URL 规则,是因为我们也可以在 URL 里定义变量部分。比如下面这个视图函数会处理所有类似 /user/<name> 的请求:

@app.route('/user/<name>')
def user_page():
    return 'User page'

不论你访问 http://localhost:5000/user/greyli,还是 http://localhost:5000/user/peter,抑或是 http://localhost:5000/user/甲,都会触发这个函数。通过下面的方式,我们也可以在视图函数里获取到这个变量值:

@app.route('/user/<name>')
def user_page(name):
    return 'User: %s' % name

修改视图函数名?

最后一个可以修改的部分就是视图函数的名称了。首先,视图函数的名字是自由定义的,和 URL 规则无关。和定义其他函数或变量一样,只需要让它表达出所要处理页面的含义即可。

除此之外,它还有一个重要的作用:作为代表某个路由的端点(endpoint),同时用来生成 URL。对于程序内的 URL,为了避免手写,Flask 提供了一个 url_for 函数来生成 URL,它接受的第一个参数就是端点值,默认为视图函数的名称:

from flask import url_for

...

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

@app.route('/user/<name>')
def user_page(name):
    return 'User: %s' % name

@app.route('/test')
def test_url_for():
	# 下面是一些调用示例:
    print(url_for('hello'))  # 输出:/
    # 注意下面两个调用是如何生成包含 URL 变量的 URL 的
    print(url_for('user_page', name='greyli'))  # 输出:/user/greyli
    print(url_for('user_page', name='peter'))  # 输出:/user/peter
    print(url_for('test_url_for'))  # 输出:/test
    # 下面这个调用传入了多余的关键字参数,它们会被作为查询字符串附加到 URL 后面。
    print(url_for('test_url_for', num=2))  # 输出:/test?num=2
    return 'Test page'

实验过程中编写的代码可以删掉,也可以保留,但记得为根地址返回一行问候,这可是我们这一章的任务。

本章小结

这一章我们为程序编写了主页,同时学习了 Flask 视图函数的基本编写方式。结束前,让我们提交代码:

$ git add .
$ git commit -m "Add minimal home page"
$ git push

为了保持简单,我们统一在章节最后一次提交所有改动。在现实世界里,通常会根据需要分为多个 commit;同样的,这里使用 -m 参数给出简单的提交信息。在现实世界里,你可能需要撰写更完整的提交信息。

提示 你可以在 GitHub 上查看本书示例程序的对应 commit:eca06dc

进阶提示

  • 如果你使用 Python 2.7,为了使程序正常工作,需要在脚本首行添加编码声明 # -*- coding: utf-8-*- ,并在包含中文的字符串前面添加 u 前缀。本书中对于包含中文的字符串均添加了 u 前缀,这在 Python 3 中并不需要。
  • 对于 URL 变量,Flask 还支持在 URL 规则字符串里对变量设置处理器,对变量进行预处理。比如 /user/<int:number> 会将 URL 中的 number 部分处理成整型,同时这个变量值接收传入数字。
  • 因为 Flask 的上下文机制,有一些变量和函数(比如 url_for函数)只能在特定的情况下才能正确执行,比如视图函数内。我们先暂时不用纠结,后面再慢慢了解。
  • 名字以 . 开头的文件默认会被隐藏,执行 ls 命令时会看不到它们,这时你可以使用 ls -f 命令来列出所有文件。
  • 了解 HTTP 基本知识将会有助于你了解 Flask 的工作原理。
  • 阅读文章《互联网是如何工作的》
  • 阅读文章《从HTTP请求 – 响应循环探索Flask的基本工作方式》
  • 如果你是《Flask Web 开发实战》的读者,这部分的进阶内容可以在第 1 章《初识 Flask》和第 2 章《HTTP 和 Flask》找到。
  • 本书主页 & 相关资源索引:http://helloflask.com/tutorial

《Flask 入门教程》第 1 章:准备工作

在通过这本书学习 Flask 开发前,我假设你了解了 Python 和 HTML 的基础知识。你的 Python 版本可以是 2.7,也可以是 3.3 及以上版本。电脑的操作系统可以是 Windows,也可以是 macOS 或 Linux。

安装编辑器和浏览器

对于编辑器来说,每个人都有不同的偏好,你可以自由选择。可以选择功能丰富的IDE(集成开发环境),比如 PyCharm;也可以选择相对轻量的编辑器,比如 Atom 或 Sublime Text。浏览器建议使用 Firefox 或 Chrome

使用命令行

在本书中,你需要使用命令行窗口来执行许多操作。你可以使用 Windows 下的 cmd.exe,或是 macOS 和 Linux 下的终端(Terminal)。下面我们执行一个最简单的 whoami 命令(即 Who Am I?):

$ whoami
greyli

这个命令会打印出当前计算机用户的名称。其他常用的命令还有 cd 命令,用来切换目录(change directory);mkdir 命令,用来创建目录(makdirectory)。在不同的操作系统上,执行某个操作的命令可能会有所不同,在必要的地方,书里会进行提示。

我们先来为我们的程序创建一个文件夹:

$ mkdir watchlist
$ cd watchlist

除非特别说明,从现在开始,本书假设你的工作目录将是在项目的根目录,即 watchlist/ 目录。

为了确保你已经正确安装了 Python,可以执行下面的命令测试是否有报错:

$ python --version
Python 2.7.11

对于 Windows 用户,请使用 cmder(一个基于 ConEmu 实现的终端模拟器) 来代替系统自带的 cmd.exe,或是使用安装 Git for Windows 后(下一节)附带的 Git Bash。cmder 集成了 Git Bash,支持一些在 Linux 或 macOS 下才能使用的命令(程序),比如 ls、cat、nano、ssh 等,这些命令我们在后面会用到。

使用 Git

Git 是一个流行的版本控制工具,我们可以用它来记录程序源码和文件的变动情况,或是在编程时进行多人协作,你可以把它看做一个优雅的代码变动备份工具。

如果你还不熟悉 Git 也没关系,可以先按照书中的命令去做,有时间再去了解原理。现在要做的第一件事就是在你的电脑上安装 Git (可以执行 git --help 命令检查是否已经安装,没有提示“命令未找到(Command not found)”则表示已安装)。

安装后可以在命令行先使用使用下面的命令查看版本,没有报错则表示已正确安装:

$ git --version
git version 2.17.1

为了让 Git 知道你是谁,以便在提交代码到版本仓库的时候进行记录,使用下面的命令设置你的信息:

$ git config --global user.name "Grey Li"  # 替换成你的名字
$ git config --global user.email "withlihui@gmail.com"  # 替换成你的邮箱地址

现在为我们的项目文件夹创建一个 Git 仓库,这会在我们的项目根目录创建一个 .git 文件夹:

$ git init
Initialized empty Git repository in ~/watchlist/.git/

Git 默认会追踪项目文件夹(或者说代码仓库)里所有文件的变化,但是有些无关紧要的文件不需要记录变化,我们在项目根目录创建一个 .gitignore 文件,在文件中写入忽略文件的规则。因为文件内容比较简单,我们直接在命令使用 nano 来创建:

$ nano .gitignore

在 nano 编辑界面写入常见的可忽略文件规则

*.pyc
*~
__pycache__
.DS_Store

使用 Control + O 和 Enter 键保存,然后按下 Control + X 键退出。在后续章节,对于简单的文件,都会使用 nano 创建,这部分操作你也可以使用编辑器来完成。

将程序托管到 GitHub(可选)

这一步是可选的,将程序托管到 GitHub、GitLab 或是 BitBucket 等平台上,可以更方便的备份、协作和部署。这些托管平台作为 Git 服务器,你可以为本地仓库创建远程仓库。

首先要注册一个 GitHub 账户,点击访问注册页面,根据指示完成注册流程。登录备用。

设置 SSH 密钥

一般情况下,当推送本地改动到远程仓库时,需要输入用户名和密码。因为传输通常是通过 SSH 加密,所以可以通过设置 SSH 密钥来省去验证账号的步骤。

首先使用下面的命令检查是否已经创建了 SSH 密钥:

$ cat ~/.ssh/id_rsa.pub

如果显示“No such file or directory”,就使用下面的命令生成 SSH 密钥对,否则复制输出的值备用:

$ ssh-keygen

一路按下 Enter 采用默认值,最后会在用户根目录创建一个 .ssh 文件夹,其中包含两个文件,id_rsa 和 id_rsa.pub,前者是私钥,不能泄露出去,后者是公钥,用于认证身份,就是我们要保存到 GitHub 上的密钥值。再次使用前面提到的命令获得文件内容:

$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3Nza...省略 N 个字符...3aph book@greyli

选中并复制输出的内容,访问 GitHub 的 SSH 设置页面(导航栏头像 – Settings – SSH and GPG keys),点击 New SSH key 按钮,将复制的内容粘贴到 Key 输入框里,再填一个标题,比如“My PC”,最后点击“Add SSH key”按钮保存。

创建远程仓库

访问新建仓库页面(导航栏“+” – New repository),在“Repository name”处填写仓库名称,这里填“watchlist”即可,接着选择仓库类型(公开或私有)等选项,最后点击“Create repository”按钮创建仓库。

因为我们已经提前创建了本地仓库,所以需要指定仓库的远程仓库地址(如果还没有创建,则可以直接将远程仓库克隆到本地):

$ git remote add origin git@github.com:greyli/watchlist.git  # 注意更换地址中的用户名

这会为本地仓库关联一个名为“origin”的远程仓库,注意将仓库地址中的“greyli”换成你的 GitHub 用户名

创建虚拟环境

虚拟环境是独立于 Python 全局环境的 Python 解释器环境,使用它的好处如下:

  • 保持全局环境的干净
  • 指定不同的依赖版本
  • 方便记录和管理依赖

我们将使用 Pipenv 来创建和管理虚拟环境、以及在虚拟环境中安装和卸载依赖包。它集成了 pip 和 virtualenv,可以替代这两个工具的惯常用法。另外,它还集成了 Pipfile,它是新的依赖记录标准,使用 Pipfile 文件记录项目依赖,使用 Pipfile.lock 文件记录固定版本的依赖列表。这两个文件替代了手动通过 requirements.txt 文件记录依赖的方式。我们首先使用 pip 安装 Pipenv,Windows 系统使用下面的命令:

$ pip install pipenv

Linux 和 macOS 使用下面的命令:

$ sudo -H pip install pipenv

使用 Pipenv 创建虚拟环境非常简单,使用 pipenv install 命令即可为当前项目创建一个虚拟环境:

$ pipenv install

这个命令执行的过程包含下面的行为:

  • 为当前目录创建一个 Python 解释器环境,按照 pip、setuptool、virtualenv 等工具库。
  • 如果当前目录有 Pipfile 文件或 requirements.txt 文件,那么从中读取依赖列表并安装。
  • 如果没有发现 Pipfile 文件,就自动创建。

创建虚拟环境后,我们可以使用 pipenv shell 命令来激活虚拟环境,如下所示:

$ pipenv shell

安装 Flask

无论是否已经激活虚拟环境,你都可以使用下面的命令来安装 Flask:

$ pipenv install flask

这会把 Flask 以及相关的一些依赖包安装到对应的虚拟环境,同时 Pipenv 会自动更新依赖文件中。

本章小结

当你进行到这里,就意味这我们已经做好学习和开发Flask程序的全部准备了。使用 git status 命令可以查看当前仓库的文件变动状态:

$ git status

下面让我们将文件改动提交进 Git 仓库,并推送到在 GitHub 上创建的远程仓库:

$ git add .
$ git commit -m "I'm ready!"
$ git push -u origin master # 如果你没有把仓库托管到 GitHub,则跳过这条命令,后面章节亦同

这里最后一行命令添加了 -u 参数,会将推送的目标仓库和分支设为默认值,后续的推送直接使用 git push 命令即可。在 GitHub 上,你可以通过 https://github.com/你的用户名/watchlist 查看你的仓库内容。

提示 你可以在 GitHub 上查看本书示例程序的对应 commit:1b6fe4a

进阶提示

《Flask 入门教程》前言与目录

Flask 是一个使用 Python 语言编写的 Web 框架,它可以让你高效的编写 Web 程序。Web 程序即“网站”或“网页程序”,是指可以通过浏览器进行交互的程序。我们日常使用浏览器访问的豆瓣、知乎、百度等网站都是 Web 程序。

通过这本书,你会学到 Flask 开发的基础知识,并开发出一个简单的 Watchlist(观影清单)程序。在功能上,这个程序可以看做是简化版的 IMDB Watchlist / 豆瓣豆单:你可以添加、删除和修改你收藏的电影信息。

 

本书特点

  • 基于 Flask 最新的 1.0.2 版本

  • 使用一个 Watchlist 程序作为示例

  • 复原完整的开发流程

  • 只提供入门所需的最少信息

  • 优化术语解释,更容易理解

 

阅读方法

本书复原了编写这个 Watchlist 程序的完整流程,包括每一行代码块,每一个需要执行的命令。在阅读时,你需要自己输入每一个代码和命令,检查输出是否和书中一致。在这个过程中,也可以对它进行一些调整。比如,示例程序的界面语言使用了英文,你可以修改为中文或是其他语言。对于页面布局和样式,你也可以自由修改。

在本书的最后,你会把你自己编写的 Watchlist 部署到互联网上,让任何人都可以访问。

 

内容讨论

这本书会以连载的形式发布在知乎专栏 Hello, Flask!上,未来也会考虑发布到 GitBook 或是 GitHub Pages 以便和源码仓库同步更新。如果你有任何疑问和想法,欢迎通过下面的方式提出:

  • 在专栏对应的连载文章下面撰写评论

  • 在源码仓库创建 Issue

 

参与贡献

如果你发现了书中的错误,或是有任何意见或建议,欢迎创建 Issue 反馈或提交 Pull Request 进行修正。对于较大的内容变动,建议先创建 Issue 进行讨论。谢谢!

 

付费支持

本书是开源的,任何人都可以阅读和下载。如果这本书帮到了你,也可以付费支持我。价格为 10 元,访问本书主页查看付款二维码。

 

相关资源

 

目录

  • 前言

  • 准备工作

  • Hello Flask

  • 模板和静态文件

  • 数据库

  • 表单

  • 用户认证

  • 组织你的代码

  • 测试

  • 部署上线

 

迫于生计,接了一个项目,写教程的事情就被耽误了……现在项目基本结束,接下来会在专栏连载教程内容,希望能在 2019 年到来前完成它。

写作一本技术书,能给一个社区带来哪些改变?

写作《Flask Web开发实战》花费了一年多的时间,在这期间,除了编写5个项目实例和写作外,我还花了一部分时间来和书中涉及的Python库(主要是Flask扩展)打交道。这篇文章总结了这本书的写作给整个Flask社区带来的一些改变。

国内第一本Flask书

虽然国内已经有几本书介绍过Flask,但都是顺便介绍,而不是真正意义上的Flask书。因此,这本书可以说是国内第一本Flask书,很高兴能为国内的Flask社区带来这样一本书。希望这本书能够推动更多人了解和使用Flask,进而为社区贡献更多的力量。

完善多个开源项目

在实际介绍和使用某个库时,总会遇到各种各样的问题,主要有两类:

  • Bug
  • 不完善的用法或实现

Bug不用多说,肯定要处理。对于不完善的用法和实现,如果沿用原来的代码,一来不容易读者理解,二来还需要在书中加进很多不必要的内容。所以,我更倾向于让这些代码变得更好一点。

如果把不完善的实现写到书里,我会觉得非常不舒服。比如说,WTForms本身是支持设置内置的错误消息的语言的,我们可以设为中文,而不用手动写错误消息。但是在Flask-WTF中,因为内部重写了相关方法,导致没法在不使用Flask-Babel的情况下使用内置的翻译。问题来了:如果在第4章介绍Flask-Babel和i18n等相关概念的话,这显然需要占据大量篇幅,并不合适,而且这部分内容已经计划加入到第10章。

为此,书的安排没法改动,那么只好调整Flask-WTF的内部实现,所以就有了这个PR。类似的情况还有很多,比如Firefox支持headless模式后,Selenium中却没有提供和Chrome相同的导入接口;Flask-OAuthlib中,传入access令牌只能使用元组或字典类型,而不能使用更直观的字符串变量传入……

下面是一些写作期间提交的开源贡献:

当然,如果加上其他没有太大意义的文档更新,这个列表还可以更长。
 
值得特别说明的是Werkzeug的这个PR,这个PR对应的bug困扰了我很长一段时间。一开始我把问题归咎到python-dotenv身上,还创建了一个PR(#101),但是维护者迟迟没有回复。后来再次研究,发现可以直接在Werkzeug内部解决,于是创建了#1320,然后被lepture用更好的实现取代了(#1321)。不过,在python-dotenv创建的那个PR倒是促成了Pipenv的这个PR(#2386)。

推动发布新的版本

除了让PR被合并,还要让这些项目发布新的release,这样才能在项目源码公开后,让读者可以正常使用。在我的推动下,Flask-OAuthlib、Flask-Whooshee和Flask-Moment都发布了新的版本。

其他的项目,除了Werkzeug以外,大部分都在整个写作的时间跨度发布了新的版本。尤其重要的是Flask的1.0版本。从书一开始写作,我就直接采用了主分支的代码,书中我直接将版本号写为0.13。如果Flask迟迟不发布新版本,那我会陷入一个很尴尬的境地:要么大胆采用主分支代码,但是可能会出现变动,导致书中的内容不可靠;要么采用旧版本,那书出版后可能会很快面临过时的危险。还好,在写作接近尾声的4月末,新版本1.0发布了。

4个Flask扩展

用于集成Bootstrap的扩展Flask-Bootstrap目前的维护状态很糟糕:上一次release是17年1月,上一次合并commit是17年3月,而且已经很长时间不再处理PR和Issue。

原项目还存在很多问题:Jinja语法不标准、内置基模板引入不必要的复杂度、分页宏不支持传入URL片段值。除此之外,让我决定写新的扩展替代它的最主要原因是因为它不支持Bootstrap 4,而我不能接受在书中引入一个这样过时的项目。

为了解决这些问题,只好写了Bootstrap-Flask。最终决定写替代扩展的时候,书已经完成了三分之二,大量旧的内容和源码都要改写。把书稿和源码从Flask-Bootstrap和Bootstrap3迁移到Bootstrap-Flask和Bootstrap4是一个非常痛苦的过程,到现在还记忆犹新……

对于某些空白的领域,我写了三个扩展来简化集成操作:

5个Flask开源项目

在此之前,完善的Flask开源项目屈指可数,除了Flask官方提供的两个示例程序外,就只有Miguel Grinberg的两个程序。而这本书带来了5个相对比较完善、所有依赖都基于最新版本的开源项目(严格来说,第1个程序比较简单,可以排除在外):

PyCon China 2018:自由的Flask

这是我在PyCon China 2018的主题演讲。这个演讲并没有涉及太多复杂的内容,主要的目的还是想推介一下Flask,让更多的人能够了解和使用Flask。

演讲主题简介如下:

作为一个流行的Python Web框架,很多开发者都喜欢Flask的简洁和灵活,并且常常拿它和“笨重”的Django做比较。基于这些特点,我们可以说Flask是自由的,自由的Flask会让你的Web开发更加自由。具体来说,这里的自由表现在很多方面,包括程序功能的扩展、路由的定义、项目结构的组织以及程序模式的设计等。

相关资源:

如何向Jinja宏传递额外参数(*args和**kwargs)?

这段时间有多个读者问关于Jinja宏定义时的参数接受问题。这一点在《Flask Web开发实战》里没有介绍,这篇文章作为一个补充。

一个不符合直觉的设定

在某个晴朗的早晨,你打开电脑,想在你的项目Jinja模板里编写一个宏来简化操作。按照直觉,你可能会像定义Python函数那样来定义宏,传入**kwargs来让它接收任意数量的关键字参数,比如:

{% macro say_hello(**kwargs) %}
     ...
{% endmacro %}

或是传入*args让它接收任意数量的位置参数:

{% macro say_hello(*args) %}
     ...
{% endmacro %}

遗憾的是,上面的调用会分别获得下面的错误信息:

jinja2.exceptions.TemplateSyntaxError: expected token 'name', got '**'
jinja2.exceptions.TemplateSyntaxError: expected token 'name', got '*'

在Jinja宏里接收额外的关键字参数和位置参数

在Jinja中,宏默认会自动接收额外的关键字参数和位置参数,并在宏内部提供kwargsvarargs特殊变量来获取它们。具体来说,在定义宏的时候,不需要进行任何声明。在宏的内部,你可以直接使用kwargs字典来获取额外的关键字参数;同样的,你可以使用varargs元组来获取额外传入的位置参数。

下面是使用kwargs的示例:

{% macro say_hello() %}
    <p>Hello, {{ kwargs['name'] }}!</p>
{% endmacro %}

{# 调用示例 #}
{{ say_hello(name='Grey')}}

你可以把这个字典传递给其他函数,比如url_for():

{% macro nav_link(endpoint, text) %}
    <a href="{{ url_for(endpoint, **kwargs) }}">{{ text }}</a>
{% endmacro %} 

{# 调用示例 #}
{{ nav_link('index', 'Home', foo='value1', bar='value2')}}

下面是使用varargs的示例:

{% macro say_hello() %}
    <p>Hello, {{ varargs[0] }}!</p>
{% endmacro %}

{# 调用示例 #}
{{ say_hello('Grey')}}

提示 在宏内部,如果kwargs字典里没有对应的键,那么会返回空字符串,而不是抛出KeyError异常;如果向varargs元组索引一个超出范围的下标值,也会返回空值,而不会抛出IndexError异常。

隐藏的陷阱

虽然宏自动处理额外传入的关键字参数和位置参数,但是这里有一个隐藏的小陷阱。如果你在调用一个宏的时候传入了额外的关键字参数和位置参数,但是宏的内部并没有使用它们,这时就会出错。比如下面使用kwargs的示例:

{% macro say_hello() %}
    <p>Hello!</p>
{% endmacro %} 

{# 调用示例 #} 
{{ say_hello(name='Grey')}}

调用宏的时候传入了name参数,但是宏内部并没有使用它,这时Jinja会抛出下面的异常:

TypeError: macro 'say_hello' takes no keyword argument 'name'

类似的是位置参数,你会获得下面的异常:

TypeError: macro 'say_hello' takes not more than 1 argument(s)

所以,如果你想让某个宏接收额外的关键字参数或位置参数,你就分别需要在这个宏内部至少调用一次(access)kwargs字典或是varargs元组。一般情况下,你并不需要担心这个问题。

本文隶属于《Flask Web开发实战》番外文章系列。