零、HTTP与Web服务器简介
0.1 HTTP协议
HTTP,Hypertext Transfer Protocol,超文本传输协议
HTTP URL实例:http://www.bit.edu.cn
HTTP URL的理解:
URL是通过HTTP协议存取资源的Internet路径,一个URL对应一个数据资源
(1)HTTP协议对资源的操作
通过URL和命令管理资源,操作独立无状态,网络通道及服务器成为了黑盒子:
(2)Web服务器
0.2 Flask简介
Flask是一个轻量级的可定制的web框架,使用Python语言编写,较其他同类型框架更为灵活、轻便、安全且容易上手。可快速实现一个网站或Web服务。
可以很好地结合MVC模式进行开发,开发人员分工合作,小型团队在短时间内就可以完成功能丰富的中小型网站或Web服务的实现。
一般情况下,它不会指定数据库和模板引擎等对象,用户可以根据需要自己选择各种数据库。
百度百科: https://baike.baidu.com/item/Flask/1241509
维基百科: https://zh.wikipedia.org/zh-hans/Flask
0.3 django和flask框架的区别:
Flask:小巧、灵活,让程序员自己决定定制哪些功能,非常适用于小型网站。对于普通的工人来说将毛坯房装修为城市综合体还是很麻烦的,使用Flask来开发大型网站也一样,开发的难度较大,代码架构需要自己设计,开发成本取决于开发者的能力和经验。
Django:大而全,功能极其强大,是Python web框架的先驱,用户多,第三方库极其丰富。非常适合企业级网站的开发,但是对于小型的微服务来说,总有“杀鸡焉有宰牛刀”的感觉,体量较大,非常臃肿,定制化程度没有Flask高,也没有Flask那么灵活。
如果想搞懂Python web开发WSGI协议原理以及实现过程、灵活定制组件,完全DIY应用、想实现微服务则选择Flask;如果关注产品的最终交付、想快速开发一个大的应用系统,则用Django,想实现的功能都有。
一、 准备工作
1.1 环境配置
为了保持全局环境的干净,指定不同的依赖版本,我们可以利用virtualenv来构建虚拟的环境,类似于anaconda。
pip install virtualenv
通过上述指令安装virtualenv,之后将在文件夹中创建新的虚拟环境。
mkdir newproj cd newproj virtualenv venv
要在Linux激活相应的环境。
venv/bin/activate
接下来就可以在这个环境中安装 Flask,当然如果你也可以直接选择使用下述指令直接安装Flask。
pip install Flask
1.2 测试安装
为了测试装的Flask是否能正常使用,可以在编译器中输入一下代码:
# 从框架导入Flask类 from flask import Flask # 初始化 app = Flask(__name__) # 将根URL映射到hello world函数上 @app.route('/') # 定义视图函数 def hello_world(): return 'Hello World' if __name__ == '__main__': # 运行 app.run()
运行上述代码,在浏览器中打开localhost:5000,将显示**“Hello World”**消息。
python Hello.py
上述代码中,Flask将(__name__)作为参数,即Flask在当前模块运行,route()函数是一个装饰器,将请求的url映射到对应的函数上。上述代码将’/'与hello_world()函数进行绑定,因此在请求localhost:5000时,网页显示 Hello World 结果。
程序的启动是用过Flask类的run()方法在本地启动服务器应用程序。
app.run(host, port, debug, options)
其中参数是可选的。
二、主要内容
2.1 路由
在Flask中,路由是指用户请求的URL,与视图函数之间的映射。Flask通过利用路由表将URL映射到对应的视图函数,根据视图函数的执行结果,返回给WSGI服务器。路由表的内容是由开发者进行填充,主要有一下两个方式。
- route装饰器:使用Flask应用实例的route装饰器将一个URL规则绑定到 一个视图函数上。
@app.route('/test') def test(): return 'this is response of test function.'
通过装饰器的方式,Flask框架会将URL规则*/test绑定到视图函数test()*上。
add_url_rule() :该方法直接会在路由表中注册映射关系。其实route装饰器内部也是通过调用add_url_rule()方法实现的路由注册。
def test(): return 'this is response of test function.' app.add_url_rule('/test',view_func=test)
更多参考:
(1)视图装饰器的解读:https://dormousehole.readthedocs.io/en/latest/patterns/viewdecorators.html
(2)Flask视图和模板简介:https://www.jianshu.com/p/69ecf785a2ff
2.1.1 指定HTTP方法
默认情况下,Flask的路由支持HTTP的GET请求,如果需要视图函数支持HTTP的其他方法,可以通过methods关键字参数进行设置。关键字参数methods的类型为list,可以同时指定多种HTTP方法。
@app.route('/user', methods = ['POST', 'GET']) def get_users(): if request.method == 'GET': return ... # 返回用户列表 else: return ... # 创建新用户
2.1.2 匹配动态URL
动态URL用于当需要将同一类URL,映射到同一个视图函数处理,比如,使用同一个视图函数 来显示不同用户的个人信息。那么可以将URL中的可变部分使用一对小括号<>
声明为变量, 并为视图函数声明同名的参数:
@app.route('/user/<uname>') def get_userInfo(uname): return '%s\'s Informations' % uname
除了上述方式来设置参数,还可以在URL参数前添加转换器来转换参数类型:
@app.route('/user/<int:uname>') def get_userInfo(uname): return '%s\'s Informations' % uname
使用该方法时,请求的参数必须是属于int类型,否则将会出现404错误。目前支持的参数类型转换器有:
比如不用转换器时,if的判断要逐个转类型:
# 从框架导入Flask类 from flask import Flask # 初始化 app = Flask(__name__) # 将根URL映射到hello world函数上 @app.route('/user/<id>') # 定义视图函数 def index(id): # 这里的id是字符串 if id == '1': return 'python' if id == str(2): return 'django' if int(id) == 3: return 'flask' return 'Hello World' if __name__ == '__main__': # 运行 app.run()
用了转换器后,直接对route
的参数转换类型:
# 从框架导入Flask类 from flask import Flask # 初始化 app = Flask(__name__) # 将根URL映射到hello world函数上 @app.route('/user/<int:id>') # 定义视图函数 def index(id): # 这里的id是字符串 if id == 1: return 'python' if id == 2: return 'django' if id == 3: return 'flask' return 'Hello World' if __name__ == '__main__': # 运行 app.run()
2.1.3 匹配动态URL
为了满足一个视图函数可以解决多个问题,因此每个视图函数可以配置多个路由规则。
@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'
.1.4 URL构建方法
在很多时候,在一个实用的视图中需要指向其他视图的连接,为了防止路径出现问题,我们可以让Flask框架帮我们计算链接URL。简单地给url_for()
函数传入一个访问点,它返回将是一个可靠的URL地址:
@app.route('/') def hello(): return 'Hello world!' @app.route('/user/<uname>') def get_userInfo(uname=None): if uname: return '%s\'s Informations' % uname else: return 'this is all informations of users' @app.route('/test') def test_url_for(): print(url_for('hello')) # 输出:/
添加URL变量 : 如果指定访问点对应的视图函数接收参数,那么关键字参数将生成对应的参数URL。下面的 示例将生成 /user/zhangsan:
@app.route('/') def hello(): return 'Hello world!' @app.route('/user/<uname>') def get_userInfo(uname=None): if uname: return '%s\'s Informations' % uname else: return 'this is all informations of users' @app.route('/test') def test_url_for(): print(url_for('get_userInfo', uname='zhangsan')) # 输出:/user/zhangsan print(url_for('test_url_for', num=2)) # 输出:/test?num=2
2.2 请求,响应及会话
对于一个完整的HTTP请求,包括:
(1)来自客户端的请求对象(Request);
(2)服务器端的响应对象(Respose)和会话对象(Session)等。
在Flask框架中,当然也具有这些对象,这些对象不仅可以在请求函数中使用,同时也可以在模板中使用。那我们来简单看看这些对象具体怎么使用。
2.2.1 请求对象 request
request包括前端发送过来的所有请求数据。
在Flask包中,可以直接引入request对象,其中包含Form,args ,Cookies ,files 等属性。
Form 是一个字典对象,包含表单当中所有参数及其值的键和值对;
args 是解析查询字符串的内容,它是问号(?)之后的URL的一部分,当使用get请求时,通过URL传递参数时可以通过args属性获取;
Cookies 是用来保存Cookie名称和值的字典对象;
files 属性和上传文件有关的数据。
(1)登陆栗子1
用request对象获取前端输入的账号密码,并且打印出对应信息。
# -*- coding: utf-8 -*- """ Created on Sat Dec 25 10:29:49 2021 @author: 86493 """ from flask import Flask, render_template, request app = Flask(__name__) # 默认为GET请求 @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') print(name, password) return 'this is post' if __name__ == '__main__': app.run()
其中我们的html
文件为:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="" method = "post"> 账号: <input type="text" name="name"> <br> 密码: <input type="password" name="password"> <br> <input type="submit" name="submit"> </form> </body> </html>
控制台的结果为:
127.0.0.1 - - [25/Dec/2021 10:38:06] "POST /index HTTP/1.1" 200 - andyguo 123456
(2)登陆栗子2
我们以一个登陆的例子看看如何搭配使用这些属性。
from flask import request, session, make_response @app.route('/login', methods=['POST', 'GET']) def login(): 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!' else: return 'No such user!' 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时一定要设置一个密钥app.secret_key,并且密钥要尽量复杂。
我们可以使用make_response的方法就是用来构建response对象的第二个参数代表响应状态码,缺省就是”200”。response对象的详细使用可参阅Flask的官方API文档。通过创建的response对象可以使用response.set_cookie()函数,来设置Cookie项,之后这个项值会被保存在浏览器中,等下次请求时可以从request对象中获取到cookies对象。
由于现在前后端的交互会采用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)
获取json
数据之后,可以使用flask中的jsonify对象来处理json类型数据。
2.2.2 响应对象 response
(1)引栗
# -*- coding: utf-8 -*- """ Created on Sat Dec 25 11:00:13 2021 @author: 86493 """ from flask import Flask, make_response, json # 前后端数据的交互这里用json格式数据 app = Flask(__name__) @app.route('/index') def index(): data = { 'name': '张三' } # 把python的字典转为json的字符串 response = make_response(json.dumps(data, ensure_ascii = False)) # json的数据格式 response.mimetype = 'application/json' return response if __name__ == '__main__': app.run()
运行上面代码后,返回页面'name': '张三'
,现在的response是json数据(如下图):
当然为了减少代码量,可以使用直接jsonify(data)
,但是要在开头添加上属性app.config
:
from flask import Flask, make_response, json, jsonify # 前后端数据的交互这里用json格式数据 app = Flask(__name__) app.config['JSON_AS_ASCII'] = False @app.route('/index') def index(): data = { 'name': '张三' } # 把python的字典转为json的字符串 # response = make_response(json.dumps(data, ensure_ascii = False)) # json的数据格式 # response.mimetype = 'application/json' # return response return jsonify(data) if __name__ == '__main__': app.run()
如果视图函数想向前端返回数据,必须是Response的对象, 主要将返回数据的几种方
式:
(2)视图函数 return 多个值
@app.route("/user_one") def user_one(): return "userInfo.html", "200 Ok", {"name": "zhangsan"; "age":"20"}
当return多个值的时候,第一个是字符串,也是网页的内容;"200 Ok"表示状态码及解析;{“name”: “zhangsan”; “age”:“20”} 表示请求头。其中前面两个值是必须要的并且顺序不能改变,请求头不是必须要的,这样Flask会自动将返回的值转换成一个相应对象。如果返回一个字符串,则Response将该字符串作为主体,状态码为200,然后返回该Response对象。
(3)使用Response创建
可以通过直接创建Response对象,配置其参数。
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
(4)使用make_response函数
make_response
函数可以传递三个参数 第一个是一个字符串,第二个传状态码,第三个传请求头。
@app.route("/user_one") def user_one(): response = make_response('user_one', 200, {"name": "zhangsan"; "age":"20"}) return response
由于现在前后端交互往往采用的是json的数据格式,因此可以将数据通过 jsonify
函数将其转化成json格式,再通过response对象发送给前端。
@app.route('/hot_list', methods=["GET"]) def hot_list(): if request.method == "GET": user_id = request.args.get('user_id') page_id = request.args.get('page_id') if user_id is None or page_id is None: return make_response(jsonify({"code": 2000, "msg": "user_id or page_id is none!"}), 200)
2.3 重定向与错误处理
2.3.1 重定向
【写法一】先举一个小栗子,如果想让自己的网页地址输入后跳转到百度页面(重定向):
# -*- coding: utf-8 -*- """ Created on Sat Dec 25 10:42:44 2021 @author: 86493 """ # 重定向 302 from flask import Flask, redirect app = Flask(__name__) @app.route('/index') def index(): return redirect('https://www.baidu.com') if __name__ == '__main__': app.run()
【写法二】另一种重定向写法:重定向到另一个hello
函数中(如下),则页面输出this is hello fun
。
# -*- coding: utf-8 -*- """ Created on Sat Dec 25 10:42:44 2021 @author: 86493 """ # 重定向 302 from flask import Flask, redirect, url_for app = Flask(__name__) @app.route('/index') def index(): return redirect(url_for('hello')) @app.route('/') def hello(): return "this is hello fun" if __name__ == '__main__': app.run()
重定向:当一个请求过来后可能还需要在请求另一个视图函数才能达到目的,那么就可以调用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) @app.route("/demo2") def demo2(): return "this is demo2 page" @app.route("/") def index(): # 使用方法:redirect(location, code=302, Response=None) return redirect("/demo", 301)
常用重定向状态码如下,更多状态码可以回顾:HTTP状态码(HTTP Status Code)
2.3.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 对象。
2.4 SQLAlchemy
官网对应SQLAlchemy部分
SQLAlchemy 是一个功能强大的Python ORM 工具包,为应用程序开发人员提供了SQL的全部功能和和ORM操作。其中ORM(Object Relation Mapping)指的是将对象参数映射到底层RDBMS表结构的技术,ORM API提供了执行CRUD操作的方法,不需要程序员编写原始SQL语句。
2.4.1 安装
通过下面指令可以进行安装:
pip install SQLalchemy
在连接数据库时,我们使用pymysql框架进行连接,因此还需要使用下面指令下载pymysql框架:
pip install pymysql
2.4.2 创建连接
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".format(host,dbname)) print(engine) # Engine(mysql+pymysql://root:***@127.0.0.1:49168/3306?charset=utf8)
通过create_engine函数已经创建了Engine,在Engine内部实际上会创建一个Pool(连接池)和Dialect(方言),并且可以发现此时Engine并不会建立连接,只会等到执行到具体的语句时才会连接到数据库。上述代码默认本地已经存在并开启mysql服务。
对于 create_engine 函数可以有以下参数
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设置连接失效的时间,超过时间连接池会自动断开。
2.4.3 创建数据库表类
由于SQLAlchemy 是对象关系映射,在操作数据库表时需要通过操作对象实现,因此就需要创建一个数据库表类。
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.passwd = passwd
通过declarative_base()函数,可以将python类和数据库表进行关联映射,并通过 __tablename__ 属性将数据库模型类和表进行管理。其中Column() 表示数据表中的列,Integer()和String()表示数据库的数据类型。
2.4.4 操作数据库
创建完连接之后,我们需要借助sqlalchemy中的session来创建程序与数据库之间的会话。换句话来说,需要通过session才能利用程序对数据库进行CURD,增加(Create)、检索(Retrieve)、更新(Update)和删除(Delete)。这里我们可以通过 sessionmaker()函数来创建会话。
from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker Base = declarative_base() def mysql_db(host='127.0.0.1',dbname='test'): engine = create_engine("mysql+pymysql://root:123456@{}:49168/{}?charset=utf8mb4".format(host,dbname)) session = sessionmaker(bind=engine) Base.metadata.create_all(engine) return engine, session()
session常用的方法如下:
- flush:预提交,提交到数据库文件,还未写入数据库文件中
- commit:提交了一个事务
- rollback:回滚
- close:关闭session连接
(1)增加数据
增加一个用户:
engine, session = mysql_db() user = User("100","zhangsan","11111") session.add(user) session.commit()
注意一点,session.add()
不会直接提交到数据库,而是在 commit 时才会提交到数据库。add操作会把user加入当前session维护的持久空间(可以从session.dirty
看到)中。
也可以通过add_all()
进行批量提交。
engine, session = mysql_db() user1 = User("101","lisi","11111") user2 = User("102","wangwu","22222") session.add_all([user1,user2]) session.commit()
(2)查询数据
engine, session = mysql_db() users = session.query(User).filter_by(passwd='11111').all() for item in users: print(item.username,item.passwd)
通过上面代码可以查询获取数据,通过 session.query() 我们查询返回了一个Query对象,此时没有去数据库查询,只有等到.count() .first() .all() 具体函数时才会去数据库执行。还可以使用 filter() 方法查询,与 filter_by() 的区别如下:
(3)修改数据
通过 query 中的 update()
方法:
session.query(User).filter_by(username="zhangsan").update({'passwd': "123456"})
或者
users = session.query(User).filter_by(username="zhangsan").first() users.username = "zhangsan-test" session.add(users) session.commit()
(4)删除数据
通过 query 中的 delete()
方法:
session.query(User).filter(User.username == "zhangsan-test").delete() session.commit()
或者 通过 session.delete() 方法
users = session.query(User).filter(User.username == "lisi").first() if users: session.delete(users) session.commit()