大家好,又见面了,我是你们的朋友全栈君。
1. 写在前面
最近在和几个伙伴尝试搭建一个新闻推荐系统, 算是一个推荐算法的实战项目, 里面涉及到了前后端交互, 该项目里面,使用了Flask作为后台框架, 为了理清楚整个系统的交互,所以就快速参考着资料学习了下flask, 主要还是参考伙伴们写的flask简介和基础的内容, 加上了一些其他理解和实验辅助, 整理一篇心得文章, 算是flask初步入门。
对于一个算法工程师来讲,后端这块虽然不必详细弄清楚原理,但学习一些开发相关知识还是有好处的,因为在实际工作中经常会调试线上的代码调用策略或者模型,我们至少得弄明白,我们的数据流, 模型流到底是怎么走的, 整个系统从输入到最终输出是怎么运行的,这样才能运筹帷幄,从一个更高的角度去看待问题。
好吧,有点扯远了, 本篇文章主要介绍flask,这里依然是从使用的角度整理(因为我对原理也不清楚哈哈), 先不管那么多,会用就行, flask简单的来讲, 就是一个后端框架,基于python语言编写,比较容易上手,或者说不用懂太多细节就能写代码, 把前端传过来的请求,通过编写一些函数进行处理,然后返回给前端。 这里为了更好的理解,我会用一个非常简单的例子贯穿整个流程。 当然,如果想看稍微高大上点的代码,也可以去我们写的fun-rec项目,看新闻推荐系统的代码, 那个是vue-flask交互配合的,更加高级些。
所以,最简单的整个流程就是, 我们在前端页面上输入信息,发送请求给后端(flask), flask根据我们传过来的请求,去找到相应的函数去处理我们的请求(路由), 然后函数处理的结果封装起来返回给前端展示。 这次,主要是看看请求传过来之后,后端这个怎么找函数处理以及返回回去。
主要内容:
- 先配置环境,安装flask
- 路由 – 去找函数处理请求
- 请求、响应和会话
- 重定向与错误处理
- 前端简单制作form表单 – 准备交互
- 介绍两款工具(数据库操作API(sqlarchemy)和接口测试工具(Postman))
- 小例子打通前后端交互流程
- 新闻推荐系统vue和flask是怎么联系起来的呢?
Ok, let’s go!
2. 先配置环境,安装flask
这个不用多整理, flask在python里面也是一个包的形式存在,所以我们如果事先安装好了anaconda, 建立了虚拟环境,那么就直接可以
代码语言:javascript复制pip install flask
然后输入下面代码测试下:
代码语言:javascript复制from flask import Flask
app = Flask(__name__)
@app.route('/') # 这个根目录,就是127.0.0.1:5000进入的位置
def hello_world():
return "hello world"
if __name__ = '__main__':
app.run()
如果正常的话,界面会显示hello world。其实,这就简单的走了一遍小流程(输入网址,根据路由到了hello_word函数,返回结果给前端)。
Flask将(name)作为参数,即Flask在当前模块运行,route()函数是一个装饰器,将请求的url映射到对应的函数上。上述代码将’/’与hello_world()函数进行绑定,因此在请求localhost:5000时,网页显示 Hello World 结果。
这里有几个关键点: 导包, 建立app(Flask(__name__)
),路由匹配(@app.route()
)以及启动(app.run()
)。 几乎在写每个后端处理之前,这几个先写上再说。
程序的启动是用过Flask类的run()
方法在本地启动服务器应用程序
app.run(host, port, debug, options)
# 允许服务器被公开访问
app.run(debug=True, host='0.0.0.0', port=3000, threaded=True)
# 只能被自己的机子访问
# app.run(debug=True, host='127.0.0.1', port=10086, threaded=True)
这几个参数的描述如下:
安装这块比较简单,先这样。
3. 路由 – 去找函数处理请求
web界面输入一个网址,点击回车, 其实是访问的web服务器,然后服务器把结果返回到前端。 这个过程中有个匹配url的过程, 就是flask路由。
Flask中,路由是指用户请求的URL与视图函数之间的映射。Flask通过利用路由表将URL映射到对应的视图函数,根据视图函数的执行结果返回给WSGI服务器。
路由表的内容是由开发者进行填充, 主要有以下两个方式:
route装饰器: 使用Flask应用实例的route装饰器,将一个URL规则绑定到一个视图函数上
代码语言:javascript复制# 通过装饰器的方式, Flask框架会将URL规则绑定到test()函数上, 这个很好用
@app.route('/test') # 浏览器访问的时候,输入的url是127.0.0.1/test
def test():
return 'this is response of test function'
add_url_rule()
:该方法直接会在路由表中注册映射关系。其实route装饰器内部也是通过调用add_url_rule()
方法实现的路由注册
def test():
return 'this is response of test function.'
app.add_url_rule('/test',view_func=test)
我习惯看第一种方式, 感觉比较帅。
3.1 指定HTTP方法
默认情况下, Flask的路由支持HTTP的GET请求, 如果需要试图函数支持HTTP的其他方法, 可以通过methods关键字参数进行设置。 关键字参数methods的类型为list, 可以同时指定多种HTTP方法。
代码语言:javascript复制# 接收post和get请求, 如果不指定的话,就是get请求, 此时如果提交post请求是捕捉不到的
@app.route('/user', methods = ['POST', 'GET'])
def get_users():
if request.method == 'GET':
return ... # 返回用户列表
else:
return ... # 创建新用户
这里先说下这两种请求的区别:
- GET把参数包含在URL中, 也就是直接输入网址访问, 把参数放到这个网址里面去的时候,访问的就是get请求
- POST通过request body传递参数, 采用表单的时候往往就是这个。 关于这个,后面测试接口的时候,会通过Postman进行演示。
这样就完成了最基本的功能, 当然,你说, 这个URL(/user)是写死的目前,如果我不确定怎么办呢? 比如, 我可能这个用户是某某, 而不同的某某,可能有不同的执行操作,这时候,就可以使用动态URL。
3.2 动态URL
动态URL用于当需要将同一类URL映射到同一个视图函数处理,比如,使用同一个视图函数 来显示不同用户的个人信息。那么可以将URL中的可变部分使用一对小括号<>
声明为变量, 并为视图函数声明同名的参数:
@app.route('/user/<uname>') # <>提取参数用的
def get_userInfo(uname):
return '%s's Informations' % uname
# 输入网址127.0.0.1/user/wuzhongqiang wuzhongqiang's Informations
# 输入网址127.0.0.1/user/zhangsan zhangsan's Informations
除了上述方式来设置参数,还可以在URL参数前添加转换器来转换参数类型: 前端本身默认是传入过来的是字符串格式,如果感觉本身传入的参数不应该是字符串格式的,那就可以在URL参数前添加转换器转换参数类型
代码语言:javascript复制@app.route('/user/<int:uname>') # <int: name> int 是一个转换器
def get_userInfo(uname):
return '%s's Informations' % uname
使用该方法时,请求的参数必须是属于int类型,否则将会出现404错误。目前支持的参数类型转换器有int, float, string, path。后两者的区别是path里面可以有。
为了满足一个视图函数可以解决多个问题,因此每个视图函数可以配置多个路由规则
代码语言:javascript复制@app.route('/user')
@app.route('/user/<uname>')
@app.route('/user/<int:uname>')
def get_userInfo(uname=None):
if uname:
return '%s's Informations' % uname
else:
return 'this is all informations of users'
3.3 自定义匹配规则
当然,这里也可以自定义一些规则,去进行输入方面的一些限制, 这时候,就需要继承BaseConverter类,然后写自己的规则了。 这里只是举个简单的例子:
代码语言:javascript复制from flask import Flask
from werkzeug.routing import BaseConverter
app = Flask(__name__)
class RegexConverter(BaseConverter):
"""自定义转化器类"""
def __init__(self, url_map, regex):
super(RegexConverter, self).__init__(url_map)
self.regex = regex
def to_python(self, value: str):
return value
# 将自定义的转换器类添加到flask应用中
app.url_map.converters['re'] = RegexConverter
@app.route('/index/<re("1d{5}"):value>')
def index(value):
return "hello, world"
app.run()
# 只有如输入 127.0.0.1/index/123456 1开头,后面5位整数的才能匹配到
这个里面的匹配URL就可以使用正则的形式,匹配非常特殊的那种了。不过,一般用不到这么复杂的。
3.3 URL构建方法
在很多时候,在一个实用的视图中需要指向其他视图的连接,为了防止路径出现问题,我们可以让Flask框架帮我们计算链接URL。简单地给url_for()
函数传入一个访问点,它返回将是一个可靠的URL地址
@app.route('/')
def hello():
return 'Hello world!'
@app.route('/test')
def test_url_for():
print(url_for('hello')) # 跳转到了hello函数下面执行
4. 请求、响应和会话
对于一个完整的HTTP请求,包括了来自客户端的请求对象(Request), 服务器端的响应对象(Respose)和会话对象(Session)等。 在Flask框架中,当然也具有这些对象, 这些对象不仅可以在请求函数中使用, 同时也可以在模板中使用。
4.1 请求对象Request
Flask包中, 可以直接引入request对象, 其中包含Form,args,Cookies,files等属性。
- Form: 字典对象, 包含表单当中所有参数及值的键和值对
- args: 解析字符串的内容, 是问号?之后的URL的一部分, 当使用get请求时, 通过URL传递参数时可以通过args属性获取
- cookies: 用来保存cookie名称和值的字典对象
- files: 属性和上传文件有关的数据
以一个登陆的例子看看如何搭配属性
代码语言:javascript复制from flask import request, session, make_response
@app.route('/login', methods=['POST', 'GET'])
def logion():
if request.method == 'POST':
if request.form['username'] == 'admin':
session['username'] = request.form['username']
response = make_response('Admin login successfully!')
response.set_cookie('login_time', time.strftime('%Y-%m-%d' %H:%M:%S'))
return 'Admin login successfully!'
elif request.method == 'GET':
if request.args.get("username") == 'admin':
session['username'] = request.form['username']
return 'Admin login successfully!'
else:
return 'No such user!'
app.secret_key = '123456'
可以根据method属性判断当前请求的类型,通过form属性可以获取表单信息,并通过session来存储用户登陆信息。当然这里的session,可以换成字典,然后把信息存储到数据库里面去。
由于现在前后端交互会采用json的数据格式进行传输, 因此当前端请求的数据是json类型的时候, 可以使用get_data()
方法来获取。
from flask import Flask, jsonify, request
@app.route('/login', methods=["POST"])
def login():
request_str = request.get_data()
request_dict = json.loads(request_str)
# 然后,就可以对request_dict进行处理了,相当于从后端拿到了前端的数据
4.2 响应对象response
如果函数试图想向前端返回数据, 必须是Response对象,主要有下面几种返回数据的格式:
试图函数return多个值
代码语言:javascript复制@app.route("/user_one")
def user_one():
return "userInfo.html", "200 Ok", {
"name": "zhangsan"; "age":"20"}
使用Response创建 可以通过直接创建Response对象,配置其参数。
代码语言:javascript复制from flask import Response
@app.route("/user_one")
def user_one():
response = Response("user_one")
response.status_code = 200
response.status = "200 ok"
response.data = {
"name": "zhangsan"; "age":"20"}
return response
由于现在前后端交互往往采用的是json的数据格式,因此可以将数据通过 jsonify 函数将其转化成json格式,再通过response对象发送给前端。
当然,这些东西直接这么写,可能会很抽象,后面一个小例子一串就了然, 这里可以先有个印象。
5. 重定向与错误处理
5.1 重定向
当一个请求过来后可能还需要请求另一个视图函数才能达到目的, 就可以调用redirect(location, code=302, Response=None)
函数指定重定向页面。
from flask import Flask, redirect, url_for
app = Flask(__name__)
@app.route("/demo")
def demo():
url = url_for("demo2") # 路由反转,根据视图函数名获取路由地址
return redirect(url) # 相当于到了/demo2这个页面,显式this is demo2 page
@app.route("/demo2")
def demo2():
return "this is demo2 page"
@app.route("/")
def index():
# 使用方法:redirect(location, code=302, Response=None)
return redirect("/demo", 301)
url_for函数我理解是能根据给定的url映射到对应的函数,比如给定demo2, 就映射到了demo2()
, 但具体执行, 应该是redirect()
函数起作用。
5.2 错误处理
当请求或服务器出现错误的时候, 我们希望遇到特定错误代码走不通的处理错误逻辑, 可以使用errorhandler()
装饰器
from flask import render_template # 渲染页面
@app.errorhandler(404)
def page_not_found(error):
return render_template('page_not_found.html'), 404
当遇到404错误时,会调用page_not_found()
函数,返回元组数据,第一个元素是”page_not_found.html”的模板页,第二个元素代表错误代码,返回值会自动转成 response 对象。 如果这个地方想在网页里面放张图片的话,一定要放到static目录里面才行, 访问的是静态文件目录, 这个static名字不能改。
这个东西主要是为了后面处理异常,如果满足什么条件,就进行什么样的处理:
代码语言:javascript复制from flask import abort
@app.route('/index', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
return render_template('index.html')
if request.method == 'POST':
name = request.form.get('name')
password = request.form.get('password')
if name == 'zhangsan' and password == '123':
return 'login success'
else:
abort(404)
return None
6. 构建form表单,为交互做准备
上面整理了那么一大推, 这里想通过一个例子串一下, 否则总会有一股朦胧之感, 由于我不是很懂前端, 这里就简单参考代码写一个前端页面, 不用很复杂,就构建一个输入用户名和密码的对话框,然后点击提交,看看与后端的交互效果。
前端页面的代码如下:
这个布局方式也是蛮重要的, 就是先建立一个templates目录,这个目录可以认为是有一个模板目录,默认定义了一些前后端交互的代码格式。这个模板是jinjia2(右击目录,mark directory as设置), 然后在该目录下创建一个HTML界面。
然后在上一级目录,创建一个form表单文件,把这个HTML渲染出来:
代码语言:javascript复制from flask import Flask, render_template
app = Flask(__name__)
@app.route('/index')
def index():
return render_template('index.html') # 渲染当前的html页面
if __name__ == '__main__':
app.run()
# 输入127.0.0.1:5000/index 就会出来写的那个html页面了,然后输入密码,提交,就会得到一个get请求
此时,就能把前端的html页面显示出来。
当然,比较陋, 但演示足够。下面看看如何交互。
7. 前后端交互小例子
这里前端,从上面的两个框里输入用户名和密码,然后点击提交给后端。 后端接收过来, 把用户和密码封装起来, 给到另一个前端页面, 然后另一个前端页面就能用这个数据了。
首先, 需要先修改上面前端页面数据, 把提交请求的方式改为POST,非常简单, 只需要修改这里。
然后在总目录下建立了request对象.py文件,在这里面写接收数据的逻辑
代码语言:javascript复制from flask import Flask, render_template
from flask import request
app = Flask(__name__)
@app.route('/index', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
# 渲染index页面
return render_template('index.html')
elif request.method == 'POST':
# 获取数据
data = {
}
data['name'] = request.args.get('name') # 后面这个name和前端的name保持一致
data['passwd'] = request.args.get('password')
# 返回到前端去
return render_template('index2.html', data=data)
return render_template('index.html')
app.run()
其实也非常简单,输入网址的时候,显式的就是index.html页面,这个页面就是让用户输入用户名密码,然后提交即可,此时由于修改了index的提交方式是post请求,所以后端这块捕捉到,拿到传过来的数据, 给到index2.html, 此时index2.html就可以直接拿到data使用或者用来展示。
index2.html页面此时就能使用data数据了。
框里的这两个,就是index.html传给后端,然后后端传过来的数据, 可以直接在index2.html中显示。 当然,这里的{
{变量名}}
的这种定义格式,就是模板事先定义好的,如果不是jinjia2模板,可能不能使用。所谓模板,就是事先定义了一些前后端交互的规则。
上面就是一个前后端交互的小例子啦, 其实flask框架用起来还是比较容易上手的。
8. 介绍两款工具
这里主要是介绍这两天用到的两个工具,SQLAlchemy和Postman。
8.1 SQLAlchemy
这是一个功能强大的python ORM工具包, 也就是提供了API去操作数据库里面的表的相关操作,而不是编写原始的SQL语句,非常方便。安装
代码语言:javascript复制# 安装
pip install SQLalchemy
8.1.1 连接数据库
下面创建连接,也就是连接到我们的mysql数据库:
代码语言:javascript复制from sqlalchemy import create_engine
def mysql_db(host='127.0.0.1', dbname='3306'):
engine = create_engine("mysql pymysql://root:123456@{}:49168/{}?charset=utf8".formate(host, dbname))
print(engine) # Engine(mysql pymysql://root:***@127.0.0.1:49168/3306?charset=utf8)
通过create_engine函数已经创建了Engine,在Engine内部实际上会创建一个Pool(连接池)和Dialect(方言),并且可以发现此时Engine并不会建立连接,只会等到执行到具体的语句时才会连接到数据库。上述代码默认本地已经存在并开启mysql服务。
代码语言:javascript复制create_engine("mysql://user:password@hostname/dbname?charset=utf8",
echo=True,
pool_size=8,
pool_recycle=60*30)
第一个参数是和框架表明连接数据库所需的信息,“数据库 数据库连接框架://用户名:密码@IP地址:端口号/数据库名称?连接参数”;echo是设置当前ORM语句是否转化为SQL打印;pool_size是用来设置连接池大小,默认值为5;pool_recycle设置连接失效的时间,超过时间连接池会自动断开。
8.1.2 创建数据库表类
用于SQLAlchemy是对象关系映射,在操作数据库表时是通过操作对象实现的, 每一条记录其实是一个对象,所以需要先创建一个数据库表类说明字段信息。
代码语言:javascript复制from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'UserInfo' # 表名
# 数据库字段的类型
index = Column(Integer(), primary_key=True)
user_id = Column(Integer(), unique=True)
username = Column(String(30))
passwd = Column(String(500))
def __init__(self, index, user_id, username, passwd):
self.index = index
self.user_id = user_id
self.username = username
self.paswd = passwd
这个可以当做是固定套路格式, 通过declarative_base()
函数,可以将python类和数据库表进行关联映射,并通过 _tablename_
属性将数据库模型类和表进行管理。其中Column()
表示数据表中的列,Integer()
和String()
表示数据库的数据类型。
8.1.3 操作数据库
创建完连接, 需要借助sqlarchemy中的session来创建程序与数据库之间的会话,此时通过sessionmarker()
函数创建。
def mysql_db(host='127.0.0.1', dbname='test'):
engine = create("mysql pymysql://root:123456@{}:49168/{}?charset=utf8mb4".format(host,dbname))
session = sessionmaker(bind=engine)
Base.metadata.create_all(engine)
return engine, session()
这样,相当于正式与mysql建立了连接时候的会话。 session常用的方法:
- flush: 预提交,提交到数据库文件,此时还未写入数据库文件中
- commit: 提交了一个事务
- rollback: 回滚
- close: 关闭session连接
下面演示下数据库里面的增删改查。
增加数据
代码语言:javascript复制# 增加一个用户
engine, session = mysql_db()
user = User("100", "zhangsan", "11111")
session.add(user)
session.commit()
# 也可以通过addall批量提交
engine, session = mysql_db()
user1 = User("101","lisi","11111")
user2 = User("102","wangwu","22222")
session.add_all([user1,user2])
session.commit()
不用我们事先建立数据库和表, 调用程序的时候,会自动建立好。 把用户的信息封装成一个对象,然后采用add的方式就可以添加进去了。 当然session.add()
不会直接提交到数据库,当执行了commit()
之后才会提交。 这时候,就不用什么insert table_name values
…。
查询数据
代码语言:javascript复制engine, session = mysql_db()
users = session.query(User).filter_by(passwd='11111').all()
for item in users:
print(item.username,item.passwd)
通过上面代码就可以访问数据库, 类似于select操作了, 其中query()返回一个query对象,在这里指明查哪个大类(这里面映射一个数据表),告诉去哪个表里查。 当然此时并没有真正去查询, 只有等到具体执行函数count(), first(), all()
等采取数据库查询。 当然,还可以使用filter()
方法指定查询条件,类似where。 这里其实有两种写法:
修改数据
代码语言:javascript复制session.query(User).filter_by(username="zhangsan").update({
'passwd': "123456"})
删除数据
代码语言:javascript复制session.query(User).filter(User.username == "zhangsan-test").delete()
session.commit()
8.2 Postman
当然这个东西由于也是刚接触,并不是太会用。 这个东西是有个接口测试工具, 是为了验证后端开发的接口是否可用。
因为真正开发大项目,前后端是分离开发的, 并且此时前端可能没有完全搭建好,所以接口测试的时候,postman,就相当于一个客户端, 可以模拟用户发起各类的HTTP请求, 将请求数据发送给服务端, 来获取对应的响应结果。 这样就能测试出后端的函数逻辑是否正确。
我这里是偶然接触到,因为学习上面新闻推荐系统的时候,我这边后端的每个py文件都运行通过了,此时想基于界面传数据看看效果,结果就是和前端的vue框架连不起来。 上面我自己写HTML文件好好的, 一旦用上vue框架,再去访问网址总是报错或者被拒绝啥的。
所以,这里就想看看到底是后端给的网址和接口不对,还是前端vue的问题,那么怎么测试呢? 意哥就告诉了我这个工具,用他来模拟前端,给后端发请求,看看后端能返回结果不。
当然具体的下载和使用, 我给出两篇参考文档postman教程, postman教程大全, 这玩意也是个软件,所以直接Windows下载安装即可。
打开之后, 我们新建一个collections,其实就是目录,然后在这里面新建一个request请求,就可以测试了。
我这里给出我这边的测试例子, 我当时想通过postman测试下,能不能访问到后端。测试的后端函数是这个:
代码语言:javascript复制@app.route('/recsys/register', methods=["POST"])
def register():
"""用户注册 """
request_str = request.get_data()
request_dict = json.loads(request_str)
print(request_dict)
user = RegisterUser()
user.username = request_dict["username"]
user.passwd = request_dict["passwd"]
# 查询当前用户名是否已经被用过了
result = UserAction().user_is_exist(user, "register")
if result != 0:
return jsonify({
"code": 500, "mgs": "this username is exists"})
#user.userid = snowflake.client.get_guid() # 雪花算法
user.userid = 20211971672
user.age = request_dict["age"]
user.gender = request_dict["gender"]
user.city = request_dict["city"]
print("hello world")
# 添加注册用户
save_res = UserAction().save_user(user)
if not save_res:
return jsonify({
"code": 500, "mgs": "register fail."})
return jsonify({
"code": 200, "msg": "register success."})
用户注册函数, 这是一个post请求格式的,然后需要传入用户的相关参数,给到后端,后端把这个存到用户注册表里面去。然后返回成功信息。
其实逻辑很简单,首先, 建立post请求格式在postman的操作, 首先请求格式改成POST,然后headers这里需要设定json格式。
然后, 在body里面传入请求参数,也就是用户的注册信息, 这里是一个字典的形式
这样,点击右上角send即可发送了。根据下面后端返回的信息,说明后端这块是可以被访问的,没有什么问题。如果想发送get请求,以及传参数,还可以这样:
那,这就确定了, vue框架的配置有问题。
9. 新闻推荐系统vue和flask是怎么联系起来的呢?
这里主要是记录下解决上面这个问题的方法, 因为我这边遇到了vue服务开启完了之后, 输入网址并没有到相应的界面中去,而是报错。 这个问题还是困扰我一段时间的,一开始以为是后端那边的网址不能访问, 但用了postman之后,发现后端这边没问题。
于是我觉得是我vue那边配置有问题,因为我对vue内部一窍不通, 并且我伙伴们之前都测试好了,不可能是代码方面的问题。
于是乎,开始排查路径问题: 这篇文章启发
这个没有问题。下面这里也需要改:
这样操作完了,然后在浏览器输入
这样一顿操作之后,就搞定了上面的问题。202.199.6.190
是我实验室服务器的地址。
当然我后端是这样:
总结起来, 就是需要修改前端的main.js里面的网址,然后修改package.json里面的主机地址。 然后访问的时候是从前端running的地址进行访问。
当然,开启前端的过程中还遇到一个奇葩的报错问题:
这个问题我也不知道是啥原因, vue涉及到盲区, 但下面这行代码却能无脑搞定,挺神奇:
代码语言:javascript复制# 命令行输入
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
到这里就差不多了,花了两三天的探索,终于把整个系统换了我实验室服务器跑出来,然后整理这篇文章记录下, 实测能运行, 感兴趣的伙伴可以玩玩啦