Flask 使用 flask_login 登录时报的一个错误

在用 flask_login 做登录的时候,在 base.html 里面写了这样的判断: {% if current_user.is_authenticated %} xxx {% else %} xxx {% endif %}

导致每当我访问登录页和注册页的时候报错,错误如下

未分类

这个错误通常有一个问题,就是没有对 flask_login 进行初始化

    login_manager = LoginManager()
    login_manager.init_app(app)

    @login_manager.user_loader
    def load_user(id):
        return User.query.get(int(id))

    login_manager.login_view = 'front.login'

但是这一步我是做了的,那那是哪里出的问题呢?

问题出自 macros 这个页面上

未分类

注意,这里的 macros 里面不应该去继承 “base.html” 的,因为 macros.html 是一个宏定义文件,它不应该从任何模板继承!

Flask+uWSGI 的 Logging 支持

本文基于 Flask 0.12.2 。

当 Flask App 被部署到生产环境时,我们会选择关闭 DEBUG 配置。在这种情况下,Flask 中使用 flask.current_app.logger.info() 打印的 LOG 仿佛消失了一样。它们去了哪里呢?

默认的 Handler

下面的源码位于 [flask.logging][loggin] 中。从源码可以看出,Flash 自动创建了 logger 并加入了一个 DEBUG 级别的 Handler 和一个 ERROR 级别的 Handler。根据 DEBUG 变量的值,DEBUG Handler 在生产环境下是不生效的。因此我们就只能看到来自于 ProductHandler 的 ERROR 级别 Log 信息。

def create_logger(app):
    """Creates a logger for the given application.  This logger works
    similar to a regular Python logger but changes the effective logging
    level based on the application's debug flag.  Furthermore this
    function also removes all attached handlers in case there was a
    logger with the log name before.
    """
    Logger = getLoggerClass()
    class DebugLogger(Logger):
        def getEffectiveLevel(self):
            if self.level == 0 and app.debug:
                return DEBUG
            return Logger.getEffectiveLevel(self)
    class DebugHandler(StreamHandler):
        def emit(self, record):
            if app.debug and _should_log_for(app, 'debug'):
                StreamHandler.emit(self, record)
    class ProductionHandler(StreamHandler):
        def emit(self, record):
            if not app.debug and _should_log_for(app, 'production'):
                StreamHandler.emit(self, record)
    debug_handler = DebugHandler()
    debug_handler.setLevel(DEBUG)
    debug_handler.setFormatter(Formatter(DEBUG_LOG_FORMAT))
    prod_handler = ProductionHandler(_proxy_stream)
    prod_handler.setLevel(ERROR)
    prod_handler.setFormatter(Formatter(PROD_LOG_FORMAT))
    logger = getLogger(app.logger_name)
    # just in case that was not a new logger, get rid of all the handlers
    # already attached to it.
    del logger.handlers[:]
    logger.__class__ = DebugLogger
    logger.addHandler(debug_handler)
    logger.addHandler(prod_handler)
    # Disable propagation by default
    logger.propagate = False
    return logger

要解决这个问题,我们需要创建自己的 Handler 。

创建自己的 Handler

在创建了 Flask app 之后,调用下面的 _set_logger 方法将 app 实例传入即可。详细的介绍见代码中的注释。

def _set_logger(flaskapp):
    """
    设置 Flask app 的logger
    """
    # 删除 Flask 的默认 Handler
    del flaskapp.logger.handlers[:]
    if flaskapp.config.get('DEBUG'):
        # 在 DEBUG 模式下,使用 StreamHandler,并使用 DEBUG 级别,这样可以将所有的信息都输出到控制台
        hdr = logging.StreamHandler()
        hdr.setLevel(logging.DEBUG)
        flask.logger.setLevel(DEBUG)
    else:
        # 不使用 StreamHandler 的原因,是 uwsgi 可能会在标准输出中加入它自己的 Log,为了避免Log被弄乱,单独使用一个 FileHandler
        hdr = logging.FileHandler(config.getdir('logs', 'app.log'), encoding='utf8')
        hdr.setLevel(logging.INFO)
        flask.logger.setLevel(INFO)
    # 加入足够详细的信息
    LOG_FORMAT = """
[%(asctime)s] %(levelname)s in %(module)s.%(funcName)s [%(pathname)s:%(lineno)d]:
%(message)s"""
    hdr.setFormatter(logging.Formatter(LOG_FORMAT))
    # 如果存在 sqlalchemy 的 Log 对象,也为其加入这个 Handler
    for log in (flaskapp.logger, logging.getLogger('sqlalchemy')):
        if log:
            log.addHandler(hdr)

我们还可以重写 Flask 对象的 log_exception 方法,自动将所有的异常记录下来,并提供一些更详细的信息:

class MYFlask(Flask):
    def log_exception(self, exc_info):
        """...description omitted..."""
        self.logger.error(
            """
Request:   {method} {path}
IP:        {ip}
Agent:     {agent_platform} | {agent_browser} {agent_browser_version}
Raw Agent: {agent}
            """.format(
                method=request.method,
                path=request.path,
                ip=request.remote_addr,
                agent_platform=request.user_agent.platform,
                agent_browser=request.user_agent.browser,
                agent_browser_version=request.user_agent.version,
                agent=request.user_agent.string,
            ), exc_info=exc_info
        )

uWSGI 的 Logging 配置

我使用 INI 格式的配置文件,文件名一般为 uwsgi.ini。其中关于 Logging 的配置,我常用这样几个:

; 将写入 log 的工作委托给 master 进程
log-master = true
; 单独开一个线程进行 log 写入工作,这样有更好的性能
threaded-log = true
; 所有 log 都会写入这个文件
; 若希望所有 log 放在一起,设置了此选项后,不要设置 req-logger 和 logger 选项
; %d 代表 uwsgi.ini 所在文件夹(包含结尾的/), %n 代表 uwsgi.ini 的主文件名
; 魔术变量: http://uwsgi-docs-zh.readthedocs.io/zh_CN/latest/Configuration.html#magicvars
daemonize = %dlogs/%n.log
; 将 uWSGI 请求 log 写入单独的 log 文件,这样做可以让log更加分离,便于查错
; 设置了此选项后,daemonize 设置的输出文件就得不到任何输出了
req-logger = file:%dlogs/req.log
; 将 uWSGI stdout/stderr log 写入单独的 log 文件
; 因为设定了 req-logger ,必须同时设定 logger ,此时 daemonize 不会有任何输出
logger = file:%dlogs/%n.log

自定义 uWSGI 的请求 log

uWSGI 会在 log 中自动写入请求 log,默认的格式如下:

[pid: 22740|app: 0|req: 162/324] 127.0.0.1 () {36 vars in 608 bytes} [Wed Nov 29 11:42:08 2017] GET /login/?code=001SkzFb1ppEDu0lTzHb1WPCFb1SkzF0 => generated 181 bytes in 69 msecs (HTTP/1.1 200) 5 headers in 209 bytes (2 switches on core 1)

关于其中信息如何解释,文档中并没有详细进行介绍,只能通过阅读 源码 logging.c 理解。

我在 uWSGI 的邮件列表中找到一封邮件 Default Log Format Explained? 介绍了 log 中每个项的详细作用。

pid -> the pid of the worker managing the request
app -> the id (it is a integer, starting from 0) of the app, it makes
sense when multiple apps are hosted in the same instance. It is -1 when no
app managed the request (like when serving static files) or when the ‘app’
concept does not apply (like with php or cgi’s)
req: N/M -> N is the number of managed requests by the current worker for
the specific app, M is the grand total (sum of all requests of all
workers)

then you have REMOTE_ADDR followd by the (optional) REMOTE_USER (very
similar to apache)

vars are the number of CGI vars in the request, and their size (from the
uwsgi protocol point of view). The size is never higher than the
–buffer-size (higher requests are discarded)

The time of the request follows

Then you have REQUEST_METHOD + REQUEST_URI

Then the response size and the time required for generating it

“via” is the techology used to send the response, currently can be
sendfile, routing or offloading.

The response status follows, as well as the number of response headers.

“core” is the low-level concept for uWSGI concurrency context in a process
(can be a thread or a greenlet or a fiber or a goroutine and so on…)
while switches count is incremented whenever an app “yield” its status
(this has various meanings based on the lower concurrency model used)

根据上面找到的资料和 格式化uWSGI请求日志 文档,通过设置 log-format 选项,我们可以模仿出默认的请求 log:

log-format = [pid: %(pid)] %(addr) (%(user)) {%(vars) vars in %(pktsize) bytes} [%(ctime)] %(method) %(uri) => generated %(rsize) bytes in %(msecs) msecs (%(proto) %(status)) %(headers) headers in %(hsize) bytes (%(switches) switches on core %(core))

除了 app: 和 req: 没有提供对应变量,其它的值都可以显示出来。

其它

日志编码器 也是一个重要的选项,若有需要可以添加该设置。

uWSGI 还可以使用 touch-logrotate 和 touch-logreopen 来实现 logging rotate,但为了让系统更加简单的独立,我建议使用 logrotate 来实现 logging rotate,并已经在 uWSGI+rsyslog 实现 rotating logging 一文中介绍过具体做法。

需要注意的是,我在 单独使用 logrotate 中提到的使用 copytruncate 选项替换 create 选项,是因为没有通知 uWSGI 重新打开 log 文件。要做到这一点非常简单,除了使用刚才提到的 touch-logreopen 之外,还可以使用 Master FIFO 中的 l 命令。

在阿里云上通过Ubuntu+uwsgi+nginx+mysql部署Flask(新手向)

0. 前言

这其实也是你所看到的这个网站的部署方式。

老规矩,上环境。截至2017年11月:

Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-62-generic x86_64)

uwsgi –version 2.0.15

nginx version: nginx/1.10.3 (Ubuntu)

supervisord -v 3.2.0

而我的个人电脑是

4.13.12-1-ARCH x86_64 GNU/Linux

1. 创建Ubuntu并创建新用户

在阿里云购买好ECS,我选择的最便宜的1核那个,一年300多,买完之后在控制台可以选择系统,选择好后输入root用户的密码。

未分类

随后会告诉你一个网页上可以连接到服务器的密码,你得记住。随后在网页上登录到服务器。键入

adduser edison

创建一个新用户,当然了用户名你自己取,这里是我的名字。

随后在我的电脑上用ssh连接服务器,这样方便操作些,此处怎么ssh连接服务器就不说了,百度谷歌一大把。

此处先用

ssh root@ip 

来连接。会提示加入密钥回车就好,然后输入你root也就是刚开始创建云盘时的那个密码。就连接成功了。

输入

nano /etc/sudoers

为新创建的用户加入sudo权限:

未分类

输入

apt-get update

更新索引,这里如果比较慢的话可以替换成中科大的源,如何替换百度谷歌就好也很简单。随后开始安装必要的软件。

2.安装python3和git

先安装python3

apt install python3-pip

随后使用pip3安装虚拟环境模块

pip3 install virtualenv

此时可以切换至edison这个新用户。

ssh edison@ip

我是把网站放在了github上,之后更新程序也方便管理。所以这里安装git

未分类

这里发现提示了

sudo: unable to resolve host:xxx

的信息,不影响使用的。可以通过修改hosts文件解决

未分类

未分类

输入y然后回车继续安装git。

在/home/edison下新建目录blog,在blog下git clone把源代码拷下来。

创建虚拟环境:

未分类

注意这里的虚拟环境使用的python3版本。

可以通过启动虚拟环境查看,确实是py3:

未分类

在blog下再新建一个logs文件夹。此时blog应该有三个文件夹,其他两个一个是venv文件夹一个是你的程序的文件夹。

未分类

3. 安装nginx

输入

sudo apt-get install nginx

安装nginx。输入

sudo /etc/init.d/nginx start

启动服务,shell会显示ok提示启动nginx服务成功。这时候打开浏览器,直接输入你的ECS服务器的公网ip就可以看到

未分类

就说明nginx启动成功。接下来用一个小的flask程序进行小小的演示。

创建一个test.py:

未分类

然后在虚拟环境下(根据前文,这时候已经在虚拟环境中了,可以从下图的shell里第一行的前面的venv字眼看出)

pip3 install flask

安装flask模块。

4. 安装uwsgi和supervisor并配置

接下来安装

pip install uwsgi

未分类

然后(此时所在文件夹是blog里)新建一个uwsgi的配置文件,并编辑以下内容,然后启用uwsgi:

未分类

未分类

未分类

可以看到uwsgi已经在运行了。这时候ctrl+c退出uwsgi。

接下来安装supervisor后进入配置文件夹,新建一个文件并输入:

sudo apt-get install supervisor
cd /etc/supervisor/conf.d
sudo nano blogSupervisor.conf

未分类

然后输入

sudo service supervisor start

开启进程管理。

5. 配置nginx

进入/etc/nginx/sites-available/,编辑里面default文件,其中内容是:

未分类

然后输入

sudo service nginx restart

重启nginx服务器。打开浏览器输入你的公网ip,就能看到

Hello World! 啦

未分类

这里部署就基本完了,只要把上述的配置文件里提到test这个py文件替换成你的文件,例如manage.py就好了。

6. Mysql和后续

接下来简单说说怎么使用mysql:

安装pymysql,在config.py里使用

“mysql+pymysql://root:xxx@localhost:3306/xxxxx”

的方式启用mysql。

p.s

在使用mysql上尝试了很多方法,但是由于ubuntu的系统python是2,而虚拟环境中python为3,所有总有一些包安装不上,特别是mysqldb的问题,只能使用mysql+pymysql的方法。但是很奇怪的现象是,我的archlinux默认的python是3.6,使用的mariadb,但是不用pymysql也能连接上mysql,而且:

$ python                                                           
Python 3.6.3 (default, Oct 24 2017, 14:48:20) 
[GCC 7.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import MySQLdb
>>> 

百思不得其解,如果有缘人看到了这里希望能给出答复。

使用virtualenv搭建Python下的Flask开发环境,ubu测试有效

Flask 依赖两个外部库:Werkzeug 和 Jinja2 。不过使用virtualenv就可以搞定这一切。

下面重点介绍一下环境搭建的步骤:

如果你在 Mac OS X 或 Linux 下,下面两条命令可能会适用:

$ sudo easy_install virtualenv

或更好的:

$ sudo pip install virtualenv

上述的命令会在你的系统中安装 virtualenv。它甚至可能会存在于包管理器中, 如果你用的是 Ubuntu,可以尝试:

$ sudo apt-get install python-virtualenv

virtualenv 安装完毕后,执行:

$ virtualenv venv
New python executable in venv/bin/python
Installing distribute............done.

现在,无论何时你想在某个项目上工作,只需要激活相应的环境。在 OS X 和 Linux 上,执行如下操作:

$ . venv/bin/activate

下面的操作适用 Windows:

$ venvscriptsactivate

无论通过哪种方式,你现在应该已经激活了 virtualenv(注意你的 shell 提示符显示的是当前活动的环境)。

现在你只需要键入以下的命令来激活 virtualenv 中的 Flask:

$ pip install Flask

几秒钟后,一切都搞定了。

然后deactivate退出当前环境。

Flask 与 Tornado 中的路由定义

Flask 和 Tornado 是我现在最“熟悉”的两个 Python web 框架,各自都具备一些别具一格的特性, 在很多实现上都走了截然不同的道路。它们的路由实现分别代表了 Python web 框架的两大风格:

Flask 使用函数作为 web 请求的 Handler,这一点和早期的 Django 是一致的, 不过 Django 似乎是 1.7 以后推荐使用 class 作为 Handler。和 Django 不同的是, Flask 使用装饰器注册路由,同类型的框架还有 Bottle。

# flaskapp.py
from flask import Flask
app = Flask(__name__)

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

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

说到 class handler,web.py 就是其中很有名的 一个,而 Tornado 的最初的灵感就是来自 web.py,Tornado 和 web.py 在路由方面都是 使用路由元组做配置。

# tornadoapp.py
import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

(上面两段代码分别来自 Flask 和 Tornado 官方的示例教程。)

当然,并不是说 Flask 和 Tornado 就不能使用对方的路由配置模式了, 这里提供两个简单的实现抛砖引玉:

使用路由元组的 Flask

Flask 在路由方面支持惰性加载, 提供了 Flask.add_url_rule() 函数用于手动注册路由:

# flaskapp.py
import flask

class Flask(flask.Flask):
    def add_url_rules(self, url_rules):
        for url_rule in url_rules:
            self.add_url_rule(url_rule[0], view_func=url_rule[1])

def hello():
    return "Hello World!"

app = Flask(__name__)
app.add_url_rules([("/", hello)])

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

使用装饰器配置路由的 Tornado

上面的 “Hello World” 修改如下:

# tornadoapp.py
import tornado.ioloop
import tornado.web

class Application(tornado.web.Application):
    def route(self, pattern):
        def _(handler):
            handler_pattern = [(pattern, handler)]
            self.add_handlers(".*$", handler_pattern)
            return handler
        return _

app = Application()

@app.route(r"/")
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

if __name__ == "__main__":
    app.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

使用python下的Flask应用

Flask是一个使用 Python 编写的轻量级 Web 应用框架。在学习过程中进行一些总结:

1. 一个最小的Flask应用 flask_test1.py:

from flask import Flask
app = Flask(__name__)

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

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

执行python flask_test1.py即可,在浏览器中使用127.0.0.1:5000即可访问

配置外部可访问,将脚本中的app.run()改写成app.run(host=’0.0.0.0′),如此就可以在外面使用运行脚本的服务器地址访问了,如192.168.2.22:5000

2. Flask的路由 flask_test2.py:

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

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

if __name__ == '__main__':
    app.run(host='0.0.0.0')

运行脚本后,使用127.0.0.1:5000/projects 和127.0.0.1:5000/about即可得到不同的消息处理方式

也可以使用通配的方式书写路由,如下

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

运行脚本后,使用127.0.0.1:5000/user/projects 和127.0.0.1:5000/user/about都会进入到show_user_profile()这个函数里面进行处理

3. Flask的Http方法:

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

运行脚本后可以使用get和post方法分别向login这个路由下发送消息。

Flask 读取 json 配置文件

import os
import json
from flask import Flask


def create_app():
    app = Flask('test')
    # 这里在虚拟环境中设置环境变量。 export RMON_CONFIG=xxx.json
    file = os.environ.get('RMON_CONFIG')
    content = ''
    if file:
        rest = {}
        with open(file) as f:
            for line in f:
                #  if line.strip().startswith('#'):
                if "#" in line:
                    continue
                content += line
    if content:
        config = json.loads(content)
        for k in config:
            app.config[k.upper()] = config[k]
    return app


if __name__ == '__main__':
    create_app()

Python Flask框架连接Mysql 学习笔记

认识Flask框架

Flask 是一个 Python 语言的微型网络开发框架。微框架中的 “微” 意味着 Flask 旨在保持核心简单而易于扩展。Flask 不会替你做出太多决策——比如使用何种数据库。而那些 Flask 所选择的——比如使用何种模板引擎则很容易替换。

Flask 基于 WerkzeugWSGI 工具箱和 Jinja2 模板引擎。实验中你将会知道 Jinja2 给予我们极大的方便,比如可以传递变量参数等。让我们的表示层动态的展示你想展示的信息,更详细的说明可参考Python Flask Web框架。

认识 MySQL 和简单的 SQL 语句

MySQL 作为一种关系型数据库,由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,一般中小型网站的开发都选择 MySQL 作为网站数据库。并且实验楼已经安装好 MySQL,故实验时只需使用即可。

我们要用到的 SQL 语句包括 select 语句,insert into 语句,create 语句,order by 子句,

Limit限制语法,natural join 语法。

创建myproject文件夹并激活virtualenv:

mkdir myprojectcdmyprojectvirtualenv venv. venv/bin/activate

在 virtualenv 中安装 Flask:

pipinstall -i http://mirrors.aliyuncs.com/pypi/simple flask

测试连接数据库代码:

插入数据:

importMySQLdbdb=MySQLdb.connect("localhost","root","","recommend")cursor=db.cursor()sql="create table user_anime(user int,anime int)"cursor.execute(sql)db.close()

简单说明一下上面的代码:

第一行导入连接 MySQL 的库

第二行通过指定参数(ip,用户名,密码,数据库)连接到某一个数据库

第三行使用 cursor() 方法获取操作游标

第四行为要执行的 SQL 语句,这句是创建一个名为 user_anime 表

第五行为执行 SQL 语句

查询Mysql 并取出数据

love=[]

DB=MySQLdb.connect("localhost","root","","recommend")

#获得数据库游标

c=DB.cursor()

#下面代码为实现从数据库中得到用户user所喜欢的番剧编号,以便判断重复

love=[]

#sql语句

sql="select anime_id  from user_anime where user_id=%s"%user

c.execute(sql)

#得到结果集

results=c.fetchall()

for line in results:

love.append(line[0])

一个Flask应用运行过程剖析

相信很多初学Flask的同学(包括我自己),在阅读官方文档或者Flask的学习资料时,对于它的认识是从以下的一段代码开始的:

from flask import Flask

app = Flask(__name__)

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

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

运行如上代码,在浏览器中访问http://localhost:5000/,便可以看到Hello World!出现了。这是一个很简单的Flask的应用。

然而,这段代码怎么运行起来的呢?一个Flask应用运转的背后又有哪些逻辑呢?如果你只关心Web应用,那对这些问题不关注也可以,但从整个Web编程的角度来看,这些问题非常有意义。本文就主要针对一个Flask应用的运行过程进行简要分析,后续文章还会对Flask框架的一些具体问题进行分析。

为了分析方便,本文采用 Flask 0.1版本 的源码进行相关问题的探索。

一些准备知识

在正式分析Flask之前,有一些准备知识需要先了解一下:

  • 使用Flask框架开发的属于Web应用。由于Python使用WSGI网关,所以这个应用也可以叫WSGI应用;

  • 服务器、Web应用的设计应该遵循网关接口的一些规范。对于WSGI网关,要求Web应用实现一个函数或者一个可调用对象webapp(environ, start_response)。服务器或网关中要定义start_response函数并且调用Web应用。关于这部分的内容可以参考: http://fanchunke.me/Python/wsgiref%E5%8C%85%E2%80%94%E2%80%94%E7%AC%A6%E5%90%88WSGI%E6%A0%87%E5%87%86%E7%9A%84Web%E6%9C%8D%E5%8A%A1%E5%AE%9E%E7%8E%B0%EF%BC%88%E4%B8%80%EF%BC%89/

  • Flask依赖于底层库werkzeug。相关内容可以参考: http://fanchunke.me/Flask/Werkzeug%E5%BA%93%E7%AE%80%E4%BB%8B/

本文暂时不对服务器或网关的具体内容进行介绍,只需对服务器、网关、Web应用之间有怎样的关系,以及它们之间如何调用有一个了解即可。

一个Flask应用运行的过程

1. 实例化一个Flask应用

使用app = Flask(name),可以实例化一个Flask应用。实例化的Flask应用有一些要点或特性需要注意一下:

  • 对于请求和响应的处理,Flask使用werkzeug库中的Request类和Response类。对于这两个类的相关内容可以参考: http://fanchunke.me/Flask/Werkzeug%E5%BA%93%E2%80%94%E2%80%94wrappers%E6%A8%A1%E5%9D%97/

  • 对于URL模式的处理,Flask应用使用werkzeug库中的Map类和Rule类,每一个URL模式对应一个Rule实例,这些Rule实例最终会作为参数传递给Map类构造包含所有URL模式的一个“地图”。这个地图可以用来匹配请求中的URL信息,关于Map类和Rule类的相关知识可以参考: http://fanchunke.me/Flask/Werkzeug%E5%BA%93%E2%80%94%E2%80%94routing%E6%A8%A1%E5%9D%97/

  • 当实例化一个Flask应用app(这个应用的名字可以随便定义)之后,对于如何添加URL模式,Flask采取了一种更加优雅的模式,对于这点可以和Django的做法进行比较。Flask采取装饰器的方法,将URL规则和视图函数结合在一起写,其中主要的函数是route。在上面例子中:

@app.route('/')
def index():
    pass

这样写视图函数,会将’/’这条URL规则和视图函数index()联系起来,并且会形成一个Rule实例,再添加进Map实例中去。当访问’/’时,会执行index()。关于Flask匹配URL的内容,可以参考后续文章。

  • 实例化Flask应用时,会创造一个Jinja环境,这是Flask自带的一种模板引擎。可以查看Jinja文档 (http://docs.jinkan.org/docs/jinja2/) ,这里先暂时不做相关介绍。

  • 实例化的Flask应用是一个可调用对象。在前面讲到,Web应用要遵循WSGI规范,就要实现一个函数或者一个可调用对象webapp(environ, start_response),以方便服务器或网关调用。Flask应用通过call(environ, start_response)方法可以让它被服务器或网关调用。

def __call__(self, environ, start_response):
    """Shortcut for :attr:`wsgi_app`"""
    return self.wsgi_app(environ, start_response)

注意到调用该方法会执行wsgi_app(environ, start_response)方法,之所以这样设计是为了在应用正式处理请求之前,可以加载一些“中间件”,以此改变Flask应用的相关特性。对于这一点后续会详细分析。

  • Flask应用还有一些其他的属性或方法,用于整个请求和响应过程。

2. 调用Flask应用时会发生什么

上面部分分析了实例化的Flask应用长什么样子。当一个完整的Flask应用实例化后,可以通过调用app.run()方法运行这个应用。

Flask应用的run()方法会调用werkzeug.serving模块中的run_simple方法。这个方法会创建一个本地的测试服务器,并且在这个服务器中运行Flask应用。关于服务器的创建这里不做说明,可以查看werkzeug.serving模块的有关文档。

当服务器开始调用Flask应用后,便会触发Flask应用的call(environ, start_response)方法。其中environ由服务器产生,start_response在服务器中定义。

上面我们分析到当Flask应用被调用时会执行wsgi_app(environ, start_response)方法。可以看出,wsgi_app是真正被调用的WSGI应用,之所以这样设计,就是为了在应用正式处理请求之前,wsgi_app可以被一些“中间件”装饰,以便先行处理一些操作。为了便于理解,这里先举两个例子进行说明。

例子一: 中间件SharedDataMiddleware

中间件SharedDataMiddleware是werkzeug.wsgi模块中的一个类。该类可以为Web应用提供静态内容的支持。例如:

import os
from werkzeug.wsgi import SharedDataMiddleware

app = SharedDataMiddleware(app, {
    '/shared': os.path.join(os.path.dirname(__file__), 'shared')
})

Flask应用通过以上的代码,app便会成为一个SharedDataMiddleware实例,之后便可以在http://example.com/shared/中访问shared文件夹下的内容。

对于中间件SharedDataMiddleware,Flask应用在初始实例化的时候便有所应用。其中有这样一段代码:

self.wsgi_app = SharedDataMiddleware(self.wsgi_app, {
                self.static_path: target
            })

这段代码显然会将wsgi_app变成一个SharedDataMiddleware对象,这个对象为Flask应用提供一个静态文件夹/static。这样,当整个Flask应用被调用时,self.wsgi_app(environ, start_response)会执行。由于此时self.wsgi_app是一个SharedDataMiddleware对象,所以会先触发SharedDataMiddleware对象的call(environ, start_response)方法。如果此时的请示是要访问/static这个文件夹,SharedDataMiddleware对象会直接返回响应;如果不是,则才会调用Flask应用的wsgi_app(environ.start_response)方法继续处理请求。

例子二: 中间件DispatcherMiddleware

中间件DispatcherMiddleware也是werkzeug.wsgi模块中的一个类。这个类可以讲不同的应用“合并”起来。以下是一个使用中间件DispatcherMiddleware的例子。

from flask import Flask
from werkzeug import DispatcherMiddleware

app1 = Flask(__name__)
app2 = Flask(__name__)
app = Flask(__name__)

@app1.route('/')
def index():
    return "This is app1!"

@app2.route('/')
def index():
    return "This is app2!"

@app.route('/')
def index():
    return "This is app!"

app = DispatcherMiddleware(app, {
            '/app1':        app1,
            '/app2':        app2
        })

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('localhost', 5000, app)

在上面的例子中,我们首先创建了三个不同的Flask应用,并为每个应用创建了一个视图函数。但是,我们使用了DispatcherMiddleware,将app1、app2和app合并起来。这样,此时的app便成为一个DispatcherMiddleware对象。

当在服务器中调用app时,由于它是一个DispatcherMiddleware对象,所以首先会触发它的call(environ, start_response)方法。然后根据请求URL中的信息来确定要调用哪个应用。例如:

  • 如果访问/,则会触发app(environ, start_response)(注意: 此时app是一个Flask对象),进而处理要访问app的请求;

  • 如果访问/app1,则会触发app1(environ, start_response),进而处理要访问app1的请求。访问/app2同理。

3. 和请求处理相关的上下文对象

当Flask应用真正处理请求时,wsgi_app(environ, start_response)被调用。这个函数是按照下面的方式运行的:

def wsgi_app(environ, start_response):
    with self.request_context(environ):
        ...

请求上下文

可以看到,当Flask应用处理一个请求时,会构造一个上下文对象。所有的请求处理过程,都会在这个上下文对象中进行。这个上下文对象是_RequestContext类的实例。

# Flask v0.1
class _RequestContext(object):
    """The request context contains all request relevant information.  It is
    created at the beginning of the request and pushed to the
    `_request_ctx_stack` and removed at the end of it.  It will create the
    URL adapter and request object for the WSGI environment provided.
    """

    def __init__(self, app, environ):
        self.app = app
        self.url_adapter = app.url_map.bind_to_environ(environ)
        self.request = app.request_class(environ)
        self.session = app.open_session(self.request)
        self.g = _RequestGlobals()
        self.flashes = None

    def __enter__(self):
        _request_ctx_stack.push(self)

    def __exit__(self, exc_type, exc_value, tb):
        # do not pop the request stack if we are in debug mode and an
        # exception happened.  This will allow the debugger to still
        # access the request object in the interactive shell.
        if tb is None or not self.app.debug:
            _request_ctx_stack.pop()

根据_RequestContext上下文对象的定义,可以发现,在构造这个对象的时候添加了和Flask应用相关的一些属性:

  • app ——上下文对象的app属性是当前的Flask应用;

  • url_adapter ——上下文对象的url_adapter属性是通过Flask应用中的Map实例构造成一个MapAdapter实例,主要功能是将请求中的URL和Map实例中的URL规则进行匹配;

  • request ——上下文对象的request属性是通过Request类构造的实例,反映请求的信息;

  • session ——上下文对象的session属性存储请求的会话信息;

  • g ——上下文对象的g属性可以存储全局的一些变量。

  • flashes ——消息闪现的信息。

LocalStack和一些“全局变量”

注意: 当进入这个上下文对象时,会触发_request_ctx_stack.push(self)。在这里需要注意Flask中使用了werkzeug库中定义的一种数据结构LocalStack。

_request_ctx_stack = LocalStack()

关于LocalStack,可以参考: (http://fanchunke.me/Flask/Werkzeug%E5%BA%93%E2%80%94%E2%80%94local%E6%A8%A1%E5%9D%97/) 。LocalStack是一种栈结构,每当处理一个请求时,请求上下文对象_RequestContext会被放入这个栈结构中。数据在栈中存储的形式表现成如下:

{880: {'stack': [<flask._RequestContext object>]}, 13232: {'stack': [<flask._RequestContext object>]}}

这是一个字典形式的结构,键代表当前线程/协程的标识数值,值代表当前线程/协程存储的变量。werkzeug.local模块构造的这种结构,很容易实现线程/协程的分离。也正是这种特性,使得可以在Flask中访问以下的“全局变量”:

current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
request = LocalProxy(lambda: _request_ctx_stack.top.request)
session = LocalProxy(lambda: _request_ctx_stack.top.session)
g = LocalProxy(lambda: _request_ctx_stack.top.g)

其中_request_ctx_stack.top始终指向当前线程/协程中存储的“请求上下文”,这样像app、request、session、g等都可以以“全局”的形式存在。这里“全局”是指在当前线程或协程当中。

由此可以看出,当处理请求时:

  • 首先,会生成一个请求上下文对象,这个上下文对象包含请求相关的信息。并且在进入上下文环境时,LocalStack会将这个上下文对象推入栈结构中以存储这个对象;

  • 在这个上下文环境中可以进行请求处理过程,这个稍后再介绍。不过可以以一种“全局”的方式访问上下文对象中的变量,例如app、request、session、g等;

  • 当请求结束,退出上下文环境时,LocalStack会清理当前线程/协程产生的数据(请求上下文对象);

  • Flask 0.1版本只有“请求上下文”的概念,在Flask 0.9版本中又增加了“应用上下文”的概念。关于“应用上下文”,以后再加以分析。

4. 在上下文环境中处理请求

处理请求的过程定义在wsgi_app方法中,具体如下:

def wsgi_app(environ, start_response):
    with self.request_context(environ):
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
        response = self.make_response(rv)
        response = self.process_response(response)
        return response(environ, start_response)

从代码可以看出,在上下文对象中处理请求的过程分为以下几个步骤:

  • 在请求正式被处理之前的一些操作,调用preprocess_request()方法,例如打开一个数据库连接等操作;

  • 正式处理请求。这个过程调用dispatch_request()方法,这个方法会根据URL匹配的情况调用相关的视图函数;

  • 将从视图函数返回的值转变为一个Response对象;

  • 在响应被发送到WSGI服务器之前,调用process_response(response)做一些后续处理过程;

  • 调用response(environ, start_response)方法将响应发送回WSGI服务器。关于此方法的使用,可以参考: http://fanchunke.me/Flask/Werkzeug%E5%BA%93%E2%80%94%E2%80%94wrappers%E6%A8%A1%E5%9D%97/

  • 退出上下文环境时,LocalStack会清理当前线程/协程产生的数据(请求上下文对象)。

基于Python的Flask的开发实战(第二节程序的基本结构)

1. 初始化

所有的flask程序都必须创建一个程序实例

web服务器使用wsgi接口协议,把接收客户端的请求都转发给这个程序实例来进行处理。这个程序实例就是flask对象

from flask import Flask
app = Flask(__name__)
#__name__决定程序的根目录,以便以后能找到相对于程序根目录的资源文件位置

2. 路由和视图函数

程序实例需要知道接收请求后,需要知道url请求应该运行哪些代码。所以保存了一个url和python函数的映射关系;这个映射关系就叫做路由

flask程序中路由的写法:

2.1 使用app.route装饰器,把修饰的函数注册为路由。例如

@app.route('/')
def index():
    return "<h1>Hello World</h1>"

#函数的名字不是必须写index的,只是和装饰器关联的时候写的函数名而已

#把index函数注册为程序根路径的处理程序。函数的返回值称为响应,是客户端接收的内容。

像index这样的函数称为试图函数,试图函数返回的响应可以是包含html的简单字符串,也可以是复杂的东西

2.2 可变url部分映射,使用特定的装饰器语法就可以

@app.route('/user/<name>')
def user(name):
    return "<h1>hello %s</h1>"%(name)

装饰器中的指定可变内容为name,name对user(name)函数中的传递参数,这2个部分内容必须一致

调用试图函数时候,flask会自动的将动态部分作为参数传入参数,这个函数中,参数用于生成个人的欢迎信息

#备注:路由中的动态部分默认使用字符串类型,可以使用int,float,path来定义;例如;path类型也是字符串,但不把斜线视作分隔符,而将其当做动态片段的一部分

3. 启动服务器

调用程序实例app的run方法启动flask集成开发的web服务器

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

debug=True代表的是调试模式,这个flask自带的run方法开启的服务器不适合在生产中使用,此处只用来测试

4. 一个完整的Flask程序

啥也不说,先上例子hello.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return '<h1>HelloWorld</h1>'

@app.route('/user/<name>')
def user(name):
    return "<h1>hello %s</h1>"%name



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

默认会开启服务器本机5000端口;127.0.0.1:5000

执行脚本python hello.py

浏览器测试http://127.0.0.1:5000/

     http://127.0.0.1:5000/user/xiaobai

5. 请求上下文

Flask使用请求上下文,临时把某些对象变为全局可用;例如

from flask import request

@app.route('/')
def index():
    user_agent = request.headers.get('User-Agent')
    return '<h1>your browser is %s</h1>'%(user_agent)

在这个视图函数中,我们把request当做全局变量使用,flask使用请求上下文让特定的变量在一个线程中全局可访问。于此同时却不会干扰其他线程

session:请求上下文;用户会话,用于存储请求之间需要“记住”的值的词典

激活请求上下文的后就可以使用request和session变量了

6. 程序上下文

current_app:程序上下文;当前激活程序的程序实例

g:程序上下文;处理请求时用作临时存储的对象

7. 请求映射关系表

接收请求,处理请求,,,之间有个映射表,要不然不知道该去执行什么代码。URL映射

from hello import app
print app.url_map
Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>,
  <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
  <Rule '/user/<name>' (HEAD, OPTIONS, GET) -> user>])

8. 请求钩子

有的时候在处理请求之前和之后,执行某些特定的代码是很有用的,这就用到了请求钩子

例如在请求之前创建数据库连接或者redis连接;或者是系统里面用户请求处理之前先验证用户的身份,是否激活,激活执行什么操作,没激活用户一直绑到固定页面去直到激活

为了避免每个试图函数中都使用重复的代码,flask提供了注册通用函数的功能;

也就是说只要写一个请求钩子-函数,整个程序实例全局都被应用了。

例如:在所有请求之前先验证下用户的认证状态

@before_app_request
def before_request():
    if current_user.is_authenticated:
        current_user.ping()
        if not current_user.confirmed and request.endpoint[:5] != 'auth.' and request.endpoint != 'static':
            return redirect(url_for('auth.unconfirmed'))

常见的4种钩子:

  • before_first_request:注册一个函数,在处理第一个请求之前运行

  • before_request:注册一个函数,每次请求之前运行

  • after_request:注册一个函数,没有未处理的异常抛出,每次请求之后运行

  • teardown_request:注册一个函数,有未处理的异常抛出,每次请求之后运行

在请求钩子和视图函数之间共享数据一般使用程序上下文g;

例如before_request处理程序可以从数据库中加载已登录用户,将其保存到g.user中,随后调用试图函数,试图函数再从g.user中获取用户

9. 基于Flask的http响应

flask调用试图函数处理请求,并把返回值作为响应的内容.大多情况下是一个简单的字符串或者json字符串;返回字符串常用于给对方提供接口的时候使用

http响应中很重要的一个内容是状态码,flask默认设置为200,这个代码表明请求已经被成功处理了

如果试图函数返回的响应需要不同的状态码,可以把状态码加到后面返回

例如

@app.route('/')
def index():
    return '<h1>Bad Request</h1>',400

试图函数返回的响应还可以接受第三个参数,第三个参数是一个字典类型的首部,可以添加到http响应中去,一般不用添加

如果不想返回这种好多个元素的元祖,可以使用Response对象来标准化下返回。

例如:创建一个响应对象,然后设置cookie

from flask import make_response

@app.route('/')
def index():
    response = make_response('<h1>This document carries a cookie!</h1>')
    response.set_cookie('answer',42)
    return response

还有一种特殊的响应类型,flask提供了一种基于302的跳转响应,这个响应由redirect函数来提供。指向的地址由Location首部提供,重定向的响应可以使用3个值形式的返回值生成。也可以再Response对象中设定

例如:

from flask import redirect

@app.route('/')
def index():
    return redirect('http://www.example.com')

还有一种特殊的响应类型,flask提供了一种错误响应。这个由abort函数来提供。abort抛出404异常,抛出异常后把控制权移交给web服务器

例如:

from flask import abort

@app.route('/user/<id>')
def get_user(id):
    user = load_user(id)
    if not user:
        abort(404)
    return '<h1>Hello,%s</h1>'%(user.name)

10. flask的扩展flask-script

这个例子主要是讲如何把flask扩展添加到程序中,并使用

例如下面你的例子是添加flask-script扩展,使用命令行参数增强程序的功能

使用命令行方式启动web服务器,而不是修改文件,给run方法传递参数

安装扩展

pip install flask-script

使用flask-script扩展,并把hello.py文件改为命令行参数启动的形式#添加的扩展默认会安装到flask.ext命名空间中

from flask import Flask
from flask.ext.script import Manager

app = Flask(__name__)
manager = Manager(app)

@app.route('/')
def index():
    return '<h1>HelloWorld</h1>'

@app.route('/user/<name>')
def user(name):
    return "<h1>hello %s</h1>"%name



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

flask-script扩展中添加了一个Manager的类,以上例子中,这个扩展初始化的方法是,把程序实例作为参数传递给构造函数来初始化主类的实例。后续其他flask扩展也基本是这个套路

这样修改之后,程序就可以使用一组基本的命令行选项来启动和调试了

python hello.py shell#在flask应用上下文环境中运行python shell,方便测试和调试web环境
python hello.py runserver#运行flask开发服务器,app.run()
python hello.py -h#显示帮助信息
python hello.py runserver --help
  usage: hello.py runserver [-h] [-t HOST] [-p PORT] [--threaded]
  [--processes PROCESSES] [--passthrough-errors] [-d]
  [-r]

python hello.py runserver -h 0.0.0.0 -p 80#这样就开启了本机的80端口,别的机器可以远程访问了