环境:

  • 操作系统: Mac OSX 10.9.5
  • Python: 2.7.8
  • Flask: 0.10.1
  • 开发工具: Eclipse Luna + Pydev 3.6

之前对Pyramid, Django都已经有一些项目经验了,这次研究一下Flask的易用性如何。 说不定以后能在小项目上用的上。 本文主要参考一下两个网页:

  • http://flask.pocoo.org/
  • http://flask.pocoo.org/docs/0.10/quickstart/

首先建立开发的环境

假设我们Base Dir 为 /temp/flaskstudy

cd /temp/flaskstudy
virtualenv env
mkdir proj src

这时候我们的目录结构如下:

/temp/flaskstudy
├── env          # 这是VirtualEnv生成的目录
├── proj         # 这是放Eclipse workspace的地方
└── src          # 这是项目相关源代码

打开Eclipse, Workspace路径输入 /temp/flaskstudy/proj, 进入后在Preferences中设置, PyDev -> Interpreters -> Python Interpreter -> New... -> Browse...

选择 /temp/flaskstudy/env/bin/python

然后再新建Pydev项目, File -> New -> Project -> Pydev Project

Project name: firstweb, 勾掉下面的Use default, Directory 中改为 /temp/flaskstudy/src/firstweb, 点击Finish。

这时候基本的开发环境已经弄好。

Flask 安装

进入virtualenv的环境, 就是把virtualenv加入Path环境变量

cd /temp/flaskstudy
source env/bin/activate

pip install Flask

最后输入以下信息表示成功,如果被GFW挡住了,请自行翻墙 :(

Successfully installed Flask Werkzeug Jinja2 itsdangerous markupsafe
Cleaning up...

最简单的Flask 程序

http://flask.pocoo.org/ 首页上的Hello 例子

cd /temp/flaskstudy/src/firstweb
vi hello.py

内容如下:

# -*- coding:utf-8 -*-

from flask import Flask
app = Flask(__name__)

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

if __name__ == "__main__":
    app.run()

命令行中启动:

(env)rocky:firstweb rocky$ python hello.py
* Running on http://127.0.0.1:5000/

Pydev中启动:

在左边的firstweb中刷新一下,看见hello.py, 右击 Run As -> Python Run, 在Pydev的Console Panel中就会输出同样的信息。

* Running on http://127.0.0.1:5000/

Pydev中运行的好处是可以用GUI的方式去Debug, 看程序运行时的一些信息,方便 学习过程。

验证在浏览器中 http://127.0.0.1:5000 是否看见Hello World!

要结束Server服务的话control-C

进一步学习

接着练习一下 http://flask.pocoo.org/docs/0.10/quickstart 上的一些例子

首先解释一下上面的hello.py 程序。

  1. 首先导入Flask类,此类的实例就是一个WSGI 应用。
  2. 创建Flask实例,第一个参数时应用模块的名字或着包名。Flask就会知道去哪里找 模版,静态文件,url路由等等。
  3. route() 修饰符指定用户访问 / 时候会直接调用被修饰的方法。
  4. hello_world返回的字符串就是浏览器接收的response信息。
  5. 最后的app.run()在if语句里面,就是python直接运行此module的时候执行的操作。 作用时启动web 服务。如果想让其他人也能访问到你的服务的话,那监听的端口 就得改改。 app.run(host='0.0.0.0')

Debug模式启动

在开发过程中,我们不想修改源代码就重启一下服务,我们还想在网页上看到一些代码 出错信息,就需要使用Debug 模式。

在app.run()之前加入app.debug = True, 或者 app.run(debug=True)

严重警告, 不要在生产环境中使用Debug模式。因为出错信息得页面可以进行调试, 如果权限足够的话,攻击者可以运行一些伤害服务器的命令。

启动服务,就会多了最后一句log

(env)rocky:firstweb rocky$ python hello.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader

这时候我们试着修改hello.py, 让它出错,看看浏览器上出现什么。

def hello():
    print undefined
    return "Hello World!"

当我们修改完成保存后,服务程序自动重启,

 * Detected change in 'hello.py', reloading
 * Restarting with reloader

只是后刷新页面,Python的错误trace就会友好的以html方式呈现,而且还可以输入Python 语句进行交互。


路由

记住@app.route('/url')的用法, 其他的就是依样画葫芦了。下面这个例子把hello world 挪到hello方法,添加一个首页index。

@app.route('/')
def index():
    return 'Index Page'

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

来点高级的,如果想在路由中取得变量,想用REST一点的URL, 不想拖着一窜?foo=bar&name=rocky 这种传统url,

@app.route('/user/<username>')
def show_user_profile(username):
    # 如果用户在浏览器中输入/user/rocky, "rocky"就会存到username变量中,
    # 类型是Unicode字符串
    return 'User %s' % username

@app.route('/post/<int:post_id>')
def show_post(post_id):
    # 如果用户在浏览器中输入/post/100, 100就会存到post_id变量中,
    # 类型是int数字, 如果post_id不是数字,就会返回404错误
    return 'Post %d' % post_id

@app.route('/lsdir/<path:dir_path>')
def list_dir(dir_path):
    # 如果用户在浏览器中输入/lsdir/a/b/c, 'a/b/c'会存到dir/path变量中,
    # 类型是Unicode字符串, 如果dir_path不符合Posix路径规则,返回404错误
    return 'Path %s' % dir_path

末尾带/的问题

如果是route中带上/, 我们在访问不带/的URL, 会返回301编码, 自动重定向到带/的, 但如果route中不带/, 我们访问带/的URL, 会返回404错误。 这个原则主要是方便搜索引擎索引一个URL, 避免同一个URL在带/结尾和不带/结尾被 两次索引。

@app.route('/projects/')
def projects():
    return 'The project page'

输出URL

我们定义了URL得route,现在想反过来,用方法来取得URL, 这是可以的。用url_for 方法。

@app.route('/build_url')
def build_url():
    url = ''
    from flask import url_for
    url += url_for('projects') + '<br/>'
    url += url_for('show_user_profile', username='Rocky Feng') + '<br/>'
    return url

这样做的好处是:

  1. 不想直接硬编码到程序里面,
  2. 一些URL 编码问题,字符转换问题都可以解决
  3. 如果webapp应用不是在根下,而是放在/myapplication中,url_for也会自动生成 到/myapplication下面,比如/myapplication/user/rocky.

HTTP 协议的方法

对于同一个URL, http有不同的方法访问。Flask中默认Route只响应GET 请求,但是可以 指定参数让其响应其他对http请求方法,如POST

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        do_the_login()
    else:
        show_the_login_form()

如果定义了GET, HEAD方法也会自动加上了, 如果你不清楚HTTP方法,请自行google或baidu 一下。


静态文件

动态网站也需要静态文件,比如说CSS, JS, 不过在最终的生产环境这都是有Web服务器去 传输的,但在开发阶段,你又不想在电脑上配置Apache, Nginx之类的软件,用Flask一样 可以轻松实现。就在你的py模块同级目录下创建static文件夹就行。

比如说/static/style.css, 这时候访问http://127.0.0.1:5000/static/style.css 就能 获取到其内容。

└── firstweb
    ├── hello.py
    └── static
        └── style.css

使用模版

Flask使用的是Jinja2引擎的模版,是不是很日本的一个名字。这里吐槽一下,Python的模版 引擎轮子是在太多了,mako, django, Cheetah, Genshi, Tenjin, 而且名字都很日本, 就不能统一一下战线么,浪费多少开发人力。Python Web没Ruby用的多这个估计也是个原因, 人家就集中社区去开发Rails。

要使用模版就用render_template()方法。Flask会自动到templates目录下查找模版, 所以要添加templates文件夹。

firstweb
├── hello.py
├── static
│   └── style.css
└── templates
    └── hello.html

修改hello.py中的hello方法, 文件开始处导入render_template。 从这里的代码可以看出, 一个方法是可以指定多个URL的。

from flask import render_template

@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
    return render_template('hello.html', name=name)

hello.html内容如下, 逻辑大概是如果URL中有名字,就hello 名字,没有就hello world.

<!doctype html>
<title>Hello from Flask</title>
{% if name %}
  <h1>Hello {{ name }}!</h1>
{% else %}
  <h1>Hello World!</h1>
{% endif %}

在模版中,默认可以访问到request, session, g, get_flashed_messages()等全局的变量 或方法。


访问Request 的数据

Web应用关键就是对客户发送給服务器的数据进行处理。这些数据都封装在一个全局的 request 对象中。这个request是一个线程安全的。也就是说,A用户提交的跟B用户提交 的东西各存在自己的request对象中,不会相互影响。

下面这段login程序简单介绍request的用法。

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

    # 如果HTTP方法是POST, 换句话说是提交表单过来的, 那应该有username和
    # password两个Input 数据
    if request.method == 'POST':
        # valid_login是判断用户名和密码是否配对,这属于业务逻辑,不在这里
        # 细说, 其返回True 或 False
        if valid_login(request.form['username'],
                       request.form['password']):
            return log_the_user_in(request.form['username'])
        else:
            error = 'Invalid username/password'

    # 如果是 GET 过来的,换句话说就是直接在地址栏上输入http://127.0.0.1/login
    # 直接返回template内容
    return render_template('login.html', error=error)

那如何得到URL中那些?key=value的值呢?可以用args属性, 比如说:

searchword = request.args.get('key', '')

这里指定了默认是为'', 这样如果用户手敲地址过来忘了输入?key=value的时候不至于 出400的错误。


文件上传

对于Flask来说,文件上传就是easy job, 只要我们在Form表单中设置 enctype="multipart/form-data", 否则浏览器是不会上传文件的。

上传的文件会存储到内存中或者磁盘中的临时地址。你可以通过request 的 files属性 访问。这是一个dict字典,key是文件名,value是一个类Python标准库file的对象, 此对象还直接提供了save方法,方便你把文件持久化到磁盘。不多说,上代码。

from flask import request

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        f = request.files['the_file']
        f.save('/var/www/uploads/uploaded_file.txt')
    ...

如果你想知道这个文件在客户端上传的时候是叫什么,你可以通过filename属性。不过记住, 这个值是可以被忽略的,所以不要相信它。在使用filename的时候,用secure_filename 处理一下,避免出现安全性问题。

from flask import request
from werkzeug import secure_filename

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        f = request.files['the_file']
        f.save('/var/www/uploads/' + secure_filename(f.filename))
    ...

Cookies

Cookies 名词这个不多解释,基本上相当于不安全的在客户机本地存储的Session。

如何设置cookies

正常情况下,View方法返回的字符串,Flask会帮你封装到response对象。如果你想手动 去构造这个response对象,可以使用make_response()方法。

from flask import make_response

@app.route('/')
def index():
    resp = make_response(render_template(...))
    resp.set_cookie('username', 'the username')
    return resp

如何获取cookie

from flask import request

@app.route('/')
def index():
    username = request.cookies.get('username')
    # 或者下面,但如果username不存在,会有KeyError
    username = request.cookies['username']

重定向和错误

重定向就是HTTP层面的302, 让浏览器重新发送请求到其他URL, 使用redirect()方法。 如果想直接返回其他的HTTP错误码,可以用abort() 。

redirect只要用于没权限,需要跳转到登录页面的场景。如果不想让用户登录,直接告诉 请求是非法的话,可以用abort(401)

from flask import abort, redirect, url_for

@app.route('/')
def index():
    return redirect(url_for('login'))

@app.route('/vip')
def login():
    abort(401)
    this_is_never_executed()

404是最常见的错误,如果你不想你的页面那么单调,想来点小清新的感觉,可以定制一下。 让美工做一个page_not_found.html, 放到templates下面就这么简单。

from flask import render_template

@app.errorhandler(404)
def page_not_found(error):
    return render_template('page_not_found.html'), 404

关于Reponses

一个视图方法,也就是route修饰的那个方法, Flask得到他的结果后, 处理逻辑如下

  1. 如果返回是一个正常的response对象,就直接返回此对象。
  2. 如果是一个字符串,就以其构造一个默认的response对象并返回。
  3. 如果是一个Tuple, 其结构必须是 (response, status, headers), status会覆盖 status code, headers是一些额外的header值。
  4. 如果都不是,Flask 会当它是一个WSGI 应用并把它转成reponse 对象。

如果你想先生成response,然后再修改其中的header, 你可以这么做。

@app.errorhandler(404)
def not_found(error):
    resp = make_response(render_template('error.html'), 404)
    resp.headers['X-Something'] = 'A value'
    return resp

Session

在Request 对象中,还存在session, 可以让先在一个request中把一个值存下来,然后 在下个request再使用到。但这里session是通过cookies实现的,只不过加密了而已。

新建一个login.py的例子

from flask import Flask, session, redirect, url_for, escape, request

app = Flask(__name__)

@app.route('/')
def index():
    if 'username' in session:
        return 'Logged in as %s' % escape(session['username'])
    return 'You are not logged in'

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        session['username'] = request.form['username']
        return redirect(url_for('index'))
    return '''
        <form action="" method="post">
            <p><input type=text name=username>
            <p><input type=submit value=Login>
        </form>
    '''

@app.route('/logout')
def logout():
    # remove the username from the session if it's there
    session.pop('username', None)
    return redirect(url_for('index'))

# set the secret key.  keep this really secret:
app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'

if __name__ == "__main__":
    app.debug = True
    app.run()

怎么生成随机的密码

命令行下进入python console, 就是输入python 回车

>>> import os
>>> os.urandom(24)

把输出的结果拷到 app.secret_key = ''中就行


消息提示

这里指的是比如说用户输入用户名或密码错, 输入的东西不符合逻辑,一般都会給 一个友好的提示,这时候我们需要使用flash()方法,把消息放进篮子里面, 然后在模版中使用 get_flashed_messages(), 把需要展示的消息从篮子里取出来。


日志输出

就是避免自己用print 到处写啦。例子如下

app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')

添加WSGI的组件

WSGI组件就像一个洋葱,一层一层的拨开,每一层都返回WSGI application对象就可以。

比如说,你想使用lighttpd的一个组件去修复lighttpd Web服务的bug, 可以这么写

from werkzeug.contrib.fixers import LighttpdCGIRootFixI
app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app)

结语

到这里基本的Flask 框架已经说的差不多了,接下来就是真正的在项目中如何使用。