Flask入门系列(转载)

一、入门系列:

Flask入门系列(一)–Hello World

项目开发中,经常要写一些小系统来辅助,比如监控系统,配置系统等等。用传统的Java写,太笨重了,连PHP都嫌麻烦。一直在寻找一个轻量级的后台框架,学习成本低,维护简单。发现Flask后,我立马被它的轻巧所吸引,它充分发挥了Python语言的优雅和轻便,连Django这样强大的框架在它面前都觉得繁琐。可以说简单就是美。这里我们不讨论到底哪个框架语言更好,只是从简单这个角度出发,Flask绝对是佼佼者。这一系列文章就会给大家展示Flask的轻巧之美。

系列文章

Hello World

程序员的经典学习方法,从Hello World开始。不要忘了,先安装python, pip,然后运行”pip install Flask”,环境就装好了。当然本人还是强烈建议使用virtualenv来安装环境。细节就不多说了,让我们写个Hello World吧:

1

2

3

4

5

6

7

8

9

from flask import Flask

app = Flask(__name__)

 

@app.route('/')

def index():

    return '<h1>Hello World</h1>'

 

if __name__ == '__main__':

    app.run()

一个Web应用的代码就写完了,对,就是这么简单!保存为”hello.py”,打开控制台,到该文件目录下,运行

$ python hello.py

看到”* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)”字样后,就说明服务器启动完成。打开你的浏览器,访问”http://127.0.0.1:5000/”,一个硕大的”Hello World”映入眼帘:)。

简单解释下这段代码

  1. 首先引入了Flask包,并创建一个Web应用的实例”app”

1

2

from flask import Flask

app = Flask(__name__)

这里给的实例名称就是这个python模块名。

  1. 定义路由规则

1

@app.route('/')

这个函数级别的注解指明了当地址是根路径时,就调用下面的函数。可以定义多个路由规则,会在下篇文章里详细介绍。说的高大上些,这里就是MVC中的Contoller。

  1. 处理请求

1

2

def index():

    return '<h1>Hello World</h1>'

当请求的地址符合路由规则时,就会进入该函数。可以说,这里是MVC的Model层。你可以在里面获取请求的request对象,返回的内容就是response。本例中的response就是大标题”Hello World”。

  1. 启动Web服务器

1

2

if __name__ == '__main__':

    app.run()

当本文件为程序入口(也就是用python命令直接执行本文件)时,就会通过”app.run()”启动Web服务器。如果不是程序入口,那么该文件就是一个模块。Web服务器会默认监听本地的5000端口,但不支持远程访问。如果你想支持远程,需要在”run()”方法传入”host=0.0.0.0″,想改变监听端口的话,传入”port=端口号”,你还可以设置调试模式。具体例子如下:

1

2

if __name__ == '__main__':

    app.run(host='0.0.0.0', port=8888, debug=True)

注意,Flask自带的Web服务器主要还是给开发人员调试用的,在生产环境中,你最好是通过WSGI将Flask工程部署到类似Apache或Nginx的服务器上。

本例中的代码可以在这里下载

Flask入门系列(二)–路由

上一篇中,我们用Flask写了一个Hello World程序,让大家领略到了Flask的简洁轻便。从这篇开始我们将对Flask框架的各功能作更详细的介绍,我们首先从路由(Route)开始。

系列文章

路由

从Hello World中,我们了解到URL的路由可以直接写在其要执行的函数上。有人会质疑,这样不是把Model和Controller绑在一起了吗?的确,如果你想灵活的配置Model和Controller,这样是不方便,但是对于轻量级系统来说,灵活配置意义不大,反而写在一块更利于维护。Flask路由规则都是基于Werkzeug的路由模块的,它还提供了很多强大的功能。

带参数的路由

让我们在上一篇Hello World的基础上,加上下面的函数。并运行程序。

1

2

3

@app.route('/hello/<name>')

def hello(name):

    return 'Hello %s' % name

当你在浏览器的地址栏中输入”http://localhost:5000/hello/man”,你将在页面上看到”Hello man”的字样。URL路径中”/hello/”后面的参数被作为”hello()”函数的”name”参数传了进来。
你还可以在URL参数前添加转换器来转换参数类型,我们再来加个函数:

1

2

3

@app.route('/user/<int:user_id>')

def get_user(user_id):

    return 'User ID: %d' % user_id

试下访问”http://localhost:5000/user/man”,你会看到404错误。但是试下”http://localhost:5000/user/123″,页面上就会有”User ID: 123″显示出来。参数类型转换器”int:”帮你控制好了传入参数的类型只能是整形。目前支持的参数类型转换器有:

类型转换器

作用

缺省

字符型,但不能有斜杠

int:

整型

float:

浮点型

path:

字符型,可有斜杠

另外,大家有没有注意到,Flask自带的Web服务器支持热部署。当你修改好文件并保存后,Web服务器自动部署完毕,你无需重新运行程序。

多URL的路由

一个函数上可以设施多个URL路由规则

1

2

3

4

5

6

7

@app.route('/')

@app.route('/hello')

@app.route('/hello/<name>')

def hello(name=None):

    if name is None:

        name = 'World'

    return 'Hello %s' % name

这个例子接受三种URL规则,”/”和”/hello”都不带参数,函数参数”name”值将为空,页面显示”Hello World”;”/hello/“带参数,页面会显示参数”name”的值,效果与上面第一个例子相同。

HTTP请求方法设置

HTTP请求方法常用的有Get, Post, Put, Delete。不熟悉的朋友们可以去度娘查下。Flask路由规则也可以设置请求方法。

1

2

3

4

5

6

7

8

from flask import request

 

@app.route('/login', methods=['GET', 'POST'])

def login():

    if request.method == 'POST':

        return 'This is a POST request'

    else:

        return 'This is a GET request'

当你请求地址”http://localhost:5000/login”,”GET”和”POST”请求会返回不同的内容,其他请求方法则会返回405错误。有没有觉得用Flask来实现Restful风格很方便啊?

URL构建方法

Flask提供了”url_for()”方法来快速获取及构建URL,方法的第一个参数指向函数名(加过”@app.route”注解的函数),后续的参数对应于要构建的URL变量。下面是几个例子:

1

2

3

4

url_for('login')    # 返回/login

url_for('login', id='1')    # 将id作为URL参数,返回/login?id=1

url_for('hello', name='man')    # 适配hello函数的name参数,返回/hello/man

url_for('static', filename='style.css')    # 静态文件地址,返回/static/style.css

静态文件位置

一个Web应用的静态文件包括了JS, CSS, 图片等,Flask的风格是将所有静态文件放在”static”子目录下。并且在代码或模板(下篇会介绍)中,使用”url_for(‘static’)”来获取静态文件目录。上小节中第四个的例子就是通过”url_for()”函数获取”static”目录下的指定文件。如果你想改变这个静态目录的位置,你可以在创建应用时,指定”static_folder”参数。

1

app = Flask(__name__, static_folder='files')

本例中的代码可以在这里下载

Flask入门系列(三)–模板

在第一篇中,我们讲到了Flask中的Controller和Model,但是一个完整的MVC,没有View怎么行?前端代码如果都靠后台拼接而成,就太麻烦了。本篇,我们就介绍下Flask中的View,即模板。

系列文章

模板

Flask的模板功能是基于Jinja2模板引擎实现的。让我们来实现一个例子吧。创建一个新的Flask运行文件(你应该不会忘了怎么写吧),代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

from flask import Flask

from flask import render_template

 

app = Flask(__name__)

 

@app.route('/hello')

@app.route('/hello/<name>')

def hello(name=None):

    return render_template('hello.html', name=name)

 

if __name__ == '__main__':

    app.run(host='0.0.0.0', debug=True)

这段代码同上一篇的多URL路由的例子非常相似,区别就是”hello()”函数并不是直接返回字符串,而是调用了”render_template()”方法来渲染模板。方法的第一个参数”hello.html”指向你想渲染的模板名称,第二个参数”name”是你要传到模板去的变量,变量可以传多个。

那么这个模板”hello.html”在哪儿呢,变量参数又该怎么用呢?别急,接下来我们创建模板文件。在当前目录下,创建一个子目录”templates”(注意,一定要使用这个名字)。然后在”templates”目录下创建文件”hello.html”,内容如下:

1

2

3

4

5

6

7

<!doctype html>

<title>Hello Sample</title>

{% if name %}

  <h1>Hello {{ name }}!</h1>

{% else %}

  <h1>Hello World!</h1>

{% endif %}

这段代码是不是很像HTML?接触过其他模板引擎的朋友们肯定立马秒懂了这段代码。它就是一个HTML模板,根据”name”变量的值,显示不同的内容。变量或表达式由”{{ }}”修饰,而控制语句由”{% %}”修饰,其他的代码,就是我们常见的HTML。

让我们打开浏览器,输入”http://localhost:5000/hello/man”,页面上即显示大标题”Hello man!”。我们再看下页面源代码

1

2

3

4

<!doctype html>

<title>Hello from Flask</title>

 

  <h1>Hello man!</h1>

果然,模板代码进入了”Hello {{ name }}!”分支,而且变量”{{ name }}”被替换为了”man”。Jinja2的模板引擎还有更多强大的功能,包括for循环,过滤器等。模板里也可以直接访问内置对象如request, session等。对于Jinja2的细节,感兴趣的朋友们可以自己去查查。

模板继承

一般我们的网站虽然页面多,但是很多部分是重用的,比如页首,页脚,导航栏之类的。对于每个页面,都要写这些代码,很麻烦。Flask的Jinja2模板支持模板继承功能,省去了这些重复代码。让我们基于上面的例子,在”templates”目录下,创建一个名为”layout.html”的模板:

1

2

3

4

5

6

7

<!doctype html>

<title>Hello Sample</title>

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

<div class="page">

    {% block body %}

    {% endblock %}

</div>

再修改之前的”hello.html”,把原来的代码定义在”block body”中,并在代码一开始”继承”上面的”layout.html”:

1

2

3

4

5

6

7

8

{% extends "layout.html" %}

{% block body %}

{% if name %}

  <h1>Hello {{ name }}!</h1>

{% else %}

  <h1>Hello World!</h1>

{% endif %}

{% endblock %}

打开浏览器,再看下”http://localhost:5000/hello/man”页面的源码。

1

2

3

4

5

6

7

8

<!doctype html>

<title>Hello Sample</title>

<link rel="stylesheet" type="text/css" href="/static/style.css">

<div class="page">

    

  <h1>Hello man!</h1>

 

</div>

你会发现,虽然”render_template()”加载了”hello.html”模板,但是”layout.html”的内容也一起被加载了。而且”hello.html”中的内容被放置在”layout.html”中”{% block body %}”的位置上。形象的说,就是”hello.html”继承了”layout.html”。

HTML自动转义

我们看下下面的代码:

1

2

3

@app.route('/')

def index():

    return '<div>Hello %s</div>' % '<em>Flask</em>'

打开页面,你会看到”Hello Flask”字样,而且”Flask”是斜体的,因为我们加了”em”标签。但有时我们并不想让这些HTML标签自动转义,特别是传递表单参数时,很容易导致HTML注入的漏洞。我们把上面的代码改下,引入”Markup”类:

1

2

3

4

5

6

7

from flask import Flask, Markup

 

app = Flask(__name__)

 

@app.route('/')

def index():

    return Markup('<div>Hello %s</div>') % '<em>Flask</em>'

再次打开页面,”em”标签显示在页面上了。Markup还有很多方法,比如”escape()”呈现HTML标签, “striptags()”去除HTML标签。这里就不一一列举了。

我们会在Flask进阶系列里对模板功能作更详细的介绍。

本文中的示例代码可以在这里下载

Flask入门系列(四)–请求,响应及会话

一个完整的HTTP请求,包括了客户端的请求Request,服务器端的响应Response,会话Session等。一个基本的Web框架一定会提供内建的对象来访问这些信息,Flask当然也不例外。我们来看看在Flask中该怎么使用这些内建对象。

系列文章

Flask内建对象

Flask提供的内建对象常用的有request, session, g,通过request,你还可以获取cookie对象。这些对象不但可以在请求函数中使用,在模板中也可以使用。

请求对象request

引入flask包中的request对象,就可以直接在请求函数中直接使用该对象了。让我们改进下第二篇中的login方法:

1

2

3

4

5

6

7

8

9

10

11

from flask import request

 

@app.route('/login', methods=['POST', 'GET'])

def login():

    if request.method == 'POST':

        if request.form['user'] == 'admin':

            return 'Admin login successfully!'

        else:

            return 'No such user!'

    title = request.args.get('title', 'Default')

    return render_template('login.html', title=title)

第三篇的templates目录下,添加”login.html”文件

1

2

3

4

5

6

7

{% extends "layout.html" %}

{% block body %}

<form name="login" action="/login" method="post">

    Hello {{ title }}, please login by:

    <input type="text" name="user" />

</form>

{% endblock %}

执行上面的例子,结果我就不多描述了。简单解释下,request中”method”变量可以获取当前请求的方法,即”GET”, “POST”, “DELETE”, “PUT”等;”form”变量是一个字典,可以获取Post请求表单中的内容,在上例中,如果提交的表单中不存在”user”项,则会返回一个”KeyError”,你可以不捕获,页面会返回400错误(想避免抛出这”KeyError”,你可以用request.form.get(“user”)来替代)。而”request.args.get()”方法则可以获取Get请求URL中的参数,该函数的第二个参数是默认值,当URL参数不存在时,则返回默认值。request的详细使用可参阅Flask的官方API文档

会话对象session

会话可以用来保存当前请求的一些状态,以便于在请求之前共享信息。我们将上面的python代码改动下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

from flask import request, session

 

@app.route('/login', methods=['POST', 'GET'])

def login():

    if request.method == 'POST':

        if request.form['user'] == 'admin':

            session['user'] = request.form['user']

            return 'Admin login successfully!'

        else:

            return 'No such user!'

    if 'user' in session:

        return 'Hello %s!' % session['user']

    else:

        title = request.args.get('title', 'Default')

        return render_template('login.html', title=title)

 

app.secret_key = '123456'

你可以看到,”admin”登陆成功后,再打开”login”页面就不会出现表单了。session对象的操作就跟一个字典一样。特别提醒,使用session时一定要设置一个密钥”app.secret_key”,如上例。不然你会得到一个运行时错误,内容大致是”RuntimeError: the session is unavailable because no secret key was set”。密钥要尽量复杂,最好使用一个随机数,这样不会有重复,上面的例子不是一个好密钥。

我们顺便写个登出的方法,估计我不放例子,大家也都猜到怎么写,就是清除字典里的键值:

1

2

3

4

5

6

from flask import request, session, redirect, url_for

 

@app.route('/logout')

def logout():

    session.pop('user', None)

    return redirect(url_for('login'))

关于”redirect”方法,我们会在下一篇介绍。

构建响应

在之前的例子中,请求的响应我们都是直接返回字符串内容,或者通过模板来构建响应内容然后返回。其实我们也可以先构建响应对象,设置一些参数(比如响应头)后,再将其返回。修改下上例中的Get请求部分:

1

2

3

4

5

6

7

8

9

10

11

12

13

from flask import request, session, make_response

 

@app.route('/login', methods=['POST', 'GET'])

def login():

    if request.method == 'POST':

        ...

    if 'user' in session:

        ...

    else:

        title = request.args.get('title', 'Default')

        response = make_response(render_template('login.html', title=title), 200)

        response.headers['key'] = 'value'

        return response

打开浏览器调试,在Get请求用户未登录状态下,你会看到响应头中有一个”key”项。”make_response”方法就是用来构建response对象的,第二个参数代表响应状态码,缺省就是200。response对象的详细使用可参阅Flask的官方API文档

Cookie的使用

提到了Session,当然也要介绍Cookie喽,毕竟没有Cookie,Session就根本没法用(不知道为什么?查查去)。Flask中使用Cookie也很简单:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

from flask import request, session, make_response

import time

 

@app.route('/login', methods=['POST', 'GET'])

def login():

    response = None

    if request.method == 'POST':

        if request.form['user'] == 'admin':

            session['user'] = request.form['user']

            response = make_response('Admin login successfully!')

            response.set_cookie('login_time', time.strftime('%Y-%m-%d %H:%M:%S'))

        ...

    else:

        if 'user' in session:

            login_time = request.cookies.get('login_time')

            response = make_response('Hello %s, you logged in on %s' % (session['user'], login_time))

        ...

 

    return response

例子越来越长了,这次我们引入了”time”模块来获取当前系统时间。我们在返回响应时,通过”response.set_cookie()”函数,来设置Cookie项,之后这个项值会被保存在浏览器中。这个函数的第三个参数(max_age)可以设置该Cookie项的有效期,单位是秒,不设的话,在浏览器关闭后,该Cookie项即失效。

在请求中,”request.cookies”对象就是一个保存了浏览器Cookie的字典,使用其”get()”函数就可以获取相应的键值。

全局对象g

“flask.g”是Flask一个全局对象,这里有点容易让人误解,其实”g”的作用范围,就在一个请求(也就是一个线程)里,它不能在多个请求间共享。你可以在”g”对象里保存任何你想保存的内容。一个最常用的例子,就是在进入请求前,保存数据库连接。这个我们会在介绍数据库集成时讲到。

本例中的代码可以在这里下载

Flask入门系列(五)–错误处理及消息闪现

本篇将补充一些Flask的基本功能,包括错误处理,URL重定向,日志功能,还有一个很有趣的消息闪现功能。

系列文章

错误处理

使用”abort()”函数可以直接退出请求,返回错误代码:

1

2

3

4

5

from flask import abort

 

@app.route('/error')

def error():

    abort(404)

上例会显示浏览器的404错误页面。有时候,我们想要在遇到特定错误代码时做些事情,或者重写错误页面,可以用下面的方法:

1

2

3

@app.errorhandler(404)

def page_not_found(error):

    return render_template('404.html'), 404

此时,当再次遇到404错误时,即会调用”page_not_found()”函数,其返回”404.html”的模板页。第二个参数代表错误代码。

不过,在实际开发过程中,我们并不会经常使用”abort()”来退出,常用的错误处理方法一般都是异常的抛出或捕获。装饰器”@app.errorhandler()”除了可以注册错误代码外,还可以注册指定的异常类型。让我们来自定义一个异常:

1

2

3

4

5

6

7

8

9

10

11

12

13

class InvalidUsage(Exception):

    status_code = 400

 

    def __init__(self, message, status_code=400):

        Exception.__init__(self)

        self.message = message

        self.status_code = status_code

 

@app.errorhandler(InvalidUsage)

def invalid_usage(error):

    response = make_response(error.message)

    response.status_code = error.status_code

    return response

我们在上面的代码中定义了一个异常”InvalidUsage”,同时我们通过装饰器”@app.errorhandler()”修饰了函数”invalid_usage()”,装饰器中注册了我们刚定义的异常类。这也就意味着,一但遇到”InvalidUsage”异常被抛出,这个”invalid_usage()”函数就会被调用。写个路由试一试吧。

1

2

3

@app.route('/exception')

def exception():

    raise InvalidUsage('No privilege to access the resource', status_code=403)

URL重定向

重定向”redirect()”函数的使用在上一篇logout的例子中已有出现。作用就是当客户端浏览某个网址时,将其导向到另一个网址。常见的例子,比如用户在未登录时浏览某个需授权的页面,我们将其重定向到登录页要求其登录先。

1

2

3

4

5

6

7

8

from flask import session, redirect

 

@app.route('/')

def index():

    if 'user' in session:

        return 'Hello %s!' % session['user']

    else:

        return redirect(url_for('login'), 302)

“redirect()”的第二个参数时HTTP状态码,可取的值有301, 302, 303, 305和307,默认即302(为什么没有304?留给大家去思考)。

日志

提到错误处理,那一定要说到日志。Flask提供logger对象,其是一个标准的Python Logger类。修改上例中的”exception()”函数:

1

2

3

4

5

@app.route('/exception')

def exception():

    app.logger.debug('Enter exception method')

    app.logger.error('403 error happened')

    raise InvalidUsage('No privilege to access the resource', status_code=403)

执行后,你会在控制台看到日志信息。在debug模式下,日志会默认输出到标准错误stderr中。你可以添加FileHandler来使其输出到日志文件中去,也可以修改日志的记录格式,下面演示一个简单的日志配置代码:

import logging

from logging.handlers import TimedRotatingFileHandler

1

2

3

4

5

6

7

8

9

10

11

12

13

14

server_log = TimedRotatingFileHandler('server.log','D')

server_log.setLevel(logging.DEBUG)

server_log.setFormatter(logging.Formatter(

    '%(asctime)s %(levelname)s: %(message)s'

))

 

error_log = TimedRotatingFileHandler('error.log', 'D')

error_log.setLevel(logging.ERROR)

error_log.setFormatter(logging.Formatter(

    '%(asctime)s: %(message)s [in %(pathname)s:%(lineno)d]'

))

 

app.logger.addHandler(server_log)

app.logger.addHandler(error_log)

上例中,我们在本地目录下创建了两个日志文件,分别是”server.log”记录所有级别日志;”error.log”只记录错误日志。我们分别给两个文件不同的内容格式。另外,我们使用了”TimedRotatingFileHandler”并给了参数”D”,这样日志每天会创建一个新的文件,并将旧文件加日期后缀来归档。

注:执行后会生成server.log和error.log俩文件,访问正常或错误页面这俩均无内容,不知道是本身相关代码有问题,还是访问方式有问题

你还可以将错误信息发送邮件。更详细的日志使用可参阅Python logging官方文档

消息闪现

“Flask Message”是一个很有意思的功能,一般一个操作完成后,我们都希望在页面上闪出一个消息,告诉用户操作的结果。用户看完后,这个消息就不复存在了。Flask提供的”flash”功能就是为了这个。我们还是拿用户登录来举例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

from flask import render_template, request, session, url_for, redirect, flash

 

@app.route('/')

def index():

    if 'user' in session:

        return render_template('hello.html', name=session['user'])

    else:

        return redirect(url_for('login'), 302)

 

@app.route('/login', methods=['POST', 'GET'])

def login():

    if request.method == 'POST':

        session['user'] = request.form['user']

        flash('Login successfully!')

        return redirect(url_for('index'))

    else:

        return '''

        <form name="login" action="/login" method="post">

            Username: <input type="text" name="user" />

        </form>

        '''

上例中,当用户登录成功后,就用”flash()”函数闪出一个消息。让我们找回第三篇中的模板代码,在”layout.html”加上消息显示的部分:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

<!doctype html>

<title>Hello Sample</title>

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

{% with messages = get_flashed_messages() %}

  {% if messages %}

    <ul class="flash">

    {% for message in messages %}

      <li>{{ message }}</li>

    {% endfor %}

    </ul>

  {% endif %}

{% endwith %}

<div class="page">

    {% block body %}

    {% endblock %}

</div>

上例中”get_flashed_messages()”函数就会获取我们在”login()”中通过”flash()”闪出的消息。从代码中我们可以看出,闪出的消息可以有多个。模板”hello.html”不用改。运行下试试。登录成功后,是不是出现了一条”Login successfully”文字?再刷新下页面,你会发现文字消失了。你可以通过CSS来控制这个消息的显示方式。

“flash()”方法的第二个参数是消息类型,可选择的有”message”, “info”, “warning”, “error”。你可以在获取消息时,同时获取消息类型,还可以过滤特定的消息类型。只需设置”get_flashed_messages()”方法的”with_categories”和”category_filter”参数即可。比如,Python部分可改为:

1

2

3

4

5

6

7

8

@app.route('/login', methods=['POST', 'GET'])

def login():

    if request.method == 'POST':

        session['user'] = request.form['user']

        flash('Login successfully!', 'message')

        flash('Login as user: %s.' % request.form['user'], 'info')

        return redirect(url_for('index'))

    ...

layout模板部分可改为:

1

2

3

4

5

6

7

8

9

10

11

...

{% with messages = get_flashed_messages(with_categories=true, category_filter=["message","error"]) %}

  {% if messages %}

    <ul class="flash">

    {% for category, message in messages %}

      <li class="{{ category }}">{{ category }}: {{ message }}</li>

    {% endfor %}

    </ul>

  {% endif %}

{% endwith %}

...

运行结果大家就自己试试吧。

本例中的代码可以在这里下载

Flask入门系列(六)–数据库集成

转眼,我们要进入本系列的最后一篇了。一个基本的Web应用功能其实已经讲完了,现在就让我们引入数据库。简单起见,我们就使用SQLite3作为例子。

系列文章

集成数据库

既然前几篇都用用户登录作为例子,我们这篇就继续讲登录,只是登录的信息会由数据库来验证。让我们先准备SQLite环境吧。

初始化数据库

怎么安装SQLite这里就不说了。我们先写个数据库表的初始化SQL,保存在”init.sql”文件中:

1

2

3

4

5

6

7

8

9

drop table if exists users;

create table users (

  id integer primary key autoincrement,

  name text not null,

  password text not null

);

 

insert into users (name, password) values ('visit', '111');

insert into users (name, password) values ('admin', '123');

运行sqlite3命令,初始化数据库。我们的数据库文件就放在”db”子目录下的”user.db”文件中。

$ sqlite3 db/user.db < init.sql

配置连接参数

创建配置文件"config.py",保存配置信息:

1

2

3

4

#coding:utf8

DATABASE = 'db/user.db'       # 数据库文件位置

DEBUG = True                  # 调试模式

SECRET_KEY = 'secret_key_1'   # 会话密钥

在创建Flask应用时,导入配置信息:

1

2

3

4

5

from flask import Flask

import config

 

app = Flask(__name__)

app.config.from_object('config')

这里也可以用"app.config.from_envvar('FLASK_SETTINGS', silent=True)"方法来导入配置信息,此时程序会读取系统环境变量中"FLASK_SETTINGS"的值,来获取配置文件路径,并加载此文件。如果文件不存在,该语句返回False。参数"silent=True"表示忽略错误。

建立和释放数据库连接

这里要用到请求的上下文装饰器,我们会在进阶系列的第一篇里详细介绍上下文。

1

2

3

4

5

6

7

8

9

@app.before_request

def before_request():

    g.db = sqlite3.connect(app.config['DATABASE'])

 

@app.teardown_request

def teardown_request(exception):

    db = getattr(g, 'db', None)

    if db is not None:

        db.close()

我们在"before_request()"里建立数据库连接,它会在每次请求开始时被调用;并在"teardown_request()"关闭它,它会在每次请求关闭前被调用。

查询数据库

让我们取回上一篇登录部分的代码,"index()"和"logout()"请求不用修改,在"login()"请求中,我们会查询数据库,验证客户端输入的用户名和密码是否存在:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

@app.route('/login', methods=['POST', 'GET'])

def login():

    if request.method == 'POST':

        name = request.form['user']

        passwd = request.form['passwd']

        cursor = g.db.execute('select * from users where name=? and password=?', [name, passwd])

        if cursor.fetchone() is not None:

            session['user'] = name

            flash('Login successfully!')

            return redirect(url_for('index'))

        else:

            flash('No such user!', 'error')

            return redirect(url_for('login'))

    else:

        return render_template('login.html')

模板中加上"login.html"文件

1

2

3

4

5

6

7

8

{% extends "layout.html" %}

{% block body %}

<form name="login" action="/login" method="post">

    Username: <input type="text" name="user" /><br>

    Password: <input type="password" name="passwd" /><br>

    <input type="submit" value="Submit" />

</form>

{% endblock %}

终于一个真正的登录验证写完了(前几篇都是假的),打开浏览器登录下吧。因为比较懒,就不写CSS美化了,受不了这粗糙界面的朋友们就自己调吧。

到目前为止,Flask的基础功能已经介绍完了,是否很想动手写个应用啦?其实Flask还有更强大的高级功能,之后会在进阶系列里介绍。

本例中的代码可以在这里下载

二、模板引擎详解:

Flask中Jinja2模板引擎详解(一)–控制语句和表达式

让我们开启Jinja2模板引擎之旅,虽说标题是Flask中的Jinja2,其实介绍的主要是Jinja2本身,Flask是用来做例子的。如果对Flask不熟悉的朋友们建议将本博客的入门系列先看下。怎么,不知道什么是模板引擎?你可以将模板比作MVC模式中的View视图层,而模板引擎就是用来将模板同业务代码分离,并解析模板语言的程序。你可以耐心地看下本系列文章,就能体会到什么是模板引擎了。

系列文章

回顾

我们在Flask入门系列第三篇中已经介绍了Jinja2模板的基本使用方式,让我们先回顾下,把其中的代码拿过来。
Flask Python代码:

1

2

3

4

5

6

7

8

9

10

11

from flask import Flask,render_template

 

app = Flask(__name__)

 

@app.route('/hello')

@app.route('/hello/<name>')

def hello(name=None):

    return render_template('hello.html', name=name)

 

if __name__ == '__main__':

    app.run(host='0.0.0.0', debug=True)

模板代码:

1

2

3

4

5

6

7

<!doctype html>

<title>Hello Sample</title>

{% if name %}

  <h1>Hello {{ name }}!</h1>

{% else %}

  <h1>Hello World!</h1>

{% endif %}

我们了解到,模板的表达式都是包含在分隔符”{{ }}”内的;控制语句都是包含在分隔符”{% %}”内的;另外,模板也支持注释,都是包含在分隔符”{# #}”内,支持块注释。

表达式

表达式一般有这么几种:

  • 最常用的是变量,由Flask渲染模板时传过来,比如上例中的”name”
  • 也可以是任意一种Python基础类型,比如字符串{{ “Hello” }},用引号括起;或者数值,列表,元祖,字典,布尔值。直接显示基础类型没啥意义,一般配合其他表达式一起用
  • 运算。包括算数运算,如{{ 2 + 3 }};比较运算,如{{ 2 > 1 }};逻辑运算,如{{ False and True }}
  • 过滤器“|”和测试器“is”。这个在后面会介绍
  • 函数调用,如{{ current_time() }};数组下标操作,如{{ arr[1] }}
  • “in”操作符,如{{ 1 in [1,2,3] }}
  • 字符串连接符”~”,作用同Python中的”+”一样,如{{ “Hello ” ~ name ~ “!” }}
  • “if”关键字,如{{ ‘Hi, %s’ % name if name }}。这里的”if”不是条件控制语句。

有没有觉得,这里的表达式很像Python的语法呀?

控制语句

Jinja2的控制语句主要就是条件控制语句if,和循环控制语句for,语法类似于Python。我们可以改动下上节的模板代码:

1

2

3

4

5

6

7

{% if name and name == 'admin'  %}

    <h1>This is admin console</h1>

{% elif name %}

    <h1>Welcome {{ name }}!</h1>

{% else %}

    <h1>Please login</h1>

{% endif %}

上面是一个条件控制语句的例子,注意if控制语句要用”{% endif %}”来结束。模板中无法像代码中一样靠缩进来判断代码块的结束。再来看个循环的例子,我们先改下Python代码中的”hello”函数,让其传两个列表进模板。

1

2

3

4

5

6

def hello(name=None):

    return render_template('hello-1.html', name=name, digits=[1,2,3,4,5],

                           users=[{'name':'John'},

                                  {'name':'Tom', 'hidden':True},

                                  {'name':'Lisa'}

                                  {'name':'Bob'}])

然后在模板中加上:

1

2

3

  {% for digit in digits %}

    {{ digit }}

  {% endfor %}

是不是列表被显示出来了?同if语句一样,for控制语句要用”{% endfor %}”来结束。页面上,每个元素之间会有空格,如果你不希望有空格,就要在”for”语句的最后,和”endfor”语句的最前面各加上一个”-“号。如:

1

2

3

  {% for digit in digits -%}

    {{ digit }}

  {%- endfor %}

现在,你可以看到数字”12345″被一起显示出来了。我们再来看个复杂的循环例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

<dl>

{% for user in users if not user.hidden %}

  {% if loop.first %}

    <div>User List:</div>

  {% endif %}

  <div class="{{ loop.cycle('odd', 'even') }}">

  <dt>User No {{ loop.index }}:</dt>

  <dd>{{ user.name }}</dd>

  </div>

  {% if loop.last %}

    <div>Total Users: {{ loop.length }}</div>

  {% endif %}

{% else %}

  <li>No users found</li>

{% endfor %}

</dl>

这里有三个知识点。首先for循环支持else语句,当待遍历的列表”users”为空或者为None时,就进入else语句。

其次,在for语句后使用if关键字,可以对循环中的项作过滤。本例中,所有hidden属性为True的user都会被过滤掉。

另外,for循环中可以访问Jinja2的循环内置变量。本例中,我们会在第一项前显示标题,最后一项后显示总数,每一项显示序号。另外,奇偶项的HTML div元素会有不同的class。如果我们加入下面的CSS style,就能看到斑马线。

直接在html页面中写入下列内容即可。

不过也可单独写入一个css文件路,不过需要引用才能生效

1

2

3

4

5

<style type="text/css">

    .odd {

        background-color: #BDF;

    }

</style>

Jinja2的循环内置变量主要有以下几个:

变量

内容

loop.index

循环迭代计数(从1开始)

loop.index0

循环迭代计数(从0开始)

loop.revindex

循环迭代倒序计数(从len开始,到1结束)

loop.revindex0

循环迭代倒序计数(从len-1开始,到0结束)

loop.first

是否为循环的第一个元素

loop.last

是否为循环的最后一个元素

loop.length

循环序列中元素的个数

loop.cycle

在给定的序列中轮循,如上例在”odd”和”even”两个值间轮循

loop.depth

当前循环在递归中的层级(从1开始)

loop.depth0

当前循环在递归中的层级(从0开始)

关于递归循环,大家看看下面的例子,我就不多介绍了:

1

2

3

4

5

6

7

8

{% for item in [[1,2],[3,4,5]] recursive %}

  Depth: {{ loop.depth }}

  {% if item[0] %}

    {{ loop(item) }}

  {% else %}

    Number: {{ item }} ;

  {% endif %}

{% endfor %}

另外,如果你启用了”jinja2.ext.loopcontrols”扩展的话,你还可以在循环中使用”{% break %}”和”{% continue %}”来控制循环执行。关于Jinja2的扩展,我们会在本系列的第七篇第八篇中介绍。

其他常用语句

忽略模板语法

有时候,我们在页面上就是要显示”{{ }}”这样的符号怎么办?Jinja2提供了”raw”语句来忽略所有模板语法。

1

2

3

4

5

6

7

{% raw %}

    <ul>

    {% for item in items %}

        <li>{{ item }}</li>

    {% endfor %}

    </ul>

{% endraw %}

自动转义

我们将本文一开始的Flask代码”hello()”方法改动下:

1

2

3

4

5

6

@app.route('/hello')

@app.route('/hello/<name>')

def hello(name=None):

    if name is None:

        name = '<em>World</em>'

    return render_template('hello.html', name=name)

此时,访问”http://localhost:5000/hello”,页面上会显示”Welcome <em>World</em>!”,也就是这个HTML标签”<em>”被自动转义了。正如我们曾经提到过的,Flask会对”.html”, “.htm”, “.xml”, “.xhtml”这四种类型的模板文件开启HTML格式自动转义。这样也可以防止HTML语法注入。如果我们不想被转义怎么办?

1

2

3

{% autoescape false %}

  <h1>Hello {{ name }}!</h1>

{% endautoescape %}

将”autoescape”开关设为”false”即可,反之,设为”true”即开启自动转义。使用”autoescape”开关前要启用”jinja2.ext.autoescape”扩展,在Flask框架中,这个扩展默认已启用。

赋值

使用”set”关键字给变量赋值:

1

{% set items = [[1,2],[3,4,5]] %}

  用法可以参考下面的with语句

with语句

类似于Python中的”with”关键字,它可以限制with语句块内对象的作用域:

1

2

3

4

5

{% with foo = 1 %}

    {% set bar = 2 %}

    {{ foo + bar }}

{% endwith %}

{# foo and bar are not visible here #}

使用”with”关键字前要启用”jinja2.ext.with_”扩展,在Flask框架中,这个扩展默认已启用。

执行表达式

1

2

3

4

{% with arr = ['Sunny'] %}

  {{ arr.append('Rainy') }}

  {{ arr }}

{% endwith %}

看上面这段代码,我们想执行列表的”append”操作,这时使用”{{ arr.append(‘Rainy’) }}”页面会输出”None”,换成”{% %}”来执行,程序会报错,因为这是个表达式,不是语句。那怎么办?我们可以启用”jinja2.ext.do”扩展。然后在模板中执行”do”语句即可:

1

2

3

4

{% with arr = ['Sunny'] %}

  {% do arr.append('Rainy') %}

  {{ arr }}

{% endwith %}

默认jinja2没开启这个,需要启用

在py文件中添加这个:app.jinja_env.add_extension("jinja2.ext.do"),表示启用jinja2的do扩展,然后就能在html文件中使用上述语句了

本篇中的示例代码可以在这里下载

Flask中Jinja2模板引擎详解(二)–上下文环境

Flask每个请求都有生命周期,在生命周期内请求有其上下文环境Request Context。我们在Flask进阶系列第一篇中有详细介绍。作为在请求中渲染的模板,自然也在请求的生命周期内,所以Flask应用中的模板可以使用到请求上下文中的环境变量,及一些辅助函数。本文就会介绍下这些变量和函数。

系列文章

标准上下文变量和函数

请求对象request

request对象可以用来获取请求的方法”request.method”,表单”request.form”,请求的参数”request.args”,请求地址”request.url”等。它本身是一个字典。在模板中,你一样可以获取这些内容,只要用表达式符号”{{ }}”括起来即可。

1

<p>{{ request.url }}</p>

在没有请求上下文的环境中,这个对象不可用。

会话对象session

session对象可以用来获取当前会话中保存的状态,它本身是一个字典。在模板中,你可以用表达式符号”{{ }}”来获取这个对象。

Flask代码如下,别忘了设置会话密钥哦:

注:需要导入from flask import Flask,render_template,session

1

2

3

4

5

6

@app.route('/')

def index():

    session['user'] = 'guest'

    return render_template('hello.html')

 

app.secret_key = '123456'

模板代码:

1

  <p>User: {{ session.user }}</p>

在没有请求上下文的环境中,这个对象不可用。

全局对象g

全局变量g,用来保存请求中会用到全局内容,比如数据库连接。模板中也可以访问。

Flask代码:

注:需要导入from flask import Flask,render_template,g

1

2

3

4

@app.route('/')

def index():

    g.db = 'mysql'

    return render_template('hello.html')

模板代码:

1

  <p>DB: {{ g.db }}</p>

g对象是保存在应用上下文环境中的,也只在一个请求生命周期内有效。在没有应用上下文的环境中,这个对象不可用。

Flask配置对象config

Flask入门系列第六篇中,我们曾介绍过如何将配置信息导入Flask应用中。导入的配置信息,就保存在”app.config”对象中。这个配置对象在模板中也可以访问。

1

  <p>Host: {{ config.DEBUG }}</p>

结果返回:Host:True,表示的是开启了调试模式

“config”是全局对象,离开了请求生命周期也可以访问。

url_for()函数

url_for()函数可以用来快速获取及构建URL,Flask也将此函数引入到了模板中,比如下面的代码,就可以获取静态目录下的”style.css”文件。

1

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

该函数是全局的,离开了请求生命周期也可以调用。

get_flashed_messages()函数

get_flashed_messages()函数是用来获取消息闪现的。具体的示例我们在入门系列第五篇中已经讲过,这里就不再赘述了。这也是一个全局可使用的函数。

自定义上下文变量和函数

自定义变量

除了Flask提供的标准上下文变量和函数,我们还可以自己定义。下面我们就来先定义一个上下文变量,在Flask应用代码中,加入下面的函数:

1

2

3

4

5

from flask import current_app

 

@app.context_processor

def appinfo():

    return dict(appname=current_app.name)

函数返回的是一个字典,里面有一个属性”appname”,值为当前应用的名称。我们曾经介绍过,这里的”current_app”对象是一个定义在应用上下文中的代理。函数用”@app.context_processor”装饰器修饰,它是一个上下文处理器,它的作用是在模板被渲染前运行其所修饰的函数,并将函数返回的字典导入到模板上下文环境中,与模板上下文合并。然后,在模板中”appname”就如同上节介绍的”request”, “session”一样,成为了可访问的上下文对象。我们可以在模板中将其输出:

1

  <p>Current App is: {{ appname }}</p>

自定义函数

同理我们可以自定义上下文函数,只需将上例中返回字典的属性指向一个函数即可,下面我们就来定义一个上下文函数来获取系统当前时间:

1

2

3

4

5

6

7

import time

 

@app.context_processor

def get_current_time():

    def get_time(timeFormat="%b %d, %Y - %H:%M:%S"):

        return time.strftime(timeFormat)

    return dict(current_time=get_time)

我们可以试下在模板中将其输出:

1

2

  <p>Current Time is: {{ current_time() }}</p>

  <p>Current Day is: {{ current_time("%Y-%m-%d") }}</p>

上下文处理器可以修饰多个函数,也就是我们可以定义多个上下文环境变量和函数。

本篇中的示例代码可以在这里下载

Flask中Jinja2模板引擎详解(三)–过滤器

我所了解的模板引擎大部分都会提供类似Jinja2过滤器的功能,只不过叫法不同罢了。比如PHP Smarty中的Modifiers(变量调节器或修饰器),FreeMarker中的Build-ins(内建函数),连AngularJS这样的前端框架也提供了Filter过滤器。它们都是用来在变量被显示或使用前,对其作转换处理的。可以把它认为是一种转换函数,输入的参数就是其所修饰的变量,返回的就是变量转换后的值。

系列文章

过滤器使用

回到我们第一篇开篇的例子,我们在模板中对变量name作如下处理:

1

  <h1>Hello {{ name | upper }}!</h1>

你会看到name的输出都变成大写了。这就是过滤器,只需在待过滤的变量后面加上”|”符号,再加上过滤器名称,就可以对该变量作过滤转换。上面例子就是转换成全大写字母。过滤器可以连续使用:

1

  <h1>Hello {{ name | upper | truncate(3, True) }}!</h1>

现在name变量不但被转换为大写,而且当它的长度大于3后,只显示前3个字符,后面默认用”…”显示。过滤器”truncate”有3个参数,第一个是字符截取长度;第二个决定是否保留截取后的子串,默认是False,也就是当字符大于3后,只显示”…”,截取部分也不出现;第三个是省略符号,默认是”…”。

其实从例子中我们可以猜到,过滤器本质上就是一个转换函数,它的第一个参数就是待过滤的变量,在模板中使用时可以省略去。如果它有第二个参数,模板中就必须传进去。

内置过滤器 Builtin Filters

Jinja2模板引擎提供了丰富的内置过滤器。这里介绍几个常用的。

字符串操作

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

{# 当变量未定义时,显示默认字符串,可以缩写为d #}

<p>{{ name | default('No name', true) }}</p>

 

{# 单词首字母大写 #}

<p>{{ 'hello' | capitalize }}</p>

 

{# 单词全小写 #}

<p>{{ 'XML' | lower }}</p>

 

{# 去除字符串前后的空白字符 #}

<p>{{ '  hello  ' | trim }}</p>

 

{# 字符串反转,返回"olleh" #}

<p>{{ 'hello' | reverse }}</p>

 

{# 格式化输出,返回"Number is 2" #}

<p>{{ '%s is %d' | format("Number", 2) }}</p>

 

{# 关闭HTML自动转义 #}

<p>{{ '<em>name</em>' | safe }}</p>

 

{% autoescape false %}

{# HTML转义,即使autoescape关了也转义,可以缩写为e #}

<p>{{ '<em>name</em>' | escape }}</p>

{% endautoescape %}

数值操作

1

2

3

4

5

6

7

8

{# 四舍五入取整,返回13.0 #}

<p>{{ 12.8888 | round }}</p>

 

 {# 四舍五入向下截取到小数点后2位,返回12.89 #}

<p>{{ 12.8888 | round(2) }}</p>

 

{# 向下截取到小数点后2位,返回12.88 #}

<p>{{ 12.8888 | round(2, 'floor') }}</p>

 

{# 绝对值,返回12 #}

<p>{{ -12 | abs }}</p>

列表操作

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

{# 取第一个元素 #}

<p>{{ [1,2,3,4,5] | first }}</p>

 

{# 取最后一个元素 #}

<p>{{ [1,2,3,4,5] | last }}</p>

 

{# 返回列表长度,可以写为count #}

<p>{{ [1,2,3,4,5] | length }}</p>

 

{# 列表求和 #}

<p>{{ [1,2,3,4,5] | sum }}</p>

 

{# 列表排序,默认为升序 #}

<p>{{ [3,2,1,5,4] | sort }}</p>

 

{# 合并为字符串,返回"1 | 2 | 3 | 4 | 5" #}

<p>{{ [1,2,3,4,5] | join(' | ') }}</p>

 

{# 列表中所有元素都全大写。这里可以用upper,lower,但capitalize无效 #}

<p>{{ ['tom','bob','ada'] | upper }}</p>

字典列表操作

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

{% set users=[{'name':'Tom','gender':'M','age':20},

              {'name':'John','gender':'M','age':18},

              {'name':'Mary','gender':'F','age':24},

              {'name':'Bob','gender':'M','age':31},

              {'name':'Lisa','gender':'F','age':19}]

%}

 

 

{# 按指定字段排序,这里设reverse为true使其按降序排 #}

<ul>

{% for user in users | sort(attribute='age', reverse=true) %}

     <li>{{ user.name }}, {{ user.age }}</li>

{% endfor %}

</ul>

 

{# 列表分组,每组是一个子列表,组名就是分组项的值 #}

<ul>

{% for group in users|groupby('gender') %}

    <li>{{ group.grouper }}<ul>

    {% for user in group.list %}

        <li>{{ user.name }}</li>

    {% endfor %}</ul></li>

{% endfor %}

</ul>

 

{# 取字典中的某一项组成列表,再将其连接起来 #}

<p>{{ users | map(attribute='name') | join(', ') }}</p>

更全的内置过滤器介绍可以从Jinja2的官方文档中找到。

Flask内置过滤器

Flask提供了一个内置过滤器”tojson”,它的作用是将变量输出为JSON字符串。这个在配合Javascript使用时非常有用。我们延用上节字典列表操作中定义的”users”变量

1

2

3

4

<script type="text/javascript">

var users = {{ users | tojson | safe }};

console.log(users[0].name);

</script>

注意,这里要避免HTML自动转义,所以加上safe过滤器。

注:暂不知道具体用法

语句块过滤

Jinja2还可以对整块的语句使用过滤器。

1

2

3

{% filter upper %}

    This is a Flask Jinja2 introduction.

{% endfilter %}

不过上述这种场景不经常用到。

自定义过滤器

内置的过滤器不满足需求怎么办?自己写呗。过滤器说白了就是一个函数嘛,我们马上就来写一个。回到Flask应用代码中:

注:这个很有用

1

2

def double_step_filter(l):

    return l[::2]

我们定义了一个”double_step_filter”函数,返回输入列表的偶数位元素(第0位,第2位,..)。怎么把它加到模板中当过滤器用呢?Flask应用对象提供了”add_template_filter”方法来帮我们实现。我们加入下面的代码:

1

app.add_template_filter(double_step_filter, 'double_step')

函数的第一个参数是过滤器函数,第二个参数是过滤器名称。然后,我们就可以愉快地在模板中使用这个叫”double_step”的过滤器了:

1

2

{# 返回[1,3,5] #}

<p>{{ [1,2,3,4,5] | double_step }}</p>

Flask还提供了添加过滤器的装饰器”template_filter”,使用起来更简单。下面的代码就添加了一个取子列表的过滤器。装饰器的参数定义了该过滤器的名称”sub”。

1

2

3

@app.template_filter('sub')

def sub(l, start, end):

    return l[start:end]

我们在模板中可以这样使用它:

1

2

{# 返回[2,3,4] #}

<p>{{ [1,2,3,4,5] | sub(1,4) }}</p>

Flask添加过滤器的方法实际上是封装了对Jinja2环境变量的操作。上述添加”sub”过滤器的方法,等同于下面的代码。

1

app.jinja_env.filters['sub'] = sub

我们在Flask应用中,不建议直接访问Jinja2的环境变量。如果离开Flask环境直接使用Jinja2的话,就可以通过”jinja2.Environment”来获取环境变量,并添加过滤器。

本篇中的示例代码可以在这里下载

Flask中Jinja2模板引擎详解(四)–测试器

Jinja2中的测试器Test和过滤器非常相似,区别是测试器总是返回一个布尔值,它可以用来测试一个变量或者表达式,你需要使用”is”关键字来进行测试。测试器一般都是跟着if控制语句一起使用的。下面我们就来深入了解下这个测试器。

系列文章

测试器使用

再次取回第一篇开篇的例子,我们在模板中对变量name作如下判断:

1

2

3

{% if name is lower %}

  <h2>"{{ name }}" are all lower case.</h2>

{% endif %}

当name变量中的字母都是小写时,这段文字就会显示。这就是测试器,在if语句中,变量或表达式的后面加上is关键字,再加上测试器名称,就可以对该变量或表达式作测试,并根据其测试结果的真或假,来决定是否进入if语句块。测试器也可以有参数,用括号括起。当其只有一个参数时,可以省去括号。

1

2

3

{% if 6 is divisibleby 3 %}

  <h2>"divisibleby" test pass</h2>

{% endif %}

上例中,测试器”divisibleby”可以判断其所接收的变量是否可以被其参数整除。因为它只有一个参数,我们就可以用空格来分隔测试器和其参数。上面的调用同”divisibleby(3)”效果一致。测试器也可以配合not关键字一起使用:

1

2

3

{% if 6 is not divisibleby(4) %}

  <h2>"not divisibleby" test pass</h2>

{% endif %}

显然测试器本质上也是一个函数,它的第一个参数就是待测试的变量,在模板中使用时可以省略去。如果它有第二个参数,模板中就必须传进去。测试器函数返回的必须是一个布尔值,这样才可以用来给if语句作判断。

内置测试器 Builtin Tests

同过滤器一样,Jinja2模板引擎提供了丰富的内置测试器。这里介绍几个常用的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

{# 检查变量是否被定义,也可以用undefined检查是否未被定义 #}

{% if name is defined %}

    <p>Name is: {{ name }}</p>

{% endif %}

 

{# 检查是否所有字符都是大写 #}

{% if name is upper %}

  <h2>"{{ name }}" are all upper case.</h2>

{% endif %}

 

{# 检查变量是否为空 #}

{% if name is none %}

  <h2>Variable is none.</h2>

{% endif %}

 

{# 检查变量是否为字符串,也可以用number检查是否为数值 #}

{% if name is string %}

  <h2>{{ name }} is a string.</h2>

{% endif %}

 

{# 检查数值是否是偶数,也可以用odd检查是否为奇数 #}

{% if 2 is even %}

  <h2>Variable is an even number.</h2>

{% endif %}

 

{# 检查变量是否可被迭代循环,也可以用sequence检查是否是序列 #}

{% if [1,2,3] is iterable %}

  <h2>Variable is iterable.</h2>

{% endif %}

 

{# 检查变量是否是字典 #}

{% if {'name':'test'} is mapping %}

  <h2>Variable is dict.</h2>

{% endif %}

更全的内置测试器介绍可以从Jinja2的官方文档中找到。

自定义测试器

如果内置测试器不满足需求,我们就来自己写一个。写法很类似于过滤器,先在Flask应用代码中定义测试器函数,然后通过”add_template_test”将其添加为模板测试器:

1

2

3

4

import re

def has_number(str):

    return re.match(r'.*\d+', str)

app.add_template_test(has_number,'contain_number')

我们定义了一个”has_number”函数,用正则来判断输入参数是否包含数字。然后调用”app.add_template_test”方法,第一个参数是测试器函数,第二个是测试器名称。之后,我们就可以在模板中使用”contain_number”测试器了:

1

2

3

{% if name is contain_number %}

  <h2>"{{ name }}" contains number.</h2>

{% endif %}

同过滤器一样,Flask提供了添加测试器的装饰器”template_test”。下面的代码就添加了一个判断字符串是否以某一子串结尾的测试器。装饰器的参数定义了该测试器的名称”end_with”:

1

2

3

@app.template_test('end_with')

def end_with(str, suffix):

    return str.lower().endswith(suffix.lower())

我们在模板中可以这样使用它:

1

2

3

{% if name is end_with "me" %}

  <h2>"{{ name }}" ends with "me".</h2>

{% endif %}

Flask添加测试器的方法是封装了对Jinja2环境变量的操作。上述添加”end_with”测试器的方法,等同于下面的代码。

1

app.jinja_env.tests['end_with'] = " end_with "

我们在Flask应用中,不建议直接访问Jinja2的环境变量。如果离开Flask环境直接使用Jinja2的话,就可以通过”jinja2.Environment”来获取环境变量,并添加测试器。

本文中的示例代码可以在这里下载

Flask中Jinja2模板引擎详解(五)–全局函数

介绍完了过滤器测试器,接下来要讲的是Jinja2模板引擎的另一个辅助函数功能,即全局函数Global Functions。如果说过滤器是一个变量转换函数,测试器是一个返回布尔值的函数,那全局函数就可以是任意函数。可以在任一场景使用,没有输入和输出值的限制。本篇我们就来阐述下这个全局函数。

系列文章

全局函数使用

还是取出第一篇开篇的代码,我们在模板中加入下面的代码:

1

2

3

4

5

<ul>

{% for num in range(10, 20, 2) %}

    <li>Number is "{{ num }}"</li>

{% endfor %}

</ul>

页面上会显示”10,12,14,16,18″5个列表项。全局函数”range()”的作用同Python里的一样,返回指定范围内的数值序列。三个参数分别是开始值,结束值(不包含),间隔。如果只传两个参数,那间隔默认为1;如果只传1个参数,那开始值默认为0。

由此可见,全局函数如同其名字一样,就是全局范围内可以被使用的函数。其同第二篇介绍的上下文环境中定义的函数不同,没有请求生命周期的限制。

内置全局函数

演示几个常用的内置全局函数。

  • dict()函数,方便生成字典型变量

1

2

3

4

{% set user = dict(name='Mike',age=15) %}

<p>{{ user | tojson | safe }}</p>

 

{# 显示 '{"age": 15, "name": "Mike"}' #}

  • joiner()函数,神奇的辅助函数。它可以初始化为一个分隔符,然后第一次调用时返回空字符串,以后再调用则返回分隔符。对分隔循环中的内容很有帮助

1

2

3

4

5

6

{% set sep = joiner("|") %}

{% for val in range(5) %}

    {{ sep() }} <span>{{ val }}</span>

{% endfor %}

 

{# 显示 "0 | 1 | 2 | 3 | 4" #}

  • cycler()函数,作用同第一篇介绍的循环内置变量”loop.cycle”类似,在给定的序列中轮循

1

2

3

4

5

6

7

{% set cycle = cycler('odd', 'even') %}

<ul>

{% for num in range(10, 20, 2) %}

    <li class="{{ cycle.next() }}">Number is "{{ num }}",

    next line is "{{ cycle.current }}" line.</li>

{% endfor %}

</ul>

基于上一节的例子,加上”cycler()”函数的使用,你会发现列表项<li>的”class”在”odd”和”even”两个值间轮循。加入第一篇中的CSS style,就可以看到斑马线了。

“cycler()”函数返回的对象可以做如下操作

    • next(),返回当前值,并往下一个值轮循
    • reset(),重置为第一个值
    • current,当前轮循到的值

更全的内置全局函数介绍可以从Jinja2的官方文档中找到。

自定义全局函数

我们当然也可以写自己的全局函数,方法同之前介绍的过滤器啦,测试器啦都很类似。就是将Flask应用代码中定义的函数,通过”add_template_global”将其传入模板即可:

1

2

3

4

5

6

7

8

9

import re

def accept_pattern(pattern_str):

    pattern = re.compile(pattern_str, re.S)

    def search(content):

        return pattern.findall(content)

 

    return dict(search=search, current_pattern=pattern_str)

 

app.add_template_global(accept_pattern, 'accept_pattern')

上例中的accept_pattern函数会先预编译一个正则,然后返回的字典中包含一个查询函数”search”,之后调用”search”函数就可以用编译好的正则来搜索内容了。”app.add_template_global”方法的第一个参数是自定义的全局函数,第二个是全局函数名称。现在,让我们在模板中使用”accept_pattern”全局函数:

1

2

3

4

5

6

7

8

9

{% with pattern = accept_pattern("<li>(.*?)</li>") %}

  {% set founds = pattern.search("<li>Tom</li><li>Bob</li>") %}

  <ul>

  {% for item in founds %}

    <li>Found: {{ item }}</li>

  {% endfor %}

  </ul>

  <p>Current Pattern: {{ pattern.current_pattern }}</p>

{% endwith %}

“Tom”和”Bob”被抽取出来了,很牛掰的样子。你还可以根据需要在”accept_pattern”的返回字典里定义更多的方法。

Flask同样提供了添加全局函数的装饰器”template_global”,以方便全局函数的添加。我们来用它将第二篇中取系统当前时间的函数”current_time”定义为全局函数。

1

2

3

4

import time

@app.template_global('end_with')

def current_time(timeFormat="%b %d, %Y - %H:%M:%S"):

    return time.strftime(timeFormat)

第二篇中的一样,我们在模板中可以这样使用它:

1

2

<p>Current Time is: {{ current_time() }}</p>

<p>Current Day is: {{ current_time("%Y-%m-%d") }}</p>

Flask添加全局函数的方法是封装了对Jinja2环境变量的操作。上述添加”current_time”全局函数的方法,等同于下面的代码。

1

app.jinja_env.globals['current_time'] = current_time

我们在Flask应用中,不建议直接访问Jinja2的环境变量。如果离开Flask环境直接使用Jinja2的话,就可以通过”jinja2.Environment”来获取环境变量,并添加全局函数。

本文中的示例代码可以在这里下载

Flask中Jinja2模板引擎详解(六)–块和宏

考虑到模板代码的重用,Jinja2提供了块 (Block)和宏 (Macro)的功能。块功能有些类似于C语言中的宏,原理就是代码替换;而宏的功能有些类似于函数,可以传入参数。本篇我们就来介绍下块和宏的用法。

系列文章

块 (Block)

Flask入门系列第三篇介绍模板时,我们提到了模板的继承。我们在子模板的开头定义了”{% extend ‘parent.html’ %}”语句来声明继承,此后在子模板中由”{% block block_name %}”和”{% endblock %}”所包括的语句块,将会替换父模板中同样由”{% block block_name %}”和”{% endblock %}”所包括的部分。

这就是块的功能,模板语句的替换。这里要注意几个点:

  1. 模板不支持多继承,也就是子模板中定义的块,不可能同时被两个父模板替换。
  2. 模板中不能定义多个同名的块,子模板和父模板都不行,因为这样无法知道要替换哪一个部分的内容。

另外,我们建议在”endblock”关键字后也加上块名,比如”{% endblock block_name %}”。虽然对程序没什么作用,但是当有多个块嵌套时,可读性好很多。

保留父模板块的内容

如果父模板中的块里有内容不想被子模板替换怎么办?我们可以使用”super( )”方法。基于Flask入门系列第三篇的例子,我们将父模板”layout.html”改为:

1

2

3

4

5

6

7

8

9

10

11

12

13

<!doctype html>

<head>

    {% block head %}

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

    <title>{% block title %}{% endblock %}</title>

    {% endblock %}

</head>

<body>

    <div class="page">

    {% block body %}

    {% endblock %}

    </div>

</body>

并在子模板里,加上”head”块和”title”块:

1

2

3

4

5

6

7

{% block title %}Block Sample{% endblock %}

{% block head %}

    {{ super() }}

    <style type="text/css">

        h1 { color: #336699; }

    </style>

{% endblock %}

父模板同子模板的”head”块中都有内容。运行后,你可以看到,父模板中的”head”块语句先被加载,而后是子模板中的”head”块语句。这就得益于我们在子模板的”head”块中加上了表达式”{{ super( ) }}”。效果有点像Java中的”super( )”吧。

 

块内语句的作用域

默认情况下,块内语句是无法访问块外作用域中的变量。比如我们在”layout.html”加上一个循环:

1

2

3

    {% for item in range(5) %}

        <li>{% block list %}{% endblock %}</li>

    {% endfor %}

然后在子模板中定义”list”块并访问循环中的”item”变量:

1

2

3

{% block list %}

    <em>{{ item }}</em>

{% endblock %}

你会发现页面上什么数字也没显示。如果你想在块内访问这个块外的变量,你就需要在块声明时添加”scoped”关键字。比如我们在”layout.html”中这样声明”list”块即可:

1

2

3

    {% for item in range(5) %}

        <li>{% block list scoped %}{% endblock %}</li>

    {% endfor %}

宏 (Macro)

文章的开头我们就讲过,Jinja2的宏功能有些类似于传统程序语言中的函数,既然是函数就有其声明和调用两个部分。那就让我们先声明一个宏:

1

2

3

{% macro input(name, type='text', value='') -%}

    <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}">

{%- endmacro %}

代码中,宏的名称就是”input”,它有三个参数分别是”name”, “type”和”value”,后两个参数有默认值。现在,让我们使用表达式来调用这个宏:

1

2

3

<p>{{ input('username', value='user') }}</p>

<p>{{ input('password', 'password') }}</p>

<p>{{ input('submit', 'submit', 'Submit') }}</p>

大家可以在页面上看到一个文本输入框,一个密码输入框及一个提交按钮。是不是同函数一样啊?其实它还有比函数更丰富的功能,之后我们来介绍。

访问调用者内容

我们先来创建个宏”list_users”:

1

2

3

4

5

6

7

8

{% macro list_users(users) -%}

  <table>

    <tr><th>Name</th><th>Action</th></tr>

    {%- for user in users %}

      <tr><td>{{ user.name |e }}</td>{{ caller() }}</tr>

    {%- endfor %}

  </table>

{%- endmacro %}

宏的作用就是将用户列表显示在表格里,表格每一行用户名称后面调用了”{{ caller( ) }}”方法,这个有什么用呢?先别急,我们来写调用者的代码:

1

2

3

4

5

6

7

8

{% set users=[{'name':'Tom','gender':'M','age':20},

              {'name':'John','gender':'M','age':18},

              {'name':'Mary','gender':'F','age':24}]

%}

 

{% call list_users(users) %}

    <td><input name="delete" type="button" value="Delete"></td>

{% endcall %}

与上例不同,这里我们使用了”{% call %}”语句块来调用宏,语句块中包括了一段生成”Delete”按钮的代码。运行下试试,你会发现每个用户名后面都出现了”Delete”按钮,也就是”{{ caller( ) }}”部分被调用者”{% call %}”语句块内部的内容替代了。不明觉厉吧!其实吧,这个跟函数传个参数进去没啥大区别,个人觉得,主要是有些时候HTML语句太复杂(如上例),不方便写在调用参数上,所以就写在”{% call %}”语句块里了。

Jinja2的宏不但能访问调用者语句块的内容,还能给调用者传递参数。嚯,这又是个什么鬼?我们来扩展下上面的例子。首先,我们将表格增加一列性别,并在宏里调用”caller()”方法时,传入一个变量”user.gender”:

1

2

3

4

5

6

7

8

{% macro list_users(users) -%}

  <table>

    <tr><th>Name</th><th>Gender</th><th>Action</th></tr>

    {%- for user in users %}

      <tr><td>{{ user.name |e }}</td>{{ caller(user.gender) }}</tr>

    {%- endfor %}

  </table>

{%- endmacro %}

然后,我们修改下调用者语句块:

1

2

3

4

5

6

7

8

9

10

{% call(gender) list_users(users) %}

    <td>

    {% if gender == 'M' %}

    <img src="{{ url_for('static', filename='img/male.png') }}" width="20px">

    {% else %}

    <img src="{{ url_for('static', filename='img/female.png') }}" width="20px">

    {% endif %}

    </td>

    <td><input name="delete" type="button" value="Delete"></td>

{% endcall %}

大家注意到,我们在使用”{% call %}”语句时,将其改为了”{% call(gender) … %}”,这个括号中的”gender”就是用来接受宏里传来的”user.gender”变量。因此我们就可以在”{% call %}”语句中使用这个”gender”变量来判断用户性别。这样宏就成功地向调用者传递了参数。

 

宏的内部变量

上例中,我们看到宏的内部可以使用”caller( )”方法获取调用者的内容。此外宏还提供了两个内部变量:

  • varargs

这是一个列表。如果调用宏时传入的参数多于宏声明时的参数,多出来的没指定参数名的参数就会保存在这个列表中。

  • kwargs

这是一个字典。如果调用宏时传入的参数多于宏声明时的参数,多出来的指定了参数名的参数就会保存在这个字典中。

让我们回到第一个例子input宏,在调用时增加其传入的参数,并在宏内将上述两个变量打印出来:

1

2

3

4

5

6

{% macro input(name, type='text', value='') -%}

    <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}">

    <br /> {{ varargs }}

    <br /> {{ kwargs }}

{%- endmacro %}

<p>{{ input('submit', 'submit', 'Submit', 'more arg1', 'more arg2', ext='more arg3') }}</p>

可以看到,varargs变量存了参数列表”[‘more arg1’, ‘more arg2’]”,而kwargs字典存了参数”{‘ext’:’more arg3′}”。

宏的导入

一个宏可以被不同的模板使用,所以我们建议将其声明在一个单独的模板文件中。需要使用时导入进来即可,而导入的方法也非常类似于Python中的”import”。让我们将第一个例子中”input”宏的声明放到一个”form.html”模板文件中,然后将调用的代码改为:

1

2

3

4

{% import 'form.html' as form %}

<p>{{ form.input('username', value='user') }}</p>

<p>{{ form.input('password', 'password') }}</p>

<p>{{ form.input('submit', 'submit', 'Submit') }}</p>

运行下,效果是不是同之前的一样?你也可以采用下面的方式导入:

1

2

3

4

{% from 'form.html' import input %}

<p>{{ input('username', value='user') }}</p>

<p>{{ input('password', 'password') }}</p>

<p>{{ input('submit', 'submit', 'Submit') }}</p>

包含 (Include)

这里我们再介绍一个Jinja2模板中代码重用的功能,就是包含 (Include),使用的方法就是”{% include %}”语句。其功能就是将另一个模板加载到当前模板中,并直接渲染在当前位置上。它同导入”import”不一样,”import”之后你还需要调用宏来渲染你的内容,”include”是直接将目标模板渲染出来。它同block块继承也不一样,它一次渲染整个模板文件内容,不分块。

我们可以创建一个”footer.html”模板,并在”layout.html”中包含这个模板:

1

2

3

4

<body>

    ...

    {% include 'footer.html' %}

</body>

当”include”的模板文件不存在时,程序会抛出异常。你可以加上”ignore missing”关键字,这样如果模板不存在,就会忽略这段”{% include %}”语句。

1

    {% include 'footer.html' ignore missing %}

“{% include %}”语句还可以跟一个模板列表:

1

    {% include ['footer.html','bottom.html','end.html'] ignore missing %}

上例中,程序会按顺序寻找模板文件,第一个被找到的模板即被加载,而其后的模板都会被忽略。如果都没找到,那整个语句都会被忽略。

本篇中的示例代码可以在这里下载

Flask中Jinja2模板引擎详解(七)–本地化

一个强大的工具一般都支持扩展或插件的开发功能,来允许第三方通过开发新扩展或插件,扩充工具本身功能,并可以贡献给社区。Jinja2也不例外,Jinja2本身提供了一部分扩展,你可以在程序中启用。同时,你还可以创建自己的扩展,来扩充模板引擎功能。本篇会先介绍Jinja2自带的扩展”jinja2.ext.i18n”的使用,自定义扩展的开发会放在下一篇阐述。

系列文章

在Flask中启用Jinja2扩展

任何时候使用Jinja2时,都需要先创建Jinja2环境,所以启用扩展的方法就是在创建环境时指定:

1

2

from jinja2 import Environment

jinja_env = Environment(extensions=['jinja2.ext.i18n','jinja2.ext.do'])

但是你在使用Flask时,其已经有了一个Jinja2环境,你不能再创建一个,所以你需要想办法添加扩展。Flask对于扩展不像过滤器或测试器那样封装了添加方法和装饰器,这样你就只能直接访问Flask中的Jinja2环境变量来添加。

1

2

3

4

from flask import Flask

app = Flask(__name__)

app.jinja_env.add_extension('jinja2.ext.i18n')

app.jinja_env.add_extension('jinja2.ext.do')

注:Flask默认已加载了”jinja2.ext.autoescape”和”jinja2.ext.with_”扩展。

Jinja2内置扩展

本系列第一篇中,我们已经介绍了四个Jinja2内置扩展的使用:”jinja2.ext.autoescape”, “jinja2.ext.with_”, “jinja2.ext.do”和”jinja2.ext.loopcontrols”。除了这几个以外,Jinja2还有一个非常重要的扩展,就是提供本地化功能的”jinja2.ext.i18n”。它可以与”gettext”或”babel”联合使用,接下来我们采用”gettext”来介绍怎么使用这个本地化扩展。

注:本地化这个其实就是翻译页面语言,这个详看Flask-Babel

创建本地化翻译文件

建议大家先去了解下Python gettext相关知识,篇幅关系本文就不准备细讲。这里我们使用Python源代码(记住不是安装包)中”Tools/i18n”目录下的工具来创建翻译文件。

  1. 首先我们生成翻译文件模板,在”Tools/i18n”目录中找到”pygettext.py”并运行
$ python pygettext.py

上述命令会在当前目录下生成一个名为”message.pot”的翻译文件模板,内容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

# SOME DESCRIPTIVE TITLE.

# Copyright (C) YEAR ORGANIZATION

# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.

#

msgid ""

msgstr ""

"Project-Id-Version: PACKAGE VERSION\n"

"POT-Creation-Date: 2016-02-22 21:45+CST\n"

"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"

"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"

"Language-Team: LANGUAGE <LL@li.org>\n"

"MIME-Version: 1.0\n"

"Content-Type: text/plain; charset=CHARSET\n"

"Content-Transfer-Encoding: ENCODING\n"

"Generated-By: pygettext.py 1.5\n"

  1. 将”message.pot”中”CHARSET”和”ENCODING”替换成”UTF-8″。同时你可以更改注释信息

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

# Jinja2 i18n Extention Sample

# Copyright (C) 2016 bjhee.com

# Billy J. Hee <billy@bjhee.com>, 2016.

#

msgid ""

msgstr ""

"Project-Id-Version: PACKAGE VERSION\n"

"POT-Creation-Date: 2016-02-22 21:45+CST\n"

"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"

"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"

"Language-Team: LANGUAGE <LL@li.org>\n"

"MIME-Version: 1.0\n"

"Content-Type: text/plain; charset=UTF-8\n"

"Content-Transfer-Encoding: UTF-8\n"

"Generated-By: pygettext.py 1.5\n"

修改完后,将其另存为翻译文件”lang.po”。

  1. 在”lang.po”中添加你要翻译的文字,比如

1

2

msgid "Hello World!"

msgstr "世界,你好!"

将其加在文件末尾。这里”msgid”指定了待翻译的文字,而”msgstr”就是翻译后的文字。

  1. 生成”lang.mo”文件

我们依然使用”Tools/i18n”目录提供的工具,”msgfmt.py”:

$ python msgfmt.py lang.po

执行完后,当前目录生成了”lang.mo”文件。注意,只有这个”*.mo”文件才能被应用程序识别。另外,推荐一个工具Poedit,很强的图形化po编辑工具,也可以用来生成mo文件,非常好用,Mac和Windows下都能用。

  1. 将po, mo文件加入应用

我们在当前Flask工程下创建子目录”locale/zh_CN/LC_MESSAGES/”,并将刚才生成的”lang.po”和”lang.mo”文件放到这个目录下。这里”locale”子目录的名字可以更改,其他的个人建议不要改。

在模板中使用本地化

让我们在Flask应用代码中启用”jinja2.ext.i18n”,并加载刚创建的翻译文件。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

#coding:utf8

import gettext

from flask import Flask,render_template

app = Flask(__name__)

# 加载扩展

app.jinja_env.add_extension('jinja2.ext.i18n')

 

# 'lang'表示翻译文件名为"lang.mo",'locale'表示所有翻译文件都在"locale"子目录下,

# 'zh_CN'表示二级子目录,含义上讲就是加载中文翻译。所以下面的代码会加载文件:

# "locale/zh_CN/LC_MESSAGES/lang.mo"

gettext.install('lang', 'locale', unicode=True)

translations = gettext.translation('lang', 'locale', languages=['zh_CN'])

translations.install(True)

app.jinja_env.install_gettext_translations(translations)

这个”install_gettext_translations()”方法就是Jinja2提供来加载”gettext”翻译文件对象的。加载完后,你就可以在模板中使用本地化功能了。方法有两种:”{% trans %}”语句或”gettext()”方法。我们先来试下”{% trans %}”语句,在模板文件中,我们加上:

1

    <h1>{% trans  %}Hello World!{% endtrans %}</h1>

运行下,有没有看到页面上打印了”世界,你好!”,恭喜你,成功了!使用”gettext()”方法如下,效果同”{% trans %}”语句一样。

1

    <h1>{{ gettext('Hello World!') }}</h1>

Jinja2还提供了”_( )”方法来替代”gettext( )”,代码看起来很简洁,个人推荐使用这个方法。

1

    <h1>{{ _('Hello World!') }}</h1>

上面的例子是在程序中指定本地化语言,你也可以在请求上下文中判断请求头”Accept-Language”的内容,来动态的设置本地化语言。

翻译内容带参数

有时候,待翻译的文字内有一个变量必须在运行时才能确定,怎么办?我可以在翻译文字上加参数。首先,你要在po文件中定义带参数的翻译文字,并生成mo文件:

1

2

msgid "Hello %(user)s!"

msgstr "%(user)s,你好!"

然后,你就可以在模板中,使用”{% trans %}”语句或”gettext()”方法来显示它:

1

2

    <h1>{% trans user=name %}Hello {{ user }}!{% endtrans %}</h1>

    <h1>{{ _('Hello %(user)s!')|format(user=name) }}</h1>

上例中,我们把模板中的变量”name”赋给了翻译文字中的变量”user”。翻译文字上可以有多个变量。

新样式 (Newstyle)

Jinja2从2.5版本开始,支持新的gettext样式,使得带参数的本地化更简洁,上面的例子在新样式中可以写成:

1

    <h1>{{ _('Hello %(user)s!', user=name) }}</h1>

不过使用新样式前,你必须先启用它。还记得我们介绍过Jinja2加载翻译文件的方法吗?对,就是”install_gettext_translations()”。调用它时,加上”newstyle=True”参数即可。

1

app.jinja_env.install_gettext_translations(translations, newstyle=True)

单/复数支持

英文有个特点就是名词有单/复数形式,一般复数都是单数后面加s,而中文就不区分了,哎,老外就是麻烦。所谓外国人创造的Python gettext,自然也对单/复数提供了特殊的支持。让我们现在po文件中,加上下面的内容,并生成mo文件:

1

2

3

4

msgid "%(num)d item"

msgid_plural "%(num)d items"

msgstr[0] "%(num)d个物品"

msgstr[1] "%(num)d个物品集"

什么意思呢,这个”msgid_plural”就是指定了它上面”msgid”文字的复数形式。而”msgstr”的[0], [1]分别对应了单/复数形式翻译后的内容。为什么这么写?你别管了,照着写就是了。

在模板中,我们加上下面的代码:

1

2

3

  {% set items = [1,2,3,4,5] %}

  {{ ngettext('%(num)d item', '%(num)d items', items|count) }}<br />

  {{ ngettext('%(num)d item', '%(num)d items', items|first) }}

你会很惊奇的发现,当”num”变量为5时,页面显示”5个物品集”;而当”num”变量为1时,页面显示”1个物品”。也就是程序自动匹配单/复数。很神奇吧!

本来准备在本篇把扩展都介绍完的,发现单写个”i18n”后篇幅就很长了,只好把自定义扩展部分另起一篇。

本篇中的示例代码可以在这里下载

Flask中Jinja2模板引擎详解(八)–自定义扩展

说实话,关于自定义扩展的开发,Jinja2的官方文档写得真心的简单。到目前为止网上可参考的资料也非常少,你必须得好好读下源码,还好依然有乐于奉献的大牛们分享了些文章来帮助我理解怎么开发扩展。本文我就完全借鉴网上前人的例子,来给大家演示一个Jinja2的自定义扩展的开发方法。

系列文章

Pygments

Pygments是Python提供语法高亮的工具,官网是pygments.org。我们在介绍Jinja2的自定义扩展时为什么要介绍Pygments呢?因为Jinja2的功能已经很强了,我一时半会想不出该开发哪个有用的扩展,写个没意义的扩展嘛,又怕误导了读者。恰巧网上找到了一位叫Larry的外国友人开发了一个基于Pygments的代码语法高亮扩展,感觉非常实用。他的代码使用了MIT License,那就我放心拿过来用了,不过还是要注明下这位Larry才是原创。

你需要先执行”pip install pygments”命令安装Pygments包。代码中用到Pygments的部分非常简单,主要就是调用”pygments.highlight( )”方法来生成HTML文档。Pygments强的地方是它不把样式写在HTML当中,这样就给了我们很大的灵活性。开始写扩展前,让我们预先通过代码

1

2

from pygments.formatters import HtmlFormatter

HtmlFormatter(style='vim').get_style_defs('.highlight')

生成样式内容并将其保存在”static/css/style.css”文件中。这个css文件就是用来高亮语法的。

想深入了解Pygments的朋友们,可以先把官方文档看一下。

编写扩展

我们在Flask应用目录下,创建一个”pygments_ext.py”文件,内容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

#coding:utf8

from jinja2 import nodes

from jinja2.ext import Extension

 

from pygments import highlight

from pygments.formatters import HtmlFormatter

from pygments.lexers import guess_lexer, get_lexer_by_name

 

# 创建一个自定义扩展类,继承jinja2.ext.Extension

class PygmentsExtension(Extension):

    # 定义该扩展的语句关键字,这里表示模板中的{% code %}语句会该扩展处理

    tags = set(['code'])

 

    def __init__(self, environment):

        # 初始化父类,必须这样写

        super(PygmentsExtension, self).__init__(environment)

 

        # 在Jinja2的环境变量中添加属性,

        # 这样在Flask中,就可以用app.jinja_env.pygments来访问

        environment.extend(

            pygments=self,

            pygments_support=True

        )

 

    # 重写jinja2.ext.Extension类的parse函数

    # 这是处理模板中{% code %}语句的主程序

    def parse(self, parser):

        # 进入此函数时,即表示{% code %}标签被找到了

        # 下面的代码会获取当前{% code %}语句在模板文件中的行号

        lineno = next(parser.stream).lineno

 

        # 获取{% code %}语句中的参数,比如我们调用{% code 'python' %},

        # 这里就会返回一个jinja2.nodes.Const类型的对象,值为'python'

        lang_type = parser.parse_expression()

 

        # 将参数封装为列表

        args = []

        if lang_type is not None:

            args.append(lang_type)

 

            # 下面的代码可以支持两个参数,参数之间用逗号分隔,不过本例中用不到

            # 这里先检查当前处理流的位置是不是个逗号,是的话就再获取一个参数

            # 不是的话,就在参数列表最后加个空值对象

            # if parser.stream.skip_if('comma'):

            #     args.append(parser.parse_expression())

            # else:

            #     args.append(nodes.Const(None))

 

        # 解析从{% code %}标志开始,到{% endcode %}为止中间的所有语句

        # 将解析完后的内容存在body里,并将当前流位置移到{% endcode %}之后

        body = parser.parse_statements(['name:endcode'],drop_needle=True)

 

        # 返回一个CallBlock类型的节点,并将其之前取得的行号设置在该节点中

        # 初始化CallBlock节点时,传入我们自定义的"_pygmentize"方法的调用,

        # 两个空列表,还有刚才解析后的语句内容body

        return nodes.CallBlock(self.call_method('_pygmentize', args),

                                [], [], body).set_lineno(lineno)

 

    # 这个自定义的内部函数,包含了本扩展的主要逻辑。

    # 其实上面parse()函数内容,大部分扩展都可以重用

    def _pygmentize(self, lang_type, caller):

        # 初始化HTML格式器

        formatter = HtmlFormatter(linenos='table')

 

        # 获取{% code %}语句中的内容

        # 这里caller()对应了上面调用CallBlock()时传入的body

        content = caller()

 

        # 将模板语句中解析到了lang_type设置为我们要高亮的语言类型

        # 如果这个变量不存在,则让Pygmentize猜测可能的语言类型

        lexer = None

        if lang_type is None:

            lexer = guess_lexer(content)

        else:

            lexer = get_lexer_by_name(lang_type)

 

        # 将{% code %}语句中的内容高亮,即添加各种<span>, class等标签属性

        return highlight(content, lexer, formatter)

这段程序解释起来太麻烦,我就把注释都写在代码里了。总的来说,扩展中核心部分就在”parse()”函数里,而最关键的就是这个”parser”对象,它是一个”jinja2.parser.Parser”的对象。建议大家可以参考下它的源码。我们使用的主要方法有:

    • parser.stream 获取当前的文档处理流,它可以基于文档中的行迭代,所以可以使用”next()”方法向下一行前进,并返回当前行
    • parser.parse_expression() 解析下一个表达式,并将结果返回
    • parser.parse_statements() 解析下一段语句,并将结果返回。可以连续解析多行。它有两个参数
      1. 第一个是结束位置”end_tokens”,上例中是”{% endcode %}”标签,它是个列表,可是设置多个结束标志,遇到其中任意一个即结束
      2. 第二个是布尔值”drop_needle”,默认为False,即解析完后流的当前位置指向结束语句”{% endcode %}”之前。设为True时,即将流的当前位置设在结束语句之后

在”parse()”函数最后,我们创建了一个”nodes.CallBlock”的块节点对象,并将其返回。初始化时,我们先传入了”_pygmentize()”方法的调用;然后两个空列表分别对应了字段和属性,本例中用不到,所以设空;再传入解析后的语句块”body”。”CallBlock”节点初始化完后,还要记得将当前行号设置进去。接下来,我们对于语句块的所有操作,都可以写在”_pygmentize()”方法里了。

“_pygmentize()”里的内容我就不多介绍了,只需要记得声明这个方法时,最后一定要接收一个参数caller,它是个回调函数,可以获取之前创建”CallBlock”节点时传入的语句块内容。

使用自定义扩展

扩展写完了,其实也没几行代码,就是注释多了点。现在我们在Flask应用代码中将其启用:

1

2

3

4

5

from flask import Flask,render_template

from pygments_ext import PygmentsExtension

 

app = Flask(__name__)

app.jinja_env.add_extension(PygmentsExtension)

然后让我们在模板中试一下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<head>

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

</head>

<body>

<p>A sample of JS code</p>

{% autoescape false %}

{% code 'javascript' %}

    var name = 'World';

    function foo() {

        console.log('Hello ' + name);

    }

{% endcode %}

{% endautoescape %}

</body>

运行下,页面上这段代码是不是有VIM的效果呀?这里我们引入了刚才创建在”static/css”目录下”style.css”样式文件,另外千万别忘了要将自动转义关掉,不然你会看到一堆的HTML标签。

另外提醒下大家,网上有文章说,对于单条语句,也就是不需要结束标志的语句,”parse()”函数里无需调用”nodes.CallBlock”,只需返回”return self.call_method(xxx)”即可。别相信他,看看源码就知道,这个方法返回的是一个”nodes.Expr”表达式对象,而”parse()”必须返回一个”nodes.Stmt”语句对象。

本篇中的示例代码可以在这里下载

 

posted @ 2018-07-20 09:32  哈喽哈喽111111  阅读(1222)  评论(0编辑  收藏  举报