偷个懒,公号抠腚早报80%自动化——3.Flask速成大法

3,760 阅读33分钟

简述

在上一节中,我们编写了抓取新闻的爬虫脚本,每天早上八点定时抓取新闻保存到 MySQL数据库中。直接用DataGrip连下数据库,就可以查看爬取到的新闻了。不过, 并不是我们想要的最终形态。我希望新闻筛选的工作可以直接在手机上进行,而不是 每次都要打开电脑,打开DataGrip黑乎乎的页面,筛选,复制粘贴。在写这个APP之前, 要先学点web的东东,写点接口。Python中有比较流行的两个Flask和Django,笔者选择 的比较轻量级的Flask。在写《如何用Python投机倒把几天“暴富”》,有些同学私信我有 没有Flask的教程推荐。索性本节就来过一过Flask,下一节再来利用Flask来写API接口和 静态页面,以及直接生成公号文章样式。内容较多,比较枯燥,建议先收藏后慢慢观看~


1、Flask简介

一个轻量级的“微内核”Web应用框架,基于Werkzeug实现的WSGI套件和Jinja2模板引擎, 内核精简,易于扩展,花费较少的学习成本,即可能开发一个简单的网站。

相关文档


2、Flask开发环境搭建

直接通过pip命令安装即可:

pip install flask

这里要注意「全局安装」和「虚拟环境安装」的区别,之前有很多读者都问过这样的问题:

我明明在命令行里已经执行了pip install xxx库,但是进去pycharm,还是提示模块找不到?

对此,有两种可选的解决方案:

  • 方案一在PyCharm处的终端处执行pip安装命令(注:前面有个venv)

  • 方案二勾选 inherit global stie-packages

个人更倾向于第一种,为了解决维护不同项目对应不同版本的问题,Python使用了虚拟环境的概念, 在虚拟环境中安装第三方包只会作用到虚拟环境中,全局的Python解释器不受影响。在Python3中, 虚拟环境已成为一个内置模块,创建一个带虚拟环境的文件示例如下:

mkdir Test
cd Test
python -m venv venv

执行完上述命令后,Python会运行venv包,创建一个venv的虚拟环境,上面的两个venv参数分别为:

  • Python虚拟环境包的名称,固定写venv
  • 应用于这个特定的虚拟环境的名称,可以改成你喜欢的名字,不过笔者习惯命名为venv,切换到别 的项目时,都能快速的找到对应的虚拟环境。

虚拟环境创建后,需要激活后才能进入,通过下述命令「激活虚拟环境」:

source venv/bin/activate

执行完后会看到终端前缀多了个venv,激活虚拟环境后,终端会话的环境配置就会被修改, 此时键入Python或者pip,实际上调用的都是虚拟环境中的Python解释器。一个应用场景: 打开多个终端调试多个应用,每个终端窗口可以激活不同的虚拟环境,且不相互干扰。

注:如果你使用的是Python2或者Windows系统,如果想使用虚拟环境,要先通过pip命令 安装先安装一波virtualenvwrapper:pip install virtualenvwrapper。然后创建虚拟环境: virtualenv venv,最后是激活虚拟环境:venv\Scripts\activate.bat


3、最简单的Hello Flask

新建一个hello.py的脚本,内容如下:

# coding=utf-8
from flask import Flask

app = Flask(__name__)

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

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

在终端键入下述命令执行脚本:

python hello.py

运行后可以看到输出如下信息:

浏览器打开:http://127.0.0.1:5000,可以看到如图所示的Hello Flask!

服务启动后就会进入轮询,等待并处理请求,知道程序被终止,也可以直接按Ctrl+C停掉。 接着逐行解析一波代码:

  • 第1行:用于声明Python源文件的编码语法,该信息后续会被Python解析器用于解析源文件, 一般统一用utf-8。
  • 第2行:引入Flask类。
  • 第4行:实例化一个Flask类实例app,该实例接收包或模块名称作为参数,一般直接传入__name__, 让flask.helpers.get_root_path函数通过传入这个名字确定程序的根目录,以便获得静态文件和 模板文件的目录。
  • 第6-8行:app:route装饰器会将URL和执行的视图函数的关系保存到app.url_map属性上。 这三行简单点说就是:当浏览器访问服务器程序的根地址("/")时,Flask实例执行视图函数hello(), 然后返回:Hello Flask!
  • 第10行:其他文件引用此文件时,不会执行这个判断内的代码。
  • 第11行:启动服务,默认Flask只监听本地127.0.0.1地址和5000端口,如果你想修改端口,可以 传入参数「port=端口号」,想支持远程访问的话,则传入「host="0.0.0.0"」,你还可以设置 调试模式,只需传入参数「debug=True」,启用调试模式后,服务器会在代码修改后会自动重新 载入,并在发生错误的时候提供一个能获取错误上下文及可执行代码的调试页面。

4、flask-script模块

该模块的作用是:通过命令行的形式来操作Flask,使用命令行传参,而不仅仅是通过 app.run()函数来传。直接通过pip命令即可安装此模块:

pip install flask-script

新建一个manage.py的文件:

from flask_script import Manager
from flask import Flask

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

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

接着命令行键入:

python manage.py runserver -h ip -p 端口

除此之外的参数还有:-d(开启调试模式),-r(代码修改后自动加载),--help(查看帮助信息)。


5、路由与视图

上面这种:

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

定义了URL到Python函数间的映射关系,这种映射关系就叫路由。

0x1 动态路由

有时,可能有一些具有相同规则的URL,比如:

app.route("/login")
app.route("/register")

我们可以把这类URL抽象成一个URL模式,示例如下:

@app.route('/<do>')
def hello(do):
    return "<h1>当前请求:%s </h1>" % do

Flask支持这种动态形式的路由,动态部分默认是字符串,你也可以指定参数类型, 比如只接受整数:<int:id>,更多规则如下表所示:

字段标记 描述 示例
string 默认,接受任何没有斜杆"/"的文本 <string:name>
int 整数 <in:id>
float 浮点数 <float:price>
path 和string类似,但也接受斜杠 <path:address>
uuid 只接受uuid字符串 <string:uuid>
any 可以指定多种路径,但需要传入参数 <any(int,string):params>

另外有一点要注意:唯一URL,比如下面代表的是两个不同的URL:

CodingBoy.io/article/
CodingBoy.io/article

0x2 URL构造

在Flask中,可以使用url_for()函数来构造URL,接受一个视图函数名作为第一个参数, 也接受对应URL规则变量部分的命名参数,未知变量部分会被添加到URL末尾作为查询 参数。这里务必注意一点:操作的是函数,不是路由里的路径!!!! 通过函数构建,而不是直接用字符串拼接,主要出于下面两个目的:

  • 未来需要修改时,只需一次性修改URL,而不用到处替换;
  • URL构建会自动转义特殊字符和Unicode数据,而不用我们自己处理;

使用示例如下

with app.test_request_context():
    print(url_for('hello', uid=1))
    print(url_for('hello', uid=2, kind='测试'))

输出结果如下

/?uid=1
/?uid=2&kind=%E6%B5%8B%E8%AF%95

如果你想生成一个绝对路径,可以添加「_external=True」参数。

注:test_request_context可以在交互模式下产生请求上下文,不用app.run() 来运行这个项目,直接执行也会有Falsk上下文

0x3 请求方法限定

HTTP支持多种请求方法,默认情况下,路由只响应GET请求,如果不信可以自行用 PostMan对接口发起一个POST请求,结果如图所示:

响应码是405,表示不允许使用这种请求方法请求此URL,可以直接在 app.route装饰器中设置methods参数来修改。修改后的代码示例如下:

@app.route("/", methods=['GET', 'POST'])
def hello():
    return "Hello Flask!"

当然也支持其他的请求方法:PUT,HEAD,DELETE,OPTIONS,PATCH,想支持多种方法用逗号隔开即可。


6、模板

在Web开发中,我们经常会用到模板引擎,什么是模板?就是一个包含响应文本的文件, 用占位符(变量)标识动态部分,告诉模板引擎,其具体值需要从使用的数据中获取

0x1 视图与模板的关系

在前面的例子中,视图函数的主要作用是生成请求的响应,而实际开发中,视图函数有 两个作用:「处理业务逻辑」和「返回响应内容」。在大型项目中,如果把业务逻辑 和表现内容放在一起的话,会增加代码的复杂度和维护成本。而模板的作用就是: 「承担视图函数返回响应内容这一部分的职责,从而使得代码结构清晰,耦合度低。」 使用真实值替换变量,再(控制)返回最终得到的字符串,这个过程称作「渲染」。 Flask中默认使用「Jinja2」这个模板引擎来渲染模板。

0x2 Jinja2语法

通过一个简单的例子,引入模板,下面是一个没有使用模板的简单程序:

# coding=utf-8
from flask import Flask

app = Flask(__name__)
@app.route("/<user>")
def hello(user):
    if user == 'admin':
        return "<h1>管理员,您好!<h1>"
    else:
        return "<h1>%s, 您好!</h1>" % user
if __name__ == "__main__":
    app.run()

接着我们使用Jinja2模板进行改写,默认情况下,Flask会在「项目的templates子文件夹」 中寻找模板,我们新建一个templates文件夹,然后新建一个index.html,内容如下:

"<h1>{{name}},您好!<h1>"

接着修改下hello.py文件,修改后的内容如下:

from flask import Flask, render_template

app = Flask(__name__)

@app.route("/<user>")
def hello(user):
    if user == 'admin':
        return render_template("index.html", user="管理员")
    else:
        return render_template("index.html", user=user)

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

接着运行hello.py,浏览器键入以下地址,对应输出结果如下:

http://127.0.0.1:5000/admin     # 输出结果:管理员,您好!
http://127.0.0.1:5000/jay       # 输出结果:jay,您好!

以上就是一个简单的模板使用示例,通过调用Flask提供的render_template函数, 生成了一个模板,第一个参数是模板的名称,随后的参数是键值对,表示模板中 变量对应的真实值。接着详细讲解一波Jinja2的语法:

  • 1.变量

Jinja2使用**{{变量名}}**来表示一个变量,除了基本数据类型外还可以使用列表,字段 或对象等复杂类型,示例如下:

<h1> 账号:{{ user['name'] }},密码:{{ user['passwd'] }}
  • 2.控制结构

Jinja2提供了多种控制结构,比如常见的判断和循环结构,用于修改模板的渲染流程。 我们把上面判断的逻辑也放到模板里,示例如下:

{# 注释,不会输出到浏览器中 #}
{% if user == 'admin' or user == '管理员' %}
    <h1> 管理员,您好! </h1>
{% else %}
    <h1> {{ user }},您好!</h1>
{% endif %}

{# 循环打印 #}
{% for num in num_list %}
    <h2>{{ num }}</h2>
{% endfor %}

接着修改下hello.py文件:

@app.route("/<user>")
def hello(user):
    return render_template("index.html", user=user, num_list=[1, 2, 3])

键入不同的URL,对应浏览器的输出结果如下:

http://127.0.0.1:5000/admin
管理员,您好!
1
2
3

http://127.0.0.1:5000/jay
jay,您好!
1
2
3
  • 3.

在Python中如果有一些很常用的代码,我们会习惯性地抽取成一个函数,在Jinji2中, 可以使用宏来实现。语法如下:

# 创建宏
{% macro 标签名(key=value) %} 
    常用代码
{% end macro %}

# 调用宏
{{ 标签名(key=value) }}

如果宏比较多,可以抽到单独的HTML中,再import进来。

{% import 'xxx.html' as 别名 %}
{{ 别名.标签名(key=value) }}
  • 4.模板继承

另一种代码复用的方式:模板继承,和Python中类继承一样,需要一个基模板,用 {% block XXX %}{% endblock %} 标识一个代码块,可以在子模块中重载。 代码示例如下:

# 基模板:base.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    {% block head %}
        <title>{% block title %} Title {% endblock %} </title>
    {% endblock %}
</head>
<body>
    {% block body %}
    {% endblock %}
</body>
</html>

# 子模板:son.html
{% extends "base.html" %}
{% block title %}Son{% endblock %}
{% block head %}
    {{ super() }}
{% endblock %}
{% block body %}
<h1>Hello Flask!</h1>
{% endblock %}

代码简述:

第一行代码使用extends命令,表明该模板继承自base.html,接着重写了title,head和 body代码块,上面的super()函数用于获取基模板原先的内容。

  • 5.inclue

可以用include语句包含一个模板,在渲染时会把include语句对应位置添加被包含的模板, 使用示例如下:

{% include 'header.html' %}

另外还可以使用「ignore missing」标记,如果模板不存在,Jinja2会忽略此语句,示例如下:

{% include 'header.html' ignore missing %}
  • 6.赋值

可以使用set标签来进行赋值,示例如下:

{% set a,b = 1,2}
<h1>{{ a }}{{ b }}</h1>
  • 7.内建过滤器

过滤器的本质就是函数,有时需要修改,格式化或者执行变量间的计算,而在模板中是不能 直接调用Python中的某些方法的,可以用到过滤器。模板的过滤器支持链式调用:

{{ "hello flask" | reverse | upper}},

这串代码就是反转+转换成大写,常见的内建过滤器有字符串和列表两种。 字符串操作过滤器如下表所示:

过滤器 描述
safe 禁用转义
capitalize 把变量值的首字母转成大写,其余字母转小写
lower 把值转成小写
upper 把值转成大写
title 把值中的每个单词的首字母都转成大写
reverse 字符串反转
format 格式化输出
striptags 渲染之前把值中所有的HTML标签都删掉
truncate 字符串截断

列表操作过滤器如下表所示:

过滤器 描述
first 取第一个元素
last 取最后一个元素
length 获取列表长度
sum 列表求和
sort 列表排序
  • 8.自定义过滤器

直接在py文件中编写,代码示例如下:

# 1.定义过滤器
def do_listreverse(li):
    temp_li = list(li)
    temp_li.reverse()
    return temp_li

# 2.添加自定义过滤器
app.add_template_filter(do_listreverse, 'listreverse')

总结

宏,继承和包含都用实现代码复用,宏功能类似于函数,可以传参,需要定义调用; 继承本质是代码替换,一般用来实现多个页面中重复不变的区域;而包含是直接将 目标模板文件整个渲染出来。


7、请求与相应

在Flask中,HTTP请求被封装成了Request对象,而HTTP响应则被封装成了Response对象。 因此Flask应用开发的逻辑处理,都是基于这两个对象。

0x1 Request请求

Flask会将WSGI服务器转发的http请求数据封装为一个Request对象,这个对象中包含了请求的 相关信息,可以通过下表中的属性来获取对应的信息。

属性 描述 数据类型
form 记录请求中的表单数据。 MultiDict
args 记录请求中的查询参数。 MultiDict
cookies 记录请求中的cookie。 Dict
headers 记录请求中的报文头。 EnvironHeaders
method 记录请求使用的HTTP方法。 string
environ 记录WSGI服务器转发的环境变量。 Dict
url 记录请求的URL地址。 string
  • 1.读取request的查询参数

浏览器以GET请求的方式提交的表单数据,Flask会将其存储在request实例的args,也可以用values属性 来查询,读取代码示例如下:

# coding=utf-8
from flask import Flask, request, json

app = Flask(__name__)
@app.route("/index")
def index():
    return '''
           <form method="GET" action="/login">
               <input type="text" placeholder="账号" name="user"> <br />
               <input type="text" placeholder="密码" name="pawd"> <br />
               <input type="submit" value="登录">
           </form>
       '''
@app.route("/login")
def login():
    msg = ""
    if 'user' in request.args:
        msg += request.args['user'] + ':'
    msg += request.values.get('pawd','')
    return msg
if __name__ == "__main__":
    app.run(debug=True)

代码运行后,打开:http://127.0.0.1:5000/index,浏览器:

输入账号密码后,跳转到:http://127.0.0.1:5000/login?user=zpj&pawd=123, 浏览器:

  • 2.读取request的表单数据

浏览器以GET请求的方式提交的表单数据,Flask会将其存储在request实例的form中,可以使用[]操作符 读取指定键值。读取代码示例如下:

# coding=utf-8
from flask import Flask, request, json

app = Flask(__name__)

@app.route("/index")
def index():
    return '''
           <form method="POST" action="/login">
               <input type="text" placeholder="账号" name="user"> <br />
               <input type="text" placeholder="密码" name="pawd"> <br />
               <input type="submit" value="登录">
           </form>
       '''

@app.route("/login", methods=['POST'])
def login():
    msg = ""
    msg += request.form['user'] + ':'
    msg += request.form['pawd']
    return msg

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

代码运行后,打开:http://127.0.0.1:5000/index,浏览器:

输入账号密码后,跳转到:http://127.0.0.1:5000/login,浏览器:

0x2 Response响应

和Request对应,Response用于给浏览器发送响应信息,根据视图函数的返回结果。这个视图函数 就是我们路由下面的函数,视图函数的返回值会自动转换成一个响应对象,转换逻辑如下:

  • 1、返回值是合法的响应对象,从视图直接返回;
  • 2、返回值是字符串,会用字符串和默认参数创建以字符串为主体,返回码为200,MIME类型为 text/html的werkzeug.wrappers.Response响应对象。
  • 3、返回值是元组,其中的元素可以提供额外信息,但格式必须是(response,status,headers) 的形式,至少包含一个元素,status值会覆盖状态码,headers可以是字典或列表,作为额外的消息头。
  • 4、如果都不是,Flask会假设返回值是合法的WSGI程序,通过Response.force(rv.request.environ) 转换为一个请求对象。除了通过return方式返回,还可以显式地调用make_response函数返回,好处是可以 设置一些额外的信息,示例如下:
def hello():
    resp = make_response("Hello Flask!", 250)
    return resp

另外,现在的API接口都是返回JSON格式的,可以用jsonify包装下,修改后的示例代码如下:

# coding=utf-8
from flask import Flask, make_response, jsonify
from werkzeug.wrappers import Response

app = Flask(__name__)

@app.route("/", methods=['GET', 'POST'])
def hello():
    resp = make_response({'code': '200', 'msg': '请求成功', 'data': [{'data_1': ['数据', '数据'], 'data_2': ['数据', '数据']}]})
    return resp

class JsonResponse(Response):
    @classmethod
    def force_type(cls, response, environ=None):
        if isinstance(response, dict):
            response = jsonify(response)
        return super(JsonResponse, cls).force_type(response, environ)

app.response_class = JsonResponse

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

请求接口输出如下:

{
    "code": "200",
    "data": [
        {
            "data_1": [
                "数据",
                "数据"
            ],
            "data_2": [
                "数据",
                "数据"
            ]
        }
    ],
    "msg": "请求成功"
}

也可以用Flask的json模块的dumps()函数将数组或字典对象转换为JSON字符串,代码示例如下:

data = {'code': '200', 'msg': '请求成功', 'data': [{'data_1': ['数据', '数据'], 'data_2': ['数据', '数据']}]}
return json.dumps(data),200,[('Content-Type','application/json;charset=utf-8')]
  • 3.设置Cookie Response类中提供了set_cookie()函数用于设置客户端的cookie,如果要设置cookie,需要我们自己构建Response实例 (通过make_response),可选参数如下:
set_cookie(
 key, //键
 value='', //值
 max_age=None, //秒为单位的cookie寿命,None表示http-only
 expires=None, //失效时间,datetime对象或unix时间戳
 path='/', //cookie的有效路径
 domain=None, //cookie的有效域
 secure=None, 
 httponly=False)

8、重定向与会话

Web开发中经常需要处理重定向和会话,Flask中内建了「redirect」和「session」来对它们进行处理。

0x1 重定向

页面重定向非常常见,最常用的就是登陆状态判断,如果没登陆将网页重定向到登录页,Flask中可以使用redirect 对象对其进行处理,状态码默认为302,可以传入code参数来修改,一般是:301,302,303,305和307, 简单的代码示例如下:

# coding=utf-8
from flask import Flask, redirect

app = Flask(__name__)

user_name = ''

@app.route('/article')
def article():
    if user_name == '':
        # 如果用户名为空,重定向跳转到登录页
        return redirect('/login')
    else:
        return "文章页"

@app.route("/login")
def login():
    global user_name
    user_name = 'admin'
    return "登录成功!"

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

运行后操作流程如下:

浏览器键入:http://127.0.0.1:5000/article
自动跳转到:http://127.0.0.1:5000/login,显示登录成功
再次访问:http://127.0.0.1:5000/article,显示文章页

0x2 会话

我们可以把数据存储在用户会话(session)中,用户会话是一种私有存储,默认情况下保存在客户端cookie中。 会话主要为了解决两个问题:「访问者的标识和访问者信息记录」。浏览器第一次访问服务器,服务器在其 cookie中设置一个唯一的会话ID,浏览器后续对服务器的访问头中将自动包含该信息,服务器通过这个ID号来 区分不同的访问者。session依赖于cookie,一般存储在服务器,Flask提供了session对象来操作用户会话, 可以使用[]操作符读取或者设置指定键值,默认情况下,Flask将会话对象加密后存储在客户端的cookie里, 因此必须要应用实例的secret_key属性配置一个加密种子才能使用session。用法示例如下:

# 设置session
session['name'] = 'jay'
# 读取session
session.get('name')
# 配置加密种子(两种方法二选一)
app.secret_key = '123456' 
app.config['SECRET_KEY']='123456'

9、静态文件管理

静态文件就是那些不会被改变的文件,例如:图片,CSS样式文件,JavaScript脚本文件和字体文件等。 Flask默认会在根目录中名为static的子目录中寻找静态文件,所以如果需要用到静态文件可以创建一个 static的文件夹,然后把静态文件丢里面。可以参考下面这样的结构来组织项目:

static/
    css/
        lib/
            bootstrap.css
        style.css
        home.css
    js/
        lib/
            jquery.js
            chart.js
        home.js
    img/
        logo.svg
        favicon.ico

另外,不要在模板中写死静态文件路径,应该使用url_for函数生成路径,示例如下:

url_for('static', filename='css/style.css')

当然,如果你想修改静态文件的真实目录,可以在Flask构造函数中传入参数:static_folder='文件夹名'。 另外,为了获得更好的处理能力,建议使用Nginx或其他Web服务器管理静态文件,图片这类资源可以 托管到CDN平台上。(比如七牛云)


10、蓝图

蓝图(Blueprint),定义了可用于单个应用的视图,模板,静态文件等等的集合。通俗点理解就是 一个实现应用模块化的好工具,使用蓝图能使得项目层次更加清晰,更易于开发和维护,通常作用于 相同URL前缀的路由。先来看一个没使用蓝图的示例:

# coding=utf-8
from flask import Flask

app = Flask(__name__)

@app.route('/user/index')
def user_index():
    return 'user_index'

@app.route('/user/show')
def user_show():
    return 'user_show'

@app.route('/user/add')
def user_add():
    return 'user_add'

@app.route('/admin/index')
def admin_index():
    return 'admin_index'

@app.route('/admin/show')
def admin_show():
    return 'admin_show'

@app.route('/admin/add')
def admin_add():
    return 'admin_add'

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

上面的代码挺整齐的,不过有几个问题:

  • 如果user和admin的功能不止上面的几个,而是好几百呢,代码会非常庞大臃肿。
  • 大型的项目都是多人协作的,所有人都在这里文件里开发的话,处理合并冲突会很头痛。
  • 如果哪天这两个用户模块不要了,还需要一行行的去找,然后删代码。

我们使用蓝图来把user和admin拆分成两个不同的.py文件,admin.py 文件内容如下:

# coding=utf-8
from flask import Blueprint

admin = Blueprint('admin', __name__,url_prefix='/admin')

@admin.route('/index')
def admin_index():
    return 'admin_index'

@admin.route('/show')
def admin_show():
    return 'admin_show'

@admin.route('/add')
def admin_add():
    return 'admin_add'

user.py 文件内容如下:

# coding=utf-8
from flask import Blueprint

user = Blueprint('user',__name__)

@user.route('/index')
def user_index():
    return 'user_index'

@user.route('/show')
def user_show():
    return 'user_show'

@user.route('/add')
def user_add():
    return 'user_add'

注册蓝图,hello.py的代码内容如下:

# coding=utf-8
from flask import Flask
from admin import *
from user import *

app = Flask(__name__)
app.register_blueprint(admin)
app.register_blueprint(user, url_prefix='/user')

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

利用app.url_map函数,查看所有的路由,打印结果如下:

Map([<Rule '/admin/index' (GET, HEAD, OPTIONS) -> admin.admin_index>,
 <Rule '/admin/show' (GET, HEAD, OPTIONS) -> admin.admin_show>,
 <Rule '/admin/add' (GET, HEAD, OPTIONS) -> admin.admin_add>,
 <Rule '/user/index' (GET, HEAD, OPTIONS) -> user.user_index>,
 <Rule '/user/show' (GET, HEAD, OPTIONS) -> user.user_show>,
 <Rule '/user/add' (GET, HEAD, OPTIONS) -> user.user_add>,
 <Rule '/static/<filename>' (GET, HEAD, OPTIONS) -> static>])

url_prefix这个参数用于设置request.url中的url前缀,另外只有满足前缀的请求才会 通过注册的蓝图的视图方法处理请求并返回。可以写在子模块中,也可以在register_blueprint 注册蓝图的时候传入,只需传入一次!之后打开http://127.0.0.1:5000/admin/index,可以看到 如图所示的结果:


10、g对象和钩子函数

有时在处理请求前后,执行某些特定代码是非常有用的,这就用到了请求钩子,比如:请求前创建db链接, 验证用户身份等,flask提供了注册通用函数的功能,只需要写一个请求钩子——函数,整个程序实例全局都被应用, 比如请求前验证用户状态的例子,没登陆跳转登录页面等。钩子函数需要借助Flask的全局变量g,g作为中间变量, 在钩子函数和视图函数间传递数据。

0x1 g对象

g,global,g对象是专门用来保存用户数据的,存储的数据在全局都可以使用。代码示例如下:

from flask import g, request

@app.route('/')
def index():
    user = request.args.get('user')
    g.user = user   # 保存用户数据

0x2 钩子函数

Flask提供下述四种钩子函数:

  • before_first_request:在第一次请求前调用,可以在此方法内做一些初始化操作。
  • before_request:在每次请求前调用,一般做校验,如果校验不成功,可以在这个方法内直接响应,直接return的话,不会执行视图函数。
  • after_request:在执行完视图函数之后会调用,并把视图函数生成的响应传入,可以在此方法中对响应做最后一步同意的处理。
  • teardown_request:每一次请求后都会调用,会接收一个参数——服务器出现的错误信息。

写一个程序来验证下钩子函数的执行流程(运行后,访问两次):

127.0.0.1 - - [30/Aug/2018 10:53:42] "GET / HTTP/1.1" 200 -
before_first_request
before_request
after_request
teardown_request

127.0.0.1 - - [30/Aug/2018 10:53:45] "GET / HTTP/1.1" 200 -
before_request
after_request
teardown_request

11、上下文

上下文相当于一个容器,保存了Flask程序运行过程中的一些信息,根据管理机制分为两种:

  • 请求上下文(RequestContext) Request:请求的对象,封装了Http请求的内容; Session:根据请求中的cookie,重载访问者相关的会话信息。

  • 程序上下文(AppContext) g:处理请求时用作临时存储的对象,保存的是当前请求的全局变量,不同的请求会有不同的全局变量! current_app:当前运行程序的程序实例,保存的是应用程序的变量,比如可以使用current_app.name获取当前应用的名称, 也可以在current_app中存储一些配置信息,变量等,使用示例:current_app.text = 'value'。

Flask中,而关于上下文的管理可以分为三个阶段:

  • 请求进来时:将request,session封装到RequestContext类中,app, g封装在AppContext类中, 并通过LocalStack将RequestContext和AppContext放入Local类中。
  • 视图函数:通过localproxy -> 偏函数 -> localstack -> local取值。
  • 请求结束前:执行save.session() -> 各自执行pop() -> 清除local中的数据。

12、异常处理

在开发中,后台发生异常,但又不想把异常显示给用户看,或者需要同一处理的时候,可以使用abort()函数 主动抛出异常,再捕获异常,然后返回一个美化后的页面,最常见的就是404了,代码示例如下:

@app.route("/test")
def test():
abort(404)

@app.errorhandler(404)
def error(e):
    return "一个精美的404页面"

代码执行后,浏览器键入:http://127.0.0.1:5000/test,可以看到如图所示的结果:

另外,你也可以把所有异常处理些写到一个蓝图中,代码示例如下:

# coding=utf-8
from flask import Blueprint, abort

exception = Blueprint('exception', __name__)

@exception.errorhandler(404)
def error(e):
    return "一个精美的404页面"

# 注册蓝图
from error import exception
app.register_blueprint(exception, url_prefix='/error')

13、ORM框架——SQLAlchemy

使用对象映射关系(Object-Relational Mapper)ORM框架来操作数据库。所谓的ORM框架就是:

将底层的数据操作指令抽象成高层的面向对象操作

不用再写繁琐的SQL操作语句,利用ORM框架可以简化成对Python对象的操作。

表映射成类行作为实例字段作为属性.

ORM在执行对象操作时会将对应操作转换为数据库原生语句。Python中用得最广泛的ORM框架是SQLAlchemy。

0x1 安装flask-sqlalchemy

直接使用pip命令安装即可

pip install flask-sqlalchemy

另外SQLAlchemy本身无法操作数据库,依赖于pymysql等第三方库,里面有个Dialect模块专门用于 和数据API交流,根据配置文件的不同而调用不同的数据库API,从而实现对数据库的操作。数据库使用 URL限定,常见的数据库引擎与其对应的URL如下表所示:

数据库引擎 URL
MySQL mysql+pymysql://username:password@hostname/database
Postgres postgresql://username:password@hostname/database
SQLite (Unix,开头四个斜线) sqlite:////absolute/path/to/database
SQLite (Windows) sqlite:///c:/absolute/path/to/database
Postgres postgresql://username:password@hostname/database
Oracle oracle://username:password@hostname/database

参数简述

  • username:登录数据库的用户名。
  • password:登录数据库的密码。
  • hostname:SQL服务所在的主句,可以是本地也可以是远程。
  • database:使用的数据库。

0x2 连接数据库

连接MySQL数据库的代码示例如下:

from sqlalchemy import create_engine

engine = create_engine('mysql+pymysql://jay:zpj12345@localhost:3306/todo')
with engine.connect() as con:
    rs = con.execute("SELECT 1")
    print(rs.fetchone())

输出结果如下

(1,)

另外还可以通过下面这样的方法来初始化SQLAlchemy,代码示例如下:

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
app = Flask(__name__)
    db.init_app(app)

0x3 使用原生SQL

sqlalchemy支持直接执行原生的SQL语句,代码示例如下:

from sqlalchemy import create_engine

engine = create_engine('mysql+pymysql://jay:zpj12345@localhost:3306/todo')
with engine.connect() as con:
    con.execute("CREATE TABLE IF Not Exists note(_id INT AUTO_INCREMENT PRIMARY KEY, tran TEXT, status int)")
    con.execute("INSERT INTO note(tran, status) VALUES ('吃饭', 1)")
    con.execute("INSERT INTO note(tran, status) VALUES ('睡觉', 1)")
    rs = con.execute("SELECT * FROM note")
    for row in rs:
        print(row)

代码输出结果如下:

(1, '吃饭', 1)
(2, '睡觉', 1)

0x4 ORM模型与基本操作

演示一波Flask-SQLAlchemy的基本操作:

  • 1.建表create_all,对应的删除表可以用drop_all
# models.py
from manager import db

class Note(db.Model):
    __tablename__ = 'note'
    _id = db.Column(db.INTEGER, primary_key=True, autoincrement=True)
    tran = db.Column(db.TEXT)
    status = db.Column(db.INT,default=0)

db.create_all() # 创建表
  • 2.插入数据
from model import Note
from manager import db

def create_note():
    # 创建一个新对象
    note1 = Note()
    note1.tran = "吃饭"
    note1.status = "1"

    note2 = Note()
    note2.tran = "睡觉"
    note2.status = "2"

    # 将新建笔记添加到数据库会话中
    db.session.add(note1)
    db.session.add(note2)

    # 将数据库会话中的变动提交到数据库中,如果不commit,数据库内容是不会变化的
    db.session.commit()

create_note()
  • 3.删除数据
def delete_note():
    # 获取笔记对象(这里是获取_id=1的记录)
    note = Note.query.filter_by(_id=1).first()

    # 删除笔记
    db.session.delete(note)

    # 提交数据库会话
    db.session.commit()

delete_note()
  • 4.修改数据
def update_note():
    # 获取笔记对象(这里是获取_id=2的记录)
    note = Note.query.filter_by(_id=2).first()

    # 修改笔记内容
    note.tran = "打豆豆"

    # 提交数据库会话
    db.session.commit()

update_note()
  • 5.查询数据

说到查询,必然有查询条件,SQLAlchemy提供了如下表所示的常用查询函数:

函数 描述 使用示例
filter_by 精确查询 filter_by(xxx='xxx')
filter 模糊查询 filter(xxx.endWith('xxx'))
get(主键) 根据主键查询,一般为id get(1)
not_() 逻辑非,也可以直接把==换成!= not_(xxx='xxx')
and_() 逻辑与 and_(xxx='xxx')
or_() 逻辑或 or_(xxx='xxx')
in_() 在某个范围里 XXX.xxx.in_((1,2,3))
notin_() 不在某个范围内 XXX.xxx.notin_((1,2,3))
first() 返回查询到的一个对象 XXX.query.first()
all() 返回查询到的所有对象 XXX.query.all()
order_by() 排序 XXX.order_by(xxx.xxx.desc())
limit() 限制返回条数 XXX.limit(3)
offset() 设置偏移量 XXX.offset()
count() 返回记录的总条数 xxx.count()

代码示例如下:

from sqlalchemy import not_, or_

def query_all():
    notes = Note.query.all()
    print("查询全部数据:", notes)
    note = Note.query.filter(not_(or_(Note.tran == '吃饭', Note.tran == '睡觉'))).first()
    print(note._id, ":", note.tran, ":", note.status)

if __name__ == '__main__':
    # 先插入几条数据
    create_note()
    create_note()
    query_all()

输出结果如下:

查询全部数据: [<Note 2>, <Note 3>, <Note 4>, <Note 5>, <Note 6>]
2 : 打豆豆 : 2

14.Web表单插件——Flask-WTF

Flask中一般不会直接用原始表单,而是通过Flask-WTF扩展,它简单继承了WTForms,包括CSRF (跨域请求伪造),验证表单数据的功能,文件上传以及Google内嵌的验证码。

0x1 WTForms支持的HTML标准字段

字段类型 说明
StringField 文本字段
TextAreaField 多行文本字段
PasswordField 密码文本字段
HiddenField 隐藏文本字段
DateField 文本字段,值为datetime.date格式
DateTimeField 文本字段,值为datetime.datetime格式
IntegerField 文本字段,值为整数
DecimalField 文本字段,值为decimal.Decimal
FloatField 文本字段,值为浮点数
BooleanField 复选框,值为True和False
RadioField 一组单选框
SelectField 下拉列表
SelectMultipleField 下拉列表,可选择多个值
FileField 文件上传字段
SubmitField 表单提交按钮
FormField 把表单作为字段嵌入另一个表单
FieldList 一组指定类型的字段

0x2 WTForms验证函数

验证函数 说明
Email 验证电子邮件地址
EqualTo 比较两个字段的值,常用于要求输入两次密码进行确认的情况
IPAddress 验证IPv4网络地址
Length 验证输入字符串的长度
NumberRange 验证输入的值在数字范围内
Optional 无输入值时跳过其他验证函数
Required 确保字段中有数据
Regexp 使用正则表达式验证输入值
URL 验证URL
AnyOf 确保输入值在可选值列表中
NoneOf 确保输入值不在可选列表中

0x3 写个简单的例子

接着我们来编写一个注册表单的例子:

# coding=utf-8
from flask import Flask, request, render_template
from flask_wtf import FlaskForm
# 导入自定义表单需要的字段
from wtforms import SubmitField, StringField, PasswordField
# 导入表单验证
from wtforms.validators import DataRequired, EqualTo

app = Flask(__name__)
app.config['SECRET_KEY'] = '123456'


# 自定义表单类
# StringField和PasswordField用于区分文本框类型
# 第一个参数是label值,第二个参数validators是要验证的内容
class RegisterForm(FlaskForm):
    username = StringField('用户名:', validators=[DataRequired()])
    password = PasswordField('密码:', validators=[DataRequired()])
    password2 = PasswordField('确认密码:', validators=[DataRequired(), EqualTo('password', '两次输入的密码不一致!')])
    submit = SubmitField('注册')


@app.route("/register", methods=['GET', 'POST'])
def register():
    # 实例化注册表单类
    register_from = RegisterForm()
    if request.method == 'POST':
        # 获取请求参数参数
        username = request.form.get('username')
        password = request.form.get('password')
        password2 = request.form.get('password2')
        # 调用validation_on_submit,一次性执行完所有验证函数的逻辑
        if register_from.validate_on_submit():
            return '注册成功!'
        else:
            return '前后密码不一致!'
    return render_template('register.html', form=register_from)


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

# templates/register.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form method="post">
        {# 设置scrf_token #}
        {{ form.csrf_token() }}
        {{ form.username.label }}&nbsp;&nbsp;{{ form.username }}<br>
        {{ form.password.label }}&nbsp;&nbsp;{{ form.password }}<br>
        {{ form.password2.label }}&nbsp;&nbsp;{{ form.password2 }}<br>
        <br>
        {{ form.submit }}
    </form>
</body>
</html>

输入一波一样的密码和不一样的密码,浏览器的输出结果如下所示:

另外有一点要注意:

使用Flask-WTF需要配置参数SECRET_KEYCSRF_ENABLED是为了**CSRF(跨站请求伪造)**保护。 SECRET_KEY用于生成加密令牌,当CSRF激活的时候,该设置会根据设置的密钥生成加密令牌。


15.一个简单通用的Flask项目结构

Flask基础学得差不多了,接着我们来规范下项目结构,一个比较简单通用的项目结构如图:

简述下结构

  • app:整个项目的包目录。
  • models:数据模型。
  • static:静态文件,css,JavaScript,图标等。
  • templates:模板文件。
  • views:视图文件。
  • config.py:配置文件。
  • venv:虚拟环境。
  • manage.py:项目启动控制文件。
  • requirements.txt:项目启动控制文件。

创建流程

__init__.py中初始化app实例,代码如下:

from flask import Flask

app = Flask(__name__)

views.py中写个简单的index路由:

from app import app
from flask import render_template

@app.route('/')
def index():
    return render_template("index.html")

templates文件夹创建一个index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Hello Flask!</h1>
</body>
</html>

接着键入:python manage.py runserver,浏览器打开:http://127.0.0.1:5000/

项目结构基本就弄好了,接着我们把项目丢到远程服务器上。


16.把Flask项目部署到云服务器上

0x1 代码上传至云服务器

有两种可选的方法,最简单的就是通过FTP/SFTP工具直接传。另外一种是把项目托管到 类似于Github这类代码托管平台,然后直接ssh连云服务器,通过git clone命令把项目 拷贝到服务器上,这里笔者直接用的第一种方法,把项目上传到云服务器上。

0x2 安装nginx

Nginx是一款轻量级、性能强、占用资源少,能很好的处理高并发的反向代理软件。 Flask自带的Web Server只能开单线程,自己测试还行,放到线上就不行了,这里 我们用到Nginx,直接通过apt-get命令进行安装,命令如下:

apt-get install nginx

安装完后,外网访问服务器的公网ip,出现下述页面说明安装成功:

Nginx安装完,会默认创建一个目录:/var/www/,直接通过命令把我们的 项目移动到这个路径下。

mv AutoPubNews /var/www/AutoPubNews

0x3 安装配置uwsgi

WSGI是一种WEB服务器,或者叫网关接口,Web服务器(如nginx)与应用服务器 (如uWSGI)通信的一种规范(协议)。而uWSGI实现了WSGI的所有接口,是一个 快速、自我修复、开发人员和系统管理员友好的服务器。uWSGI代码完全用C编写, 效率高、性能稳定。举个例子:uWSGI把HTTP协议转化成WSGI协议,让Python可以 直接使用。直接键入pip命令安装:

pip install uwsgi

一般都是能直接安装完成的,如果出错了可以试试先安装libpython3.x-dev, 比如笔者的版本是3.5:

apt-get install libpython3.5-dev

接着配置一下uwsgi,在项目里新建一个config.ini作为配置文件:

vim config.ini

添加下述内容:

[uwsgi]
# uwsgi 启动时所使用的地址与端口
socket = 127.0.0.1:8001 # 可以使用其他端口                                                  

# 指向网站目录                                                                              
chdir = /var/www/AutoPubNews                                                                   

# python 启动程序文件                                                                       
wsgi-file = manage.py                                                                      

# python 程序内用以启动的 application 变量名                                                
callable = app                                                                              

# 处理器数                                                                                  
processes = 4                                                                               

# 线程数                                                                                    
threads = 2                                                                                 

#状态检测地址
stats = 127.0.0.1:5000    

接着执行下述命令:

uwsgi config.ini

出现: Stats server enabled on 127.0.0.1:5000,代表正常启动。

0x4 配置Nginx

接着配置下Nginx,不要去动默认的nginx.conf,直接将:/etc/nginx/sites-available/default 覆盖掉,新建default文件,添加下述内容:

server {
      listen  80;
      server_name _; 

      location / {
        include      uwsgi_params;
        uwsgi_pass   127.0.0.1:8001;  # 指向uwsgi 所应用的内部地址,所有请求将转发给uwsgi 处理
        uwsgi_param UWSGI_PYHOME /home/www/AutoPubNews/venv; # 指向虚拟环境目录
        uwsgi_param UWSGI_CHDIR  /home/www/AutoPubNews; # 指向网站根目录
        uwsgi_param UWSGI_SCRIPT manager:app; #  
      }
    }

接着键入下述命令重启加载下nginx配置:

sudo service nginx restart

接着启动uwsgi,然后就可以通过服务器的公网ip直接访问我们的项目了:

0x5 域名解析

每次访问项目都用ip,显得有些繁琐,我们可以买个域名做下映射耍耍。 域名直接买就好,需要备案,搞定后打开域名管理页,找到刚买的域名 点击解析

然后点击「添加记录」会出现如图所示的对话框,记录值那里填你的云服务器公网ip即可。

此时就可以直接通过域名来访问我们的项目了。


行吧,关于Flask速成就这么都,下节我们来利用Flask编写API接口,动态生成页面等。 有疑问的欢迎在评论区留言,谢谢~

Tips:公号目前只是坚持发早报,在慢慢完善,有点心虚,只敢贴个小图,想看早报的可以关注下~