简述

在上一节中,我们编写了抓取新闻的爬虫脚本,每天早上八点定时抓取新闻保存到 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(‘’)
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(‘’)
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 = <span class="hljs-string">"睡觉"</span>
note2.status = <span class="hljs-string">"2"</span>

<span class="hljs-comment"># 将新建笔记添加到数据库会话中</span>
db.session.add(note1)
db.session.add(note2)

<span class="hljs-comment"># 将数据库会话中的变动提交到数据库中,如果不commit,数据库内容是不会变化的</span>
db.session.commit()

create_note()

复制代码
  • 3.删除数据
def delete_note():
    # 获取笔记对象 (这里是获取 _id=1 的记录)
    note = Note.query.filter_by(_id=1).first()
<span class="hljs-comment"># 删除笔记</span>
db.session.delete(note)

<span class="hljs-comment"># 提交数据库会话</span>
db.session.commit()

delete_note()

复制代码
  • 4.修改数据
def update_note():
    # 获取笔记对象 (这里是获取 _id=2 的记录)
    note = Note.query.filter_by(_id=2).first()
<span class="hljs-comment"># 修改笔记内容</span>
note.tran = <span class="hljs-string">"打豆豆"</span>

<span class="hljs-comment"># 提交数据库会话</span>
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;  <span class="hljs-comment"># 指向uwsgi 所应用的内部地址,所有请求将转发给uwsgi 处理</span>
    uwsgi_param UWSGI_PYHOME /home/www/AutoPubNews/venv; <span class="hljs-comment"># 指向虚拟环境目录</span>
    uwsgi_param UWSGI_CHDIR  /home/www/AutoPubNews; <span class="hljs-comment"># 指向网站根目录</span>
    uwsgi_param UWSGI_SCRIPT manager:app; <span class="hljs-comment">#  </span>
  }
}
复制代码

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

sudo service nginx restart
复制代码

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

0x5 域名解析

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

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

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


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

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


  • python

    Python (发音:[ paiθ(ə)n; (US) paiθɔn ]n. 蟒蛇,巨蛇 ),是一种面向对象的解释性的计算机程序设计语言,也是一种功能强大而完善的通用型语言,已经具有十多年的发…

    7944 引用 • 22 回帖 • 2 关注
感谢    赞同    分享    收藏    关注    反对    举报    ...