python_web
Alembic
安装
pip install alembic
操作步骤
1.初始化仓库
初始化alembic仓库。在终端中,cd到你的项目目录中,然后执行命令alembic init alembic,创建一个名叫alembic的仓库。
2.创建模型(ORM)类:
比如这里创建一个models.py模块,
from sqlalchemy import Column,Integer,String,create_engine,Text
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer,primary_key=True)
username = Column(String(20),nullable=False)
password = Column(String(100),nullable=False)
3.修改配置文件:
-
在
alembic.ini中设置数据库的连接,sqlalchemy.url = driver://user:pass@localhost/dbname,比如以mysql数据库为例,则配置后的代码为:sqlalchemy.url = mysql+mysqldb://root:root@localhost/alembic_demo?charset=utf8为了使用模型类更新数据库,需要在
env.py文件中设置target_metadata,默认为target_metadata=None。使用sys模块把当前项目的路径导入到path中:import os import sys sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../") from models import Base ... #省略代码 target_metadata = Base.metadata # 设置创建模型的元类 ... #省略代码
cd optima_models
#1.初始化,创建一个仓库
alembic init alembic(anyname)
# 2.修改配置alembic.ini
sqlalchemy.url = mysql+pymysql://root:123@localhost/optima?charset=utf8
# 3.创建model
# 4.修改env.py
import os, sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)) +"/../")
# target_metadata = base_model.Base.metadata
target_metadata = db.Model.metadata # flask-sqlalchemy
# 5.如果只创建表,不删除表,改成
# existing_db.py
from sqlalchemy import create_engine
from sqlalchemy.schema import MetaData
engine =create_engine('mysql+pymysql://root:123@127.0.0.1:3306/test_tcloud?charset=utf8')
metadata = MetaData(engine)
metadata.reflect()
# env.py 需要引入这个metadata
import existing_db # 只能新建表,不能修改
target_metadata = existing_db.metadata, db.Model.metadata
4.生成迁移文件
使用命令alembic revision --autogenerate -m "message"可以将当前模型中的状态生成迁移文件。
5.更新数据库
使用alembic upgrade head将刚刚生成的迁移文件,真正映射到数据库中。同理,如果要降级,那么使用alembic downgrade head。
6.重复
如果以后修改了代码,则重复4~5的步骤。
生成sql脚本
rm -f alembic/versions/*.py
mysql -uroot -p123 -e "use test_tcloud;truncate table alembic_version"
alembic revision --autogenerate -m "auto generate sql"
alembic upgrade head --sql | tee migration.sql
if [ x$1 = xc ];then
# tables need to be created in the database
alembic upgrade head
fi
部署 uwsgi
uwsgi.ini
[uwsgi]
socket = 127.0.0.1:9000 # 监听到网络套接字 也可以是uwsgi.sock
pythonpath=/PyPro/optima_order
py-autoreload = 1 # 代码修改自动重载
module=server
callable=app
master = true
processes=4
threads=2
pidfile=uwsgi.pid
daemonize=uwsgi.log
uwsgi --ini uwsgi.int
uwsgi --reload uwsgi.pid
/etc/nginx/conf.d/default.conf
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:9000;
}
location /v1/truck {
include uwsgi_params;
uwsgi_pass unix:/var/www/trucks/backend/current/tmp/uwsgi.sock;
}
常用的sqlalchemy查询
联合索引
__table_args__ = (
db.Index("ix_regular_line_truck_location_code_x_y", 'location_code_x', 'location_code_y'),
db.Index("ix_regular_line_truck_location_name", 'location_name_x', 'location_name_y')
)
modified_time = db.Column(db.TIMESTAMP,
nullable=False,
default=datetime.now,
onupdate=datetime.now)
server_default='北京' # 数据库default字段能看到
# 布尔值类型指定server_default时,需要用text(“0”)或者text(“1”)
# mysql 不支持时间字段默认
Boolean:布尔类型,映射到数据库中的是tinyint类型。
flask-sqlalchemy 会在每个请求上下文内自动创建销毁 session,如果你需要在当前请求内继续使用 session,必须执行 session.rollback();如果确定不再需要这个 session 执行任何数据库操作了,可以不用 rollback,让它在请求结束时自动销毁。
like查询
db.session.query(tablename).filter(tablename.colname.like(''%xxx%))
打印SQL
query = session.query(Model).filter(*filter)
print(str(query))
inner join 且 like
db.session.query(tableA, tableB).join(tableB, tableA.id==tableB.id).filter(tableA.colname.like('%xx%'))
select * from tableA inner join tableB on tableA.id=tableB.id where tableA.colname like '%xx%'
left join
db.session.query(tableA, tableB).outerjoin(tableB,tableA.id==tableB.id).filter(tableA.colname=='xxx')
select * from tableA left outer join tableB on tableA.id=tableB.id where tableA.colname='xxx'
join有多个条件
from sqlalchemy import and_
db.session.query(tableA, tableB).join(tableB, and_(tableA.id==tableB.id, tableA.col2==tableB.col2)).filter(tableA.colname=='xxx')
select * from tableA left outer join tableB on tableA.id=tableB.id and tableA.col2=tableB.col2 where tableA.colname='xxx'
union
# 组合
q1 = session.query(Users.name).filter(Users.id > 2)
q2 = session.query(Favor.caption.lable("name")).filter(Favor.nid < 2)
ret = q1.union(q2).all() #union默认会去重
ret2 = q1.union_all(q2).all() # union_all不去重
func.COUNT(func.IF((TransactionMessage.tm_read==0 and TransactionMessage.tm_type==1),1,0)).label('t_message_count'),
子查询
subquery = PipelineHistory.query.with_entities(func.max(PipelineHistory.id).label("history_id"),PipelineHistory.pipeline_id,PipelineHistory.result).group_by(
PipelineHistory.pipeline_id).subquery()
query = Pipeline.query.with_entities(Pipeline.id, Pipeline.name,subquery.c.result).filter(*filter_query).filter(subquery.c.pipeline_id == Pipeline.id)
print(query.all())
between 查询
from sqlalchemy import between
db.session.query(tableA).filter(between(tableA.colA, 'xxx1', 'xxx2'))
select * from tableA where colA between 'xxx1' and 'xxx2'
查找指定月份
from sqlalchemy import func, extract
.filter(extract('month', table.run_time) == 10)
.filter(func.year())
and查询
from sqlalchemy import and_
db.session.query(tableA).filter(and_(tableA.col1='xxx1', tableA.col2='xxx2'))
select * from tableA where (tableA.col1='xxx1' and tableA.col2='xxx2')
or查询
from sqlalchemy import or_
db.session.query(tableA).filter(or_(tableA.col1='xxx1', tableA.col2='xxx2'))
select * from tableA where (tableA.col1='xxx1' or tableA.col2='xxx2')
in 查询
db.session.query(tableA).filter(tableA.col1.in_(['xx1', 'xx2']))
select * from tableA where col1 in ('xx1', 'xx2')
is not NULL
db.session.query(tableA).filter(tableA.col1.isnot(None))
db.session.query(tableA).filter(tableA.col1 != None)
select * from tableA where col1 is not null
distinct
db.session.query(tableA).distinct(tableA.col1).all()
查询指定字段
# 1.
db.session.query(tableA.col1, tableA.col2).all()
# 2.with_entities 返回特定的列去重
tp.query.with_entities(tp.fk_driver_id).distinct().all()
# 3. add_columns (返回 (<Requirement 23>, 23, 'SWCS插件自动化'), ret.Requirement, ret.id, ret.title)
Req.query.add_columns(Req.id,Req.title.label('title')).all()
分页
# 1.paginate
tl_objs = tl.query.filter(*filter_list).order_by(*order_by).paginate(page_index, page_size, False)
current_page = tl_objs.page
total = tl_objs.total
total_pages = tl_objs.pages
# 属性'has_next', 'has_prev', 'items', 'iter_pages', 'next', 'next_num', 'page', 'pages', 'per_page', 'prev', 'prev_num', 'query', 'total
data = tl_objs.items
# 2. limit
count = query.count()
if page_index and page_size:
query = query.limit(page_size).offset((page_index - 1) * page_size)
data = query.all()
text
tpf.query.filter(text(
"fk_trans_number='%s' and status!=302 and status!=301 and status!=201 and content is null" %
trans_number)).count()
tp.query.filter(text("fk_to_location_code=:id and fk_driver_id=:id").params(id=1)).all()
a['296'] = [<296>, b['297']]
b['297'] = [<297>, ...]
执行SQL语句
tp.query.from_statement(text("select * from Transport_Protocol where fk_to_location_code=1")).all()
假如字符串str 在由N 子链组成的字符串列表strlist 中,则返回值的范围在 1 到 N 之间。
func.find_in_set(business, Location.business)
查询优化
1. 选择合适的加载方式:
- 延迟加载(Lazy Loading): 默认情况下,SQLAlchemy 使用延迟加载,只在需要时才加载关联数据。这可以避免不必要的查询,但如果在循环中访问关联属性,可能会导致 N+1 查询问题。
- 立即加载(Eager Loading): 可以使用
joinedload,subqueryload,selectinload等选项来指定立即加载关联数据,避免 N+1 查询。选择哪种方式取决于具体场景和查询的数据量。 - 选择性加载(Select Only): 使用
with_entities或load_only方法可以选择只加载需要的字段,减少数据传输量。
# 延迟加载,可能导致 N+1 查询
for user in session.query(User).all():
print(user.orders) # 每次循环都会查询 orders 表
# 立即加载,避免 N+1 查询
from sqlalchemy.orm import joinedload
for user in session.query(User).options(joinedload(User.orders)).all():
print(user.orders) # 只查询一次 orders 表
# 选择性加载,只加载需要的字段
for user in session.query(User).with_entities(User.id, User.name).all():
print(user.id, user.name) # 只查询 id 和 name 字段
2. 使用编译缓存:
- SQLAlchemy 可以缓存编译后的 SQL 语句,避免重复编译,提高查询效率。可以通过配置
SQLALCHEMY_CACHE_TYPE来启用缓存。
3. 使用原生 SQL:
- 对于复杂的查询,直接使用原生 SQL 可能比使用 ORM 更高效。SQLAlchemy 提供了
text和execute方法来执行原生 SQL 语句。
4. 索引优化:
- 和直接使用 SQL 一样,确保在数据库中为经常查询的字段创建索引。可以使用 SQLAlchemy 的
Index对象来创建索引。
5. 数据库优化:
- 使用数据库连接池、数据库缓存、定期进行数据库维护等措施,同样适用于 SQLAlchemy。
6. 其他优化技巧:
- 使用
in操作符进行批量查询: 避免循环中执行多次查询。 - 使用
bulk_update_mappings方法进行批量更新: 避免循环中逐条更新数据。 - 使用
bulk_insert_mappings方法进行批量插入: 避免循环中逐条插入数据。
https://blog.csdn.net/five3/article/details/70786741
https://www.cnblogs.com/shangwei/p/15654553.html
最慢的就是 add 方式
次慢 bulk_save_objects 方式
慢的 bulk_insert_mappings 方式
快的 engine.execute 方式
最快的 copy_from 方式
如果 使用 bulk_insert_mappings 插入10W条数据需要10秒,那么使用 copy_from 方式只需要1秒
数据库锁
MySQL 默认的锁机制主要是在后台自动管理的,比如在插入、更新和删除时会自动加锁。然而,这些自动锁定通常不包括读取操作。
SELECT * FROM products WHERE id=’3’ FOR UPDATE;
表级: MyISAM
行级: INNODB
由于InnoDB预设是Row-Level Lock,所以只有「明确」的指定主键,
MySQL才会执行Row lock , 上句id是主键,
with_lockmode --> sqlalchemy 0.9.0以后废弃
对需要经常并发操作的表,sqlalchemy可以加锁保证数据一致性,
mode参数
None
update
update_nowait Oracle, PostgreSQL
read
with_for_update(self, read=False, nowait=False, of=None)
db.session.query(User).with_for_update(nowait=True, of=User)
mysql 不支持nowait等参数
flask
https://www.cnblogs.com/wupeiqi/articles/7552008.html
配置文件
路由系统
模板语言
请求&响应相关
session & cookie
闪现
蓝图: bluegraphic
请求扩展
中间件
wsgi:
#werkzeug
from werkzeug.wrappers import Request, Response
@Request.application
def hello(request):
return Response("hello world!")
if __name__ == "__main__":
from werkzeug.serving import run_simple
run_simple("localhost", 4000, hello)
#wsgiref
from wsgiref.simple_server import make_server
def runserver(environ, start_response)
start_response("200 OK", [("Content-Type", "text/html")])
return [bytes('<h1>Hello web!</h1>', encoding='utf-8'),]
if __name__ == "__main__":
httpd = make_server('', 8000, runserver)
httpd.server_forever()
本质是socket
import socket
def handle_request(client):
buf = client.recv(1024)
client.send("HTTP/1.1 200 OK\r\n\r\n")
client.send("Hello, Seven")
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind('localhost', 8000)
sock.listen(5)
while True:
connection, address = sock.accpet()
handle_request(connection)
connection.close()
if __name__ == "__main__":
main()
url_for('login') // 反向生成url
@app.route("/login", methods=['GET', 'POST'], endpoint='login')
模板路径:
Flask() 参数中有个template_folder="templates"
return render_template("login.html") # from flask import render_template
2.GET请求:
from flask import request
if request.method == "GET":
...
#请求的数据
user = request.form.get("user")
重定向:
from flask import redirect
return redirect("http://www.baidu.com")
@app.route("/index", redirect_to="/index2")
render_template("login.html", **content) # 传入一个字典但是要**,也就是传入# #kwargs
render_template("login.html", error="username is wrong")
HTML:
{% for k, v in user_dict.items() %}
{{k}}
{{v.name}} {{v['name']}} {{v.get('name')}}
{% endfor %}
自动重启:
app.debug = True
<a href="/detail/{{k}}" > 查看详情</a>
@app.route("/detail/int:nid" methods=['GET'])
session:
from flask import session
user = session.get("user_info")
if not user:
url别名, 反向生成: # endpoint
from flask import url_for
@app.route('/login', methods=[], endpoint='l1')
url = url_for('l1') # 也能传参
return redirect(url)
配置文件
app:
default_config = ImmutableDict
import collections
d1={}
d1=collections.OrderedDict() #将普通字典转换为有序字典
# app.py
1. app.debug = True // 少数可以
2. app.config['debug'] = True
3. settings.py
DEBUG = True
app.config.from_pyfile("settings.py")
4. app.config.from_object()
settings.py
class Config(object):
DEBUG = False
TESTING = False
DATABASE_URI = 'sqlite://:memory:'
class ProductionConfig(Config):
DATABASE_URI = 'mysql://user@localhost/foo'
class DevelopmentConfig(Config):
DEBUG = True
class TestingConfig(Config):
TESTING = True
开发环境与正式环境不一样
app.config.from_object('pro_flask.settings.TestingConfig')
# 原理
"os.path.realpath"
module_name, obj_name = import_name.rsplit('.', 1)
module = __import__(module_name, None, None, [obj_name])
return getattr(module, obj_name)
module = <module 'posixpath' from '/usr/lib/python3.5/posixpath.py'>
flask下载文件---文件流
html:
<a name="downloadbtn" class="btn btn-success pull-right" href="/downloadfile/?filename=/root/allfile/123.txt">下载</a>
py:
@app.route('/downloadfile/', methods=['GET', 'POST'])
def downloadfile():
if request.method == 'GET':
fullfilename = request.args.get('filename')
# fullfilename = '/root/allfile/123.txt'
fullfilenamelist = fullfilename.split('/')
filename = fullfilenamelist[-1]
filepath = fullfilename.replace('/%s'%filename, '')
#普通下载
# response = make_response(send_from_directory(filepath, filename, as_attachment=True))
# response.headers["Content-Disposition"] = "attachment; filename={}".format(filepath.encode().decode('latin-1'))
#return send_from_directory(filepath, filename, as_attachment=True)
#流式读取
def send_file():
store_path = fullfilename
with open(store_path, 'rb') as targetfile:
while 1:
data = targetfile.read(20 * 1024 * 1024) # 每次读取20M
if not data:
break
yield data
response = Response(send_file(), content_type='application/octet-stream')
response.headers["Content-disposition"] = 'attachment; filename=%s' % filename # 如果不加上这行代码,导致下图的问题
return response
路由系统
@app.route('/user/<username>')
@app.route('/post/<int:post_id>')
@app.route('/post/<float:post_id>')
@app.route('/post/<path:path>')
@app.route('/login', methods=['GET', 'POST'])
route源码
def route(self, rule, **options):
def decorator(f):
endpoint = options.pop('endpoint', None)
self.add_url_rule(rule, endpoint, f, **options)
return f
return decorator
-
@app.route("/", methods=['GET'], endpoint='n1') def index()
app.add_url_rule(rule="/", endpoint="n1", view_func=index, methods=['GET'])
-
class IndexView(views.MethodView): methods = ['GET'] decorators = [auth, ] def get(self): return "index.GET" def post(self): return "Index.POST" def dispatch_request(self): # 自己写分发 print("Index") return "Index"
app.add_url_rule("/index", view_func=IndexView.as_view(name='index')) # name=endpoint
子域名
subdomian
windows: c:\windows\system32\drivers\etc
linux: /etc/hosts
@app.route("/dynamic", subdomain="<username>")
def username_index(username):
return username + ".your-domian.tld"
@app.route("/", subdomain="admin")
自定义第三方库
实现init_app()
app.config.setdefault('XXX', ...)
app.extensions['xxx'] = ...
url 支持正则
1.写类
class RegexConverter(BaseConverter):
def __init__(self, map, regex):
...
def to_python(self, value):
"""路由匹配成功后,先执行to_python处理一下"""
return value
def to_url(self, value):
val = super().to_url(value)
return val
2.将类添加到flask 中
app.url_map.converters['regex'] = RegexConverter
@app.route("/index/<regex("\d+"):nid>")
def index(nid):
url_for("index", nid=90)
return "index"
装饰器:
@functools.wraps(func) # 帮助保存函数的元信息,像帮助信息
请求扩展(类似django中间件)
@app.before_request
def process_request(*args, **kwargs):
user = session.get("user_info")
if user or request.path == "/login":
return None
return redirect("/login")
@app.after_request
def process_response(response):
return response
# 可以写多个,before#按函数的顺序执行, after倒序执行
# 请求拦截后,response还是都执行了
@app.before_first_request
@app.teardown_request
flash
基于session.pop()
@app.errorHandler(404):
def error_404(arg):
return "my 404"
html模板定制
@app.template_global()
def sb(a1,a2):
@app.template_filter()
def db(a1,a2,a3)
# 调用方式: {{sb(1,2)}} {{ 1 | db(2,3) }}
中间件
app.run() //
run_simple(host, port, self)
app.__call__()
return self.wsgi_app(environ, start_response)
class Md:
def __init__(self, old_wsgi_app):
self.old_wsgi_app = old_wsgi_app
def __call__(self, environ, start_reponse):
print("start ...")
ret = self.old_wsgi_app(environ, start_reponse)
print("end ...")
return ret
app.wsgi_app = Md(app.wsgi_app)
app.run()
蓝图
flask_pro/
|-- templates
|---views/
| |---__init__.py
| from flask import Flask
| app = Flask(__name__)
| from . import account
| |---account.py from . import app
|---app.py from views import app app.run()
pro_flask
|---pro_flask
| |----static
| |----views
| |---account.py
| |---__init__.py
|---run.py
# account.py
from flask import Blueprint
#account = Blueprint("account", __name__)
account = Blueprint("account", __name__,url_prefix="/acc",
template_folder="tpls")
@account.route("/login")
def login():
# __init__.py
from .views.account import account
app.register_blueprint(account)
-------- threading.local() -------------------
用于为每个线程开辟一块空间来保存自己的值
request源码
-- 单进程多协程
flask.globals
request = LocalProxy()
werkzeug/local.py
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
1.偏函数
import functools
def func(a,b)
new_func = functools.partial(func, 666)
new_func(999)
上下文
app.run()
__call__
1.ctx = self.request_context()
return RequestContext(environ)
RequestContext()
__init__()
Request()
2.ctx.push()
_request_ctx_stack.push(self) //
stack.append()
3.视图函数
4.ctx.auto_pop(error)
pop()
stack.pop()
数据库连接池
http://www.cnblogs.com/wupeiqi/articles/8184686.html
1.不能为每个用户创建一个连接
2.创建一定数量的连接池。
DBUtils 模块,实现连接池
模式一: 为每个线程创建连接
模式二: 创建n个连接,多线程来时,去获取。
1.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://username:password@localhost/db_name'
app.config['SQLALCHEMY_POOL_SIZE'] = 20
db = SQLAlchemy(app)
作业:
1. 上下文源码流程
2. 找源码
- before_first_request
- before_request
- after_request
实施:
- 代码刚启动?
- 请求到来时? chain
找路由匹配
3. 分组实现: 会议室预定
- 设计表
- 基于蓝图, 基于before_request是如何实现用的登录
上下文管理
- threading.local/ Local类,其中创建了一个字典{greelet做唯一标识:存数据}保证数据隔离
- 请求进来
- 请求相关所有数据封装到RequestContext中
- 再将RequestContext对象添加到Local中(通过LocalStack将对象添加到Local对象中)
- 使用,调用request
- request.method, print(request), request+xxx...会执行执行LocalProxy中对应 的方法
- 函数
- 通过LocalStack去Local中获取值.
- 请求终止
- 通过LocalStack的pop方法,Local中将值移除。
3. DBUtils, 数据库连接池。
ctx = self.request_context(environ)
ctx.push() Local()--> self.storage[self.ident_func()]["stack"]
request = LocalProxy(partial(_lookup_req_object, 'request'))
执行_lookup_req_object,自动传递request参数
object.setattr(self, '_LocalProxy__local', local)
==> self.__local = _lookup_req_object('request')
session = LocalProxy(partial(_lookup_req_object, 'session'))
目标: 去local中获取ctx,然后在ctx中获取request
请求上下文 : RequestContext
- request
- session
应用上下文 : AppContext
- app(current_app)
- g
from flask import g
app_ctx = AppContext()
app_ctx.app
app_ctx.g
多线程:
local保存数据时,使用列表创建出来的栈
多app应用
web访问多app应用时,上下文管理是如何实现? 同单app一样。
为什么要使用离线脚本?
- 如果是web运行环境,栈中永远保存1条数据(可以不用栈)
- 写脚本获取app信息时,可能存在app上下文嵌套时。
g: 每个请求周期都会创建一个用于在请求周期中传递值的一个容器。
信号
http://www.cnblogs.com/wupeiqi/articles/8249576.html
pip3 install blinker
a. before_first_request
b. 触发request_started 信号
c. before_request
d. 模板渲染
渲染前的信号: before_render_template
渲染后的信号: template_rendered.send()
e. after_request
f. session.save_session()
q. 触发request_finished信号
错误触发信号: got_request_exception.send(self, execption=e)
h. 触发信号 request_tearing_down.send(self, exc=exc)
flask-session
https://www.cnblogs.com/wupeiqi/articles/7552008.html
- session处理机制
- 请求刚到来: 获取随机字符串,存在刚去数据库中获取原来的个人数据,否则创建
一个空容器 --> 内存: 对象(随机字符串)
self.app.open_session --> SecureCookieSession()
session['xxx'] = 123
在local的ctx中找到session,在空字典中写值
请求刚来self.session = None
ctx.push()
self.session = session_interface.open_session(self.app, self.request)
视图函数:
self.dispatch_requeset()
self.finalize_request()
self.process_response()
self.session_interface.save_session(self, ctx.session, response)
response.set_cookie("session", "将数据加密之后写到cookie中")
cookie:
内容: key, value, 过期时间, 路径和域
如果设置了过期时间就保存在硬盘里。
goole设置cookie的响应头的例子:
HTTP/1.1 302 Found
Location: http://www.google.com/intl/zh-CN/
Set-Cookie:PREF=ID=0565f77e132de138:NW=1:TM=1098082649:\
LM=1098082649:S=KaeaCFPo49RiA_d8;
expires=Sun, 17-Jan-2038 19:14:07 GMT;
path=/; domain=.google.com
Content-Type: text/html
session:
cookie可以被人为的禁止,
1. 使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种,
(1)一种是作为URL路径的附加信息,表现形式为http://...../xxx;jsessionid=ByOK ... 99zWpBng!-145788764
(2)是作为查询字符串附加在URL后面,表现形式为http://...../xxx?jsessionid=ByOK ... 99zWpBng!-145788764
2.表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器
何时被删除:
于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间
1.方式一
from redis import Redis
redis = Redis()
app.session_interface = RedisSessionInterface(redis, key_prefix="__")
# permanent=False, 关闭浏览器cookie失效。 持久化
2.方式二
from redis import Redis
from flask.ext.session import Session
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = Redis(host='192.168.0.94',port='6379')
Session(app)
1.内置原理
2.自定义
3.flask_session
wtforms
https://www.cnblogs.com/wupeiqi/articles/8202357.html
fabric
ansible
salstack
new 真正创建对象:
什么时候用到了__new__:
- 单例
- WTForms,当前对象/UnboundField对象
- rest_framework序列化
many=True
many=False
__init__对对象做初始化
SQLAlchmey
http://www.cnblogs.com/wupeiqi/articles/8259356.html
ORM框架 Object Relational Mapper 关系对象映射
类/对象操作 --> SQL --> pymysql,MySQLdb --> 再在数据库中执行
http://www.cnblogs.com/wupeiqi/articles/8259356.html
pip3 install sqlalchemy
schema: 提要,纲要,图式
ORM操作
session=Session()
#session=scoped_session()
obj1 = models.User(name="alex1", email="alex1@xx.com")
session.add(obj1)
session.commit()
session.close()
查询
Role.query.filter(Role.id>1).all()
role = Role.query.filter_by(name=r).first()
from sqlalchemy import text
Role.query.filter(text('id>1')).all()
# filter与filter_by的区别:
#filter用类名.属性名,比较用==,filter_by直接用属性名,比较用=
#filter: 比filter_by的功能更强大,支持比较运算符,支持or_、in_等语法。
query.filter(and_(IS.node == node, IS.password == password))
session.query(IS).filter(IS.node == node).filter(IS.password == password).all()
#filter_by: 不支持比较运算符。
session.query(IS).filter_by(node=node, password=password).all()
session.query(Users).from_statement(text("SELECT * FROM users where name=:name")).params(name='ed').all()
Flask 使用数据库
Flask-SQLAlchemy 扩展
pip install flask-sqlalchemy
#配置数据库的地址
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:密码@127.0.0.1:3306/test'
#跟踪数据库的修改 --> 不建议开启,未来会移除
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAchemy(app)
class Role(db.Model):
#定义表名
__tablename__ = 'roles'
#定义字段
id = db.Column(db.Integer,primary_key=True)
name = db.Column(db.String(16),unique=True)
#在一的一方,写关联
#users = db.relationship('User')
users = db.relationship('User',backref='Role')
from flask_SQL import Role, db
role = Role(name="admin")
db.session.add(role)
db.session.commint() # 提交后数据库才会改变
# 改:
user.name = 'chenxuyuan'
db.session.commit()
# 删:
db.session.delete(user)
db.session.commit()
# 回退
db.session.rollback()
模型之间的关联
一对多:
db.relationship()
过滤器:
filter() #User.query.filter(User.id==4).first()
filter_by() #User.query.filter_by(id=3).first()
limit
offset()
order_by()
all() #User.query.all()
first()
first_or_404()
get()
get_or_404()
count()
paginate()
count()查询:
flask-migrate
from flask_migrate import Migrate, MigrateCommand
migrate = Migrate(app, db) # db = SQLAlchemy()
数据库迁移命令
python manager.py db init
python manager.py db migrate
python manager.py db upgrade
manager.add_command('db', MigrateCommand)
flask-script
from flask_scritp import Manager
manager = Manager(app)
manager.run()
python3 manager.py runserver
@manager.command
def custom(arg):
"""
自定义命令
python manager.py custom 123
"""
print(arg)
@manager.option('-n', '--name', dest='name')
@manager.option('-u', '--url', dest='url')
def cmd(name, url):
""" python manager.py cmd -n tom -u http://www.tom.com
"""
《Flask Web开发:基于Python的Web应用开发实战》
pip3 install flask_script
export FLASK_APP=hello.py
export FLASK_DEBUG=1
flask_run
上下文:
应用上下文:
current_app
g
请求上下文:
request
session
使用这些变量要先激活:
from flask import current_app
app_ctx = app.app_context() *
app_ctx.push() * 激活
current_app.name
app_ctx.pop()
请求调度:
app.url_map
<Rule '/index' (HEAD, OPTIONS, GET) -> index>
请求钩子
before_first_request
before_request
after_request
teardown_request
响应:
1.
return '
Bad Request
', 400视图函数返回的响应还可接受第三个参数 header组成的字典
from flask import make_reponse
response = make_response('<h1> This document carries a cookie</h1>')
response.set_cookie('answer', '42')
return response
-
reedirect
-
abort(404)
模板: jinja2
渲染模板:
render_template
常用过滤器:
safe 渲染值时不转义
capitalize 首字母大写 ,其他小写
lower
upper
title 每个单词首字母大写
trim 首尾空格去掉
striptags 渲染之前把值中的所有HTML标签都删掉
控制结构
{% if user %}
...
{% else %}
...
{% endif %}
for:
{% for i in com %}
...
{% endfor %}
宏
{% macro render_comment(comment) %}
<li> {{ comment }} </li>
{% endmacro %}
在模板中导入
{% import 'macros.html' as macros %}
{{ macros.render_comment(comment) }}
{% endfor %}
模板
base.html
<html>
<head>
{ % block head % }
<title>{ % block title % }{ % endblock % } - My Application</title>
{ % endblock % }
</head>
<body>
{ % block body % }
{ % endblock % }
</body>
</html>
使用模板
{ % extends "base.html" % }
{ % block title % }Index{ % endblock % }
{ % block head % }
{{ super() }}
<style>
</style>
{ % endblock % }
{ % block body % }
<h1>Hello, World!</h1>
{ % endblock % }
链接
url_for()
url_for('index')
url_for('index', _external=True) # 返回的是绝对地址 http://localhost:5000/
url_for('user', name='john', _external=True)
url_for('index', page=2) # 返回: /?page=2
flash('Looks')
get_flashed_messages()
flask-migrate
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
migrate.init_app(app, db)
flask db init # 生成migrate目录
flask db migrate # 自动创建一个迁移脚本 -m "message"
flask db upgrade # 把迁移应用到数据库中
配置flask_mail
app.config['MAIL_SERVER'] = 'smtp.163.com'
app.config['MAIL_PORT'] = 25
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
flask shell
from flask_mail import Message
from hello import mail
msg = Message('test email', sender='you@example.com', recipients=['you@example.com'])
msg.body = 'This is the plain text body'
msg.html = 'This is the <b>HTML</b> body'
with app.app_context():
mail.send(msg)
flask-login
保护路由:
from flask_login import login_required
@app.route('/secret')
@login_reuqired
def secret():
return "Only authenticated users are allowed!"
flask 插件
- flask_themes: 皮肤,博客必不可少的
- flask_sqlalchemy: flask对sqlalchemy的插件,定义了一些方法,使创建models和输出query更方便
- flask_wtf: 对wtforms的插件,默认加入了csrf功能(防止表单重复提交)和Recaptcha(验证码)
- flask_uploads: 上传文件的插件
- flask_cache: 缓存插件(支持memcached,gaememcached,filesystem,simple等)
- flask_principal: 权限插件 (众多插件中比较复杂的一个, 但也是作用很大的一个),支持各种权限方式,较django admin的权限,我只能说,这个插件让你知道,权限其实很简单。
- flask_mail: 发送邮件插件
- flask_script: 项目管理插件,类似django的manager
- flask_babel: 多语言支持,使用非常方便,(request.accept_languages.best_match判断语言有点怪,好象会根据系统语言判断,待深究)
- PyCasbin
- flask_admin
- flask_login
Django
创建工程
django-admin startproject demo
demo
├── demo
│ ├── init.py
│ ├── settings.py
│ ├── urls.py 项目的URL配置文件。
│ └── wsgi.py 项目与WSGI兼容的Web服务器入口
└── manage.py 是项目管理文件,通过它管理项目。
# 运行
python manage.py runserver ip:port
创建子应用模块
python manage.py startapp users
users
├── admin.py 文件跟网站的后台管理站点配置相关
├── apps.py 文件用于配置当前子应用的相关信息。
├── __init__.py
├── migrations 用于存放数据库迁移历史文件。
│ └── __init__.py
├── models.py 用户保存数据库模型类。
├── tests.py
└── views.py 编写Web应用视图。
注册安装子应用
# settings.py
INSTALLED_APPS = [
...
'users.apps.UserConfig',
]
配置数据库
settings.py
# 默认配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
配置语言与时区
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai' // 大小写
静态文件配置
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static_files'),
]
from django.conf import settings
from django.conf.urls.static import static
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
链接 URL 映射器
urlpatterns = [
path("admin/", admin.site.urls),
path('catalog/', include('catalog.urls')),
path('', RedirectView.as_view(url='/catalog/')), # 将首页重定向到catalog
]
运行数据库迁移
python3 manage.py makemigrations
python3 manage.py migrate
运行
python3 manage.py runserver
Django管理员站点
注册模型
from .models import Author, Genre, Book, BookInstance
admin.site.register(Book)
admin.site.register(Author)
admin.site.register(Genre)
admin.site.register(BookInstance)
创建超级用户
python3 manage.py createsuperuser
注册一个ModelAdmin类
/locallibrary/catalog/admin.py
# Define the admin class
class AuthorAdmin(admin.ModelAdmin):
# 配置列表视图
list_display = ('last_name', 'first_name', 'date_of_birth', 'date_of_death')
admin.site.register(Author, AuthorAdmin)
# 控制哪些字段被显示和布局, 元组是分组
fields = ['first_name', 'last_name', ('date_of_birth', 'date_of_death')]
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'display_genre')
# 需要在Book模型中添加display_genre函数
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
summary = models.TextField(max_length=1000, help_text="Enter a brief description of the book")
isbn = models.CharField('ISBN',max_length=13, help_text='13 Character <a href="https://www.isbn-international.org/content/what-isbn">ISBN number</a>')
genre = models.ManyToManyField(Genre, help_text="Select a genre for this book")
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('book-detail', args=[str(self.id)])
def display_genre(self):
return ', '.join([ genre.name for genre in self.genre.all()[:3] ])
display_genre.short_description = 'Genre'
添加列表过滤器
class BookInstanceAdmin(admin.ModelAdmin):
list_filter = ('status', 'due_back')
# 使用 fieldsets 属性添加“部分”以在详细信息表单中对相关的模型信息进行分组
fieldsets = (
(None, {
'fields': ('book','imprint', 'id')
}),
('Availability', {
'fields': ('status', 'due_back')
}),
)
关联记录的内联编辑
# 通过声明 inlines, 类型 TabularInline (水平布局 ) or StackedInline (垂直布局,就像默认布局)
class BooksInstanceInline(admin.TabularInline):
model = BookInstance
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'display_genre')
inlines = [BooksInstanceInline]
路由
#2.X:
from django.urls import path, include, re_path
path('index-<int:article_type_id>-<int:category_id>.html',views.index)
re_path就是1.X中的url('^/usrs')
url('index-(?P<article_type_id>\d+)-(?P<category_id>\d+).html',views.index)
path 不要^$等符号,类似flask
int
str
slug ASCII或数字组成的,包括-_,
uuid 必须包含短划线并且字母必须为小写
path
# 名字空间:
url(r'^users/', include('users.urls', namespace='users')),
# 名字:
url(r'^index/$', views.index, name='index'),
# reverse反解析
url = reverse('users:index')
# eg:
path(r"detail/<yyyy:id>", detail, name='detail')
url = reverse('index', kwargs={"id": 1992}) # 没有kwargs会报错
自定义转化器
class FourDigitYearConverter:
regex = '[0-9]{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return "%04d"%value
# 注册
from django.urls import register_converter
register_converter(FourDigitYearConverter, "yyyy")
图标
from Django.views.generic.base import RedirectView
urlpatterns=[
url(r'^favicon.ico$',RedirectView.as_view(url=r'static/favicon.ico')),
]
请求
# 1.URL路径参数:
url(r'^weather/(?P<city>[a-z]+)/(?P<year>\d{4})/$', views.weather),
def wearther(request, year, city):
...
# 2.查询字符串Query String QueryDict对象
request.GET.get('a') # flask:request.arg.get()
# 3.请求体:
表单类型: Form Data
request.POST.get('a') # flask: request.form.get()
非表单类型 Non-Form Data
json_str = request.body
req_data = json.loads(json_str)
4.请求头
request.META
其他常用HttpRequest对象
method
user
path
encoding
FILES
响应
# 1.django.http.HttpResponse
HttpResponse(content=响应体, content_type=响应体数据类型, status=状态码)
# 2.HttpResponse子类: (快速设置状态码)
HttpResponseRedirect 301
# 3.JsonResponse
from django.http import JsonResponse
# 4.redirect
from django.shortcuts import redirect
Cookie
# 设置Cookie:
HttpResponse.set_cookie(cookie名, value=cookie值, max_age=cookie有效期)
eg: response.set_cookie('itcast2', 'python2', max_age=3600)
# 读:
request.COOKIES.get('itcast2')
Session
# 启用Session:
默认启用:
setttings.py MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware'
]
#存储方式:
# 1.数据库:
SESSION_ENGINE='django.contrib.sessions.backends.db' (默认)
# 本地缓存 .cache/.cache_db
INSTALLED_APPS = ['django.contrib.sessions']
- Redis /etc/redis/redis.conf
在redis中保存session,需要引入第三方扩展,我们可以使用django-redis来解决。
(1) 安装扩展
pip install django-redis
(2) 配置
# 在settings.py文件中做如下设置
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
# 操作:
request.session['key'] = value
# 清理:
reqeust.session.clear() 删除值部分
# 清除session数据:
request.session.flush()
# 删除指定键及值
del request.session['key']
# 设置session的有效期:
request.session.set_expiry(value)
# 记录访问次数
num_visits = request.session.get("num_visits", 0)
request.session["num_visits"] = num_visits + 1
创建视图
类视图
from django.views.generic import View
class RegisterView(View):
def get(self,request):
...
def post(self, request):
...
# 路由:
url(r'^register/$', views.RegisterView.as_view(), name='register')
#使用装饰器:
#1.在URL配置中装饰:
url(r'^demo/$', my_decorate(DemoView.as_view()))
#2.在类视图中装饰:
from django.utils.decorators import method_decorator
@method_decorator(my_decorator, name='dispatch') // name为指定装饰的函数
@method_decorator(my_decorator) # 为get方法添加了装饰器
def get(self, request):
print('get方法')
return HttpResponse('ok')
#类视图Minxin扩展类
class ListModeMixin(object):
def list(self, request,*args, **kwargs):
...
class BooksView(ListModeMixin, View):
...
中间件
定义方法: 在users应用中新建一个middleware.py文件
def my_middleware(get_response):
# 此处编写的代码仅在Django第一次配置和初始化的时候执行一次。
def middleware(request):
# 此处编写的代码会在每个请求处理视图前被调用。
...
response = get_response(request)
# 此处编写的代码会在每个请求处理视图之后被调用。
...
return respons
return middleware
# 定义好中间件后,需要在settings.py 文件中添加注册中间件
MIDDLEWARE = [
'django.middleware.my_middleware',
]
多个中间件的执行顺序:
在请求视图被处理前,中间件由上至下依次执行
在请求视图被处理后,中间件由下至上依次执行
class MiddlewareMixin:
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request):
response = None
if hasattr(self, 'process_request'):
response = self.process_request(request)
response = response or self.get_response(request)
if hasattr(self, 'process_response'):
response = self.process_response(request, response)
return response
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse,redirect
class middle_ware1(MiddlewareMixin):
def process_request(self,request):
if request.path_info == "/login/":
return None
if not request.session.get("user_info"):
return redirect("/login/")
def process_response(self,request,response):
print("middle_ware1.process_response")
return response
列举django中间件的5个方法:
#1.process_request : 请求进来时,权限认证
#2.process_view : 路由匹配之后,能够得到视图函数
#3.process_exception : 异常时执行
#4.process_template_responseprocess : 模板渲染时执行
#5.process_response : 请求有响应时执行
# 应用场景
1.IP访问频率限制
2.URL访问过滤
模板
配置: settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 此处修改
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
<html lang="en">
<head>
{% block title %}<title>Local Library</title> {% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.3/js/bootstrap.min.js"></script>
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
{% block sidebar %}
<ul class="sidebar-nav">
<li><a href="{% url 'index' %}">Home</a></li>
<li><a href="{% url 'books' %}">All Books</a></li>
<li><a href="{% url 'authors' %}">All Authors</a></li>
</ul>
{% endblock %}
</div>
<div class="col-md-10">
{% block content %}{% endblock %}
{% block pagination %}
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="{{ request.path }}?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="{{ request.path }}?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</div>
{% endif %}
{% endblock %}
</div>
</div>
</div>
</body>
</html>
数据库
配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': '127.0.0.1', # 数据库主机
'PORT': 3306, # 数据库端口
'USER': 'root', # 数据库用户名
'PASSWORD': 'mysql', # 数据库用户密码
'NAME': 'django_demo' # 数据库名字
}
}
Model
from django.db import Models
models.Model
IntegerField
BooleanField
CharField (max_length, default)
DecimaField (max_digits=5, decimal_places=2)
ImageField
EmailField
gtype = ForeignKey(TypeInfo, on_delete=models.CASCADE, verbose_name="分类")
HTMLField // from tinymce.models import HTMLField
class Meta:
verbose_name = "商品"
table_name = "goods_info"
数据库操作
增: 1.book = BookInfo(btitle='sql',...)
book.save()
2. BookInfo.objects.create(hname='john',...)
删: book = BookInfo.objects.get(id=1)
book.delete()
改: 1. book = BookInfo.objects.get(id=1)
book.btitle='mysql'
book.save()
2. BookInfo.objects.filter(id=1).update(btitle='射雕')
查:
基本查询: get ,all, count
过滤查询:
filter
exclude 排除符合条件剩下的结果
get 过滤单一结果
属性名称__比较运算符=值
1)相等
exact: 相等
BookInfo.objects.filter(id__exact=1) , 简写id=1
2)模糊查询
contains 包含
startswith, endswith
算符前加上i表示不区分大小写,如iexact、icontains、istartswith、iendswith
3)空查询
isnull
BookInfo.objects.filter(btitle__isnull=False)
4)获取查询
in
BookInfo.objects.filter(id__in=[1, 3, 5])
5)比较查询:
gt
gte
lt
lte
不等于的运算符,使用exclude()过滤器。
6)日期查询:
year、month、day、week_day、hour、minute、second
BookInfo.objects.filter(bpub_date__year=1980)
BookInfo.objects.filter(bpub_date__gt=date(1990, 1, 1))
F对象:
两个属性怎么比较呢?
from django.db.models import F
BookInfo.objects.filter(bread__gte=F('bcomment'))
Q对象:
多个过滤器逐个调用表示逻辑与关系,同sql语句中where部分的and关键字。
BookInfo.objects.filter(bread__gt=20,id__lt=3)
或
BookInfo.objects.filter(bread__gt=20).filter(id__lt=3)
Q对象可以使用&、|连接,&表示逻辑与,|表示逻辑或
BookInfo.objects.filter(Q(bread__gt=20) | Q(pk__lt=3))
~ 不等于
BookInfo.objects.filter(~Q(pk=3))
聚合函数
Avg 平均,Count 数量,Max 最大,Min 最小,Sum 求和,被定义在django.db.models中
BookInfo.objects.aggregate(Sum('bread'))
排序:
order_by
BookInfo.objects.all().order_by('-bread') # 降序
关联查询:
一对多:
查询某本书所有英雄:
b = BookInfo.objects.get(id=1)
b.heroinfo_set.all()
多对一:
某个英雄属于哪本书
h = HeroInfo.objects.get(id=1)
h.hbook
关联过滤查询:
关联模型类名小写__属性名__条件运算符=值
查询图书,要求图书英雄为"孙悟空"
BookInfo.objects.filter(heroinfo_hname='孙悟空')
BookInfo.objects.filter(heroinfo_hname='孙悟空')
查询书名为“天龙八部”的所有英雄。
HeroInfo.objects.filter(hbook__btitle='天龙八部')
查询图书阅读量大于30的所有英雄
HeroInfo.objects.filter(hbook__bread__gt=30)
分页
from django.core.paginator import Paginator
1.Paginator
p = Paginator(a1, 10) # a1可以是列表,查询集
常用属性:
count
num_pages
page_range 下标从 1 开始的页数范围迭代器。
2.Page对象
books = p.page(1) // books[0]
常用属性:
object_list
number 当前页序号
paginator
常用函数:
has_next()
has_previous()
has_other_pages() # 是否有上或下一页
next_page_number()
previous_page_number()
查看 SQL语句
1) QuerySet有query属性,只能查看select
2)
from django.db import connection
connection.queries
执行SQL语句
1)
c = connection.cursor()
c.execute("""...""")
c.fetchone()
2)
BookInfo.objects.raw("""select * from tb_books")
必须要包含BookInfo的主键
查询集QuerySet
all()
filter()
exclude()
order_by() 会返回查询集
两大特性:
惰性执行
qs = BookInfo.objects.all()
for book in qs: -->.才真正去数据库查询
缓存
[ book.id for book in qs]
管理器Manager
自定义管理器
1. 修改原始查询集,重写all()方法。
a)打开booktest/models.py文件,定义类BookInfoManager
#图书管理器
class BookInfoManager(models.Manager):
def all(self):
#默认查询未删除的图书信息
#调用父类的成员语法为:super().方法名
return super().filter(is_delete=False)
b)在模型类BookInfo中定义管理器
class BookInfo(models.Model):
...
books = BookInfoManager()
c)使用方法
BookInfo.books.all()
django请求的生命周期
- wsgi,请求封装后交给web框架 (Flask、Django)
- 中间件,对请求进行校验或在请求对象中添加其他相关数据,例如:csrf、request.session
- 路由匹配 根据浏览器发送的不同url去匹配不同的视图函数
- 视图函数,在视图函数中进行业务逻辑的处理,可能涉及到:orm、templates => 渲染
- 中间件,对响应的数据进行处理。
- wsgi,将响应的内容发送给浏览器。
FBV: function base view
CBV: class
用户授权与许可
# 配置 默认已配置
INSTALLED_APPS = [
...
'django.contrib.auth',
'django.contrib.contenttypes',]
MIDDLEWARE = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
....
tornado
========================================================================
使用场景:
用户量大,高并发
大量的HTTP持久连接
C10K :
Concurently handling ten thousand connections.
少而精
异步非阻塞的设计方式
特点:
HTTP服务器
异步编程
WebSockets
app=tornado.web.Application([(),])
app.listen(8000) #只是绑定端口。 只能在单进程模式中使用
tornado.ioloop.IOLoop.current().start() #IOLoop.current(): 返回当前线程的IOLoop实例。
#.start() 启动,同时开启监听。
=== handler ======================= 请求处理
===================================
=== tornado.web.Application ======= 路由映射表
===================================
=== tornado-IOLoop ================
===================================
=== linux-epoll =================== 监听socket
===================================
httpserver
import tornado.httpserver
httpServer = tornado.httpserver.HTTPServer(app)
httpServer.bind(8000)
httpServer.start(4) 启动4个进程。
tornado.ioloop.IOLoop....
不建议使用上面方式启动多进程。
问题:
1.每个子进程都会从父进程中复制一份IOLoop的实例,如果在创建子进程前修
改了IOLoop,会影响所有的子进程。
2.所有进程都是由一个命令启动的,无法做到在不停止服务的情况下修改代码。
3.所有进程共享一个端口,想要分别监控很困难。
而是手动启动。多几个server.py
options
提供全局参数的定义、存储、转换。
from tornado.options import define,options
define(name,default=None,type=None,help=None,
metavar=None,multiple=False,group=None,callback=None)
取出值
options.name
type:变量类型,根据default类型进行转换,转换不成会报错。如果没有default,则不转换
可以有str,float,int,datetime,timedelta
multiple:
设置选项变量是否可以为多个值,默认为False
获取参数的方法
1.tornado.options.parse_command_line() #将命令行里的参数转换成
---- 不建议用
2.tornado.options.parse_config_file(path)
从配置文件导入参数。
书写格式仍需要按照python格式写
不支持字典。
3.config.py
options={
"port":8000,
"list":["good","nice","handsome"]
}
settings={
"autoreload":True, #自动重启。
"debug":False,
}
日志:
当我们在代码中使用parse_command_line()或者parse_config_file(path)方法
时,tornado会默认开启logging模块功能,向屏幕终端输入一些打印信息
关闭日志:
1.--logging=none
2.tornado.options.options.logging=None #在导入配置文件前执行。
settings:
debug:
设置tornado是否工作在调试模式下,默认为False,即在工作生产模式下
True:会监控源代码文件,当有保存改动时会重新启动服务器。 --->autoreload=True
取消缓存编译的模板 (可单独设置:compiled_template_cache=False)
取消缓存静态文件的hash值 (static_hash_cache=False)
提供追踪信息。 (serve_traceback=True)
static_path:
template_path:
传参:
def initialize(self,word1,word2)
反向解析:
路由:
tornado.web.url() name=""
self.reverse_url("...")
tornado.web.RequestHandler:
利用HTTP协议向服务器传递参数:
1.提取uri的特定部分
(r'/liu/(\w+)/(\w+),index.LiuHandler) # (?P
get(self,h1,h2)
2.get方式传递参数
127.0.0.1:8000/zhang?a=1&b=2&c=3:
self.get_query_argument(name,default=ARG_DEFAULT,strip=True)
strip:是否过左右两边的空白字符,默认为True.
list = self.get_query_arguments()
3.post方式传递参数
self.get_body_argument(name,default=ARG_DEFAULT,strip=True)
4.可以获取get,post请求,
self.get_argument() s
5.在http报文的头中增加自定义的字段。
tornado
========================================================================
使用场景:
用户量大,高并发
大量的HTTP持久连接
C10K :
Concurently handling ten thousand connections.
少而精
异步非阻塞的设计方式
特点:
HTTP服务器
异步编程
WebSockets

app=tornado.web.Application([(),])
app.listen(8000) #只是绑定端口。 只能在单进程模式中使用
tornado.ioloop.IOLoop.current().start() #IOLoop.current(): 返回当前线程的IOLoop实例。
#.start() 启动,同时开启监听。

=== handler ======================= 请求处理
===================================
=== tornado.web.Application ======= 路由映射表
===================================
=== tornado-IOLoop ================
===================================
=== linux-epoll =================== 监听socket
===================================
httpserver

import tornado.httpserver
httpServer = tornado.httpserver.HTTPServer(app)
httpServer.bind(8000)
httpServer.start(4) 启动4个进程。
tornado.ioloop.IOLoop....
不建议使用上面方式启动多进程。
问题:
1.每个子进程都会从父进程中复制一份IOLoop的实例,如果在创建子进程前修
改了IOLoop,会影响所有的子进程。
2.所有进程都是由一个命令启动的,无法做到在不停止服务的情况下修改代码。
3.所有进程共享一个端口,想要分别监控很困难。
而是手动启动。多几个server.py
options
提供全局参数的定义、存储、转换。
from tornado.options import define,options
define(name,default=None,type=None,help=None,
metavar=None,multiple=False,group=None,callback=None)
取出值
options.name
type:变量类型,根据default类型进行转换,转换不成会报错。如果没有default,则不转换
可以有str,float,int,datetime,timedelta
multiple:
设置选项变量是否可以为多个值,默认为False
获取参数的方法

1.tornado.options.parse_command_line() #将命令行里的参数转换成
---- 不建议用
2.tornado.options.parse_config_file(path)
从配置文件导入参数。
书写格式仍需要按照python格式写
不支持字典。
3.config.py
options={
"port":8000,
"list":["good","nice","handsome"]
}
settings={
"autoreload":True, #自动重启。
"debug":False,
}
日志:
当我们在代码中使用parse_command_line()或者parse_config_file(path)方法
时,tornado会默认开启logging模块功能,向屏幕终端输入一些打印信息
关闭日志:
1.--logging=none
2.tornado.options.options.logging=None #在导入配置文件前执行。
settings:
debug:
设置tornado是否工作在调试模式下,默认为False,即在工作生产模式下
True:会监控源代码文件,当有保存改动时会重新启动服务器。 --->autoreload=True
取消缓存编译的模板 (可单独设置:compiled_template_cache=False)
取消缓存静态文件的hash值 (static_hash_cache=False)
提供追踪信息。 (serve_traceback=True)

static_path:
template_path:
传参:
def initialize(self,word1,word2)
反向解析:
路由:

tornado.web.url() name=""
self.reverse_url("...")
tornado.web.RequestHandler:
利用HTTP协议向服务器传递参数:
1.提取uri的特定部分
(r'/liu/(\w+)/(\w+),index.LiuHandler) # (?P<p1>\w+)
get(self,h1,h2)
2.get方式传递参数
127.0.0.1:8000/zhang?a=1&b=2&c=3:
self.get_query_argument(name,default=ARG_DEFAULT,strip=True)
strip:是否过左右两边的空白字符,默认为True.
list = self.get_query_arguments()
3.post方式传递参数
self.get_body_argument(name,default=ARG_DEFAULT,strip=True)
4.可以获取get,post请求,
self.get_argument() srequest对象:
作用: 存储了关于请求的相关信息
属性:
method :
host:
uri:
path
query
version
headers
body
remote_ip 客户端的ip
files 用户上传的文件,字典类型
tornador.httputil.HTTPFile:
属性:
filename:
body: 文件数据实体
content_type: 文件类型。
<input type="file" name="img"/>
self.request.files={"name":[{"filename":..,
"body":b"...",
"content_type":'text/plain'}] -->看着是字典,
其实是HTTPFile对象。
"name2":[{...}]
}
HTTP 1.1 特点: 长连接。
响应输出:
write:
将数据写入缓冲区。 (1.程序结束,2.手动刷新,3.缓冲区满了。4.遇到\n)
finish: 刷新缓冲区,关闭当次请求通道。
利用write方法写json数据。
set_header(name,value):
作用:手动设置一个名为name,值为value的响应头字段。
set_default_headers()
def set_default_headers(self): 在进入get()之间调用。
self.set_header(...)
self.set_status(status_code,reason=None):
重定向:
redirect(url)
send_error(status_code=500,kwargs)
作用:抛出HTTP错误状态码,
write_error(status_code,kwargs)
作用:用来处理send_error抛出的错误信息,并返回给浏览器错误界面。
接口调用顺序:
set_default_headers()
initialize() 处理参数。url后,字典。
prepare() : 预处理方法,在执行对应的请求方法之前调用。
HTTP方法: get
post
head ,类似get请求,只不过响应中没有具体的内容,报头。
delete 请求服务器删除指定的资源
put 从客户端向服务器传送指定内容
patch 请求修改局部内容
options 返回URL支持的所有HTTP方法。
write_error
on_finish() 作用:资源释放,日志记录。
尽量不要在该方法中进行响应输出。
正常:
set_default_headers
initialize
prepare
HTTP方法
on_finish
出错:
set_default_headers
initialize
prepare
HTTP方法
set_dufault_headers
write_error
on_finish
模板
配置模板路径: "template_path":os.path.join(BASE_DIRS,"templates")
render()
变量与表达式:
语法: {{ var }}
{{ expression }}
流程控制:
if
for
while
函数:
static_url()
获取配置的静态目录,并将参数拼接到 静态目录后面并返回新的路径。
href="{{static_url('home.css')}}"
优点:
修改目录不用修改url
创建了一个基于文件内容的hash值,并将其添加到url末尾。总能保存每次加载的是最新的。
自定义函数:
在handler里定义,传到模板里。 #django里也有方法,不过是对象的方法,不能传参。
转义:
默认开启了自动转义功能,防止网站被攻击。
关闭: raw : {% raw str %}
当前文档的:{% autoescape None %}
在配置中修改 setttings={"autoescape":None}
escape()函数:
继承:
{% block main %}
...
{% end %}
静态文件:
static_path:
staticFileHandler:
本质:是tornado预制的用来提供静态资源文件的handler。
作用:映射静态文件
(r'/(.*)$',tornado.web.StaticFileHandler,{"path":os.path.join(config.BASE_DIRS,'static'),"default_filename":"index.html"})
数据库
tornado没有自带的ORM,对于数据库需要自己去适配。
应用安全
Cookie: 4kb,20条
普通Cookie:
设置: self.set_cookie(name,value,domain=None,expires=None,path="/",expires_days=None,**kwargs)
expires: 时间戳整数,时间元组,datetime类型,
expires_days: 有效天数,优先级低于expires.
获取: selt.get_cookie(name,default)
清除cookie:
self.clear_cookie(name,path="/",domain=None)
作用: 删除名为name的cookie,并同时匹配path,domain
并不是立即删除浏览器的cookie,而是给cookie值设置空,并改变其有效其为失效,
真正删除的是浏览器自己。
self.clear_all_cookies()
安全Cookie: tornado提供一种简易加密方式。
设置: 需要为应用配置一个用来给Cookie进行混淆加密的密钥。
"cookie_secret":"......." (base64.b64encode(uuid.uuid4().bytes))
self.set_secure_cookie(name, value, expires_days=30, version=None)
"2|1:0|10:1528168601|5:hello|8:d29ybGQ=|905b4630a06055c8b0867e0c2bcc69721d7a1412fcbf24190c0bc2f9a8b533e3"
安全cookie的版本,2,
默认为0
时间戳
cookie名
base64编码的cookie值
签名值,不带长度说明
获取: self.get_secure_cookie(name,value=None,max_age_day=31,min_version=None)
XSRF: 跨站请求代码。
保护: 同源策略。
不用get,用post。
开启xsrf保护: setting: "xsrf_cookies":True
在模板中:
{% module xsrf_form_html()%} 为浏览器设置了_xsrf的安全cookie.
原因:为模板表单添加了一个隐藏的域,名为_xsrf,值为_xsrf的cookie的值
<input type="hidden" name="_xsrf",value=
"2|fd3fd9d|dafkfdksajfdsajf..." > (在代码中添加也能访问)
在非模板中:手动设置_xsrf的cookie.
self.xsrf_token
function getCookie(name){
var cookie=document.cookie.match("\\b"+name+"([^;]*)\\b")
return cook? cook[1]:undefined
}
发起ajax请求:
<script type="text/javascript" charset="utf-8"
src="{{static_url('js/jquery.min.js)}}"></script> 引入js.
function login(){
$.post("/postfile","_xsrf="+getCookie("_xsrf")+"&username="+"sunck"...",
function(data){ // 回调函数。
alert("ok")
})
}
2.function login(){
data = {"username":"sunck","passwd":"123456"}
var datastr = JSON.stringify(data)
$.ajax({
url:"/postfile",
method:"POST", // 可以GET
data:datastr,
success:function(data){
alert("ok")
}
headers:{
"X-XSRFToken":getCookie("_xsrf")
}
})
}
在进入主页时就自动设置xsrf ,
继承tornado.web.StaticFileHandler,
def init(self,args,**kw):
super().init(args,**kw)
self.xsrf_token
用户验证:
tornado.web.authenticated装饰器, Tornado将确保合法用户才能用
验证:
def get_current_user(self):
验证失败,将重定向到配置文件中的"login_url":"/login"
class LoginHandler(RequestHandler):
def get(self):
next=self.get_argument("next","/login")
url = "login?next="+next
self.render("login.html",next=url)
class HomeHandler():
def get_current_user(self):
logined = self.get_cookie("logined",None)
return logined
@authenticated
def get(self):
self.write("welcome to home")
同步与异步:
tornado.httpclient.AsyncHTTPClient :
fetch(request.callback=None) --> 用于执行一个web请求,并异步响应返回一个
tornado.httpclient.HttpRespones
request可以是一个 url,也可以是一个tornado.httpclient.HttpRequest
HttpRequest: HTTP请求类,该类的构造函数可以接收参数
参数:
回调异步:
class StudentsHandler(RequestHandler):
def on_respone(self,response):
if response.error:
self.send_error(500)
else:
data = json.loads(response.body)
self.write(data)
self.finish() #关闭通道
@tornado.web.asynchronous #不关闭通信的通道。
def get(self,*args):
url = "http://www......"
client = AsyncHTTPClient()
client.fetch(url,self.on_response)
self.write("ok")
协程异步:
@tornoda.gen.corountine
def get(self, *args):
url = "http://www......"
client = AsyncHTTPClient()
res = yield client.fetch(url) --> self.getDate()
if res.error:
ret={"ret":0}
else:
ret = json.loads(res.body)
self.write(ret)
Tornado的WebSocket模块:
open()
on_message(message)
on_close() 当WebSocket链接关闭后调用
write_message(message,binary=False) 主动向客户端发送message消息,字符串或字典(自动转json)
close() 关闭WebSocket链接
check_origin(origin) 判断源origin,对于符合条件的请求源允许连接。
blacksheep
大部分代码都是基于 Cython 编写的
pip install 'blacksheep[full]' uvicorn
简单程序
from blacksheep import Application
import uvicorn
# 和 FastAPI 一样,先创建一个 app
app = Application()
# 通过装饰器的方式注册路由
# 如果 methods 参数不指定,默认为 ["GET"],表示只接收 GET 请求
@app.route("/index", methods=["GET"])
async def index():
return "Hello World"
# 在 Windows 中必须加上 if __name__ == "__main__"
# 否则会抛出 RuntimeError: This event loop is already running
if __name__ == "__main__":
# 启动服务,因为我们这个文件叫做 main.py
# 所以需要启动 main.py 里面的 app
# 第一个参数 "main:app" 就表示这个含义
# 然后是 host 和 port 表示监听的 ip 和端口
uvicorn.run("main:app", host="0.0.0.0", port=5555)
blacksheep-cli
pip install blacksheep-cli
blacksheep create
注册路由
from blacksheep import Application
app = Application()
# 方式一
@app.route("/index", methods=["GET"])
async def index():
pass
# 方式二
@app.router.get("/index")
async def index():
pass
# 方式三
async def index():
pass
app.router.add_get("/index", index)
# 方式四
async def index():
pass
app.router.add("GET", "/index", index)
动态路径
@app.router.get("/items/{item_id}")
async def get_item(item_id: int):
# 此时的 item_id 要求一个整型
# 准确的说是一个能够转成整型的字符串
return {"item_id": item_id}
路径包含/
# 声明 file_path 的类型为 path,这样它会被当成一个整体
# 但要注意:在 FastAPI 里面是 {file_path:path},这里刚好相反
@app.router.get("/files/{path:file_path}")
async def get_file(file_path: str):
return {"file_path": file_path}
自定义匹配器
# 路由本质上就是一个 Route 对象
# 该对象有一个类属性 value_patterns,是一个字典
# 将自定义匹配器注册进去,然后就可以在路径参数里面使用了
Route.value_patterns["number_format"] = r"185\d{8}"
@app.router.get("/phone/{number_format:phone_number}")
async def get_file(phone_number: str):
return {"phone_number": phone_number}
查询参数
from blacksheep import Application
import uvicorn
app = Application()
@app.router.get("/users/{user_id}")
async def get_user(user_id: str, name: str, age: int):
"""
我们在函数中定义了 user_id、name、age 三个参数
显然 user_id 和 路径参数中的 user_id 对应
然后 name 和 age 会被解释成查询参数,没有默认值就是必传项
这三个参数的顺序没有要求,但一般都是路径参数在前,查询参数在后
"""
return {"user_id": user_id, "name": name, "age": age}
requests.get("http://localhost:5555/users/001?name=neo&age=17")
同一个查询参数出现多次
@app.router.get("/users")
async def get_user(name: List[str]):
return {"name": name}
请求的载体:Request 对象
@app.router.get("/girl/{user_id}")
async def read_info(request: Request, user_id: str):
"""
路径参数(user_id)其实可以不用定义,因为我们定义了 request: Request
那么请求相关的所有信息都会进入到这个 Request 对象中
当然定义了也无所谓,路径参数、查询参数可以和 Request 对象一起使用
"""
# 路径参数(一个字典)
path_params = request.route_values
# 查询参数(一个字典)
query_params = request.query
获取请求头
@app.router.get("/get_headers")
async def get_headers(request: Request):
# 返回一个 Headers 对象,该对象是 Cython 实现的
headers = request.headers
print(headers.keys())
"""
(b'host', b'user-agent', b'accept-encoding', b'accept', b'connection')
"""
print(headers.values)
"""
[(b'host', b'localhost:5555'), (b'user-agent', b'python-requests/2.28.1'),
(b'accept-encoding', b'gzip, deflate'), (b'accept', b'*/*'),
(b'connection', b'keep-alive')]
"""
print(b"host" in headers)
"""
True
"""
# 获取的时候,key 一律小写,并且是字节串形式,并且返回的 value 也是字节串
# 注意:headers.get_first 在 key 不存在的时候,会返回 None
# 所以应该确保 key 存在之后,再进行 decode
return {"User-Agent": headers.get_first(b"user-agent").decode("utf-8")}
# 获取cookie
cookies = request.cookies
async def get_extra_message(request: Request):
return {
"请求的主机": request.host,
"请求方法": request.method,
# request.url 返回一个 URL 对象,里面有如下属性
# schema、host、port、path、query、fragment
# 分别对应一个 URL 的不同部分
"请求的 URL": str(request.url),
"请求的 URL 的路径": request.path,
"客户端 IP": request.client_ip,
}
响应的载体:Response 对象
cdef class Response(Message):
def __init__(self, int status, list headers = None,
Content content = None)
# 该对象接收三个属性,分别是状态码、响应头和 Content 对象,而 Content 是一个普通的静态类
cdef class Content:
cdef readonly bytes type
cdef readonly bytes body
cdef readonly int length
#该类包含三个属性,分别是响应类型、响应体、响应体的长度,但实际上第三个属性不需要传。
from blacksheep import (
Application, Request,
Response, Content,
Cookie
)
import uvicorn
import orjson
app = Application()
@app.router.get("/get_info")
async def get_info(request: Request):
data = {"name": "古明地觉", "age": 17}
content = orjson.dumps(data)
response = Response(
200,
[(b"ping", b"pong"), (b"token", b"0315abcde")],
Content(b"application/json", content)
)
# 响应头也可以单独添加
response.add_header(b"x-man", b"GOOD")
"""
# 也可以移除某个响应头
response.remove_header(b"name")
"""
# 设置 cookie
response.set_cookie(Cookie("session_id", "abcdefg"))
# 也可以调用 set_cookies 同时设置多个 cookie,传一个列表即可
"""
# 也可以移除某个 cookie
response.remove_cookie("name")
"""
return response
获取请求体
@app.router.post("/get_info")
async def get_info(request: Request):
data = await request.read()
print(data)
# 所以 await request.read() 得到的就是最原始的字节流,除了它之外还有 await request.json(),它在内部依旧会获取字节流,只不过获取之后会自动 loads 成字典
Form表单
# 调用 requests.post 发请求,如果参数通过 data 传递的话,则相当于提交了一个 form 表单
await request.form()
文件上传
# 返回一个列表,列表里面是 FormPart 对象
files = await request.files()
for file in files:
result.append(
{"name": file.name.decode("utf-8"),
"file_name": file.file_name.decode("utf-8"),
"content_type": file.content_type,
)
requests.post(url, files=[('txt文件', open('...')), ...])
分块读取请求体
await request.read():读取原始的字节流await request.form():将字节流按照表单的方式进行解析,返回一个字典await request.json():将字节流按照 JSON 的方式进行解析,返回一个字典
@app.router.post("/stream")
async def stream(request: Request):
buf = BytesIO()
async for chunk in request.stream():
buf.write(chunk)
return buf.getvalue().decode("utf-8")
实现的效果和 await request.read() 是一样的,但当请求体很庞大时,该方式不会阻塞事件循环。
返回静态资源
# 浏览器输入:localhost:5555/static/images/1.png
# 会返回指定目录下的 1.png 文件
app.serve_files("/Users/satori/Downloads/images",
root_path="static/images",
discovery=True)
# 浏览器输入:localhost:5555/static/videos/1.mp4
# 会返回指定目录下的 1.mp4 文件
app.serve_files("/Users/satori/Downloads/videos",
root_path="static/videos",
discovery=True)
discover 参数,默认为 False。如果指定为 True,那么通过 /static/images 则可以查看对应目录的文件列表,
不同种类的响应
from blacksheep import text, html, json, pretty_json
# 返回纯文本
text("...")
# 返回 HTML
html("<h1>...</h1>")
# 返回 JSON
json({"k": "v"})
# 返回美化格式的 JSON
pretty_json({"k": "v"})
# 在 blacksheep 里面内置了非常多的函数,帮助我们构造不同种类的 Response
# 文件名 blacksheep/server/responses.py
# 里面的函数直接通过 from blcaksheep import 进行导入即可
def status_code(status: int = 200, message: Any = None) -> Response:
if not message:
return Response(status)
return Response(status, content=_optional_content(message))
返回字节流
async def content():
for i in range(1, 5):
yield f"{i} chunk bytes ".encode("utf-8")
@app.router.get("/get_content")
async def get_content(request: Request):
data = b"".join([chunk async for chunk in content()])
return Response(
200, None,
Content(b"application/octet-stream", data)
)
返回文件
# 我们可以返回图片、音频、视频,以字节流的形式
# 但光有字节流还不够,我们还要告诉 Chrome
# 拿到这坨字节流之后,应该要如何解析
# 此时需要通过响应头里面的 Content-Type 指定
Response(
200, None,
# png 图片:"image/png"
# mp3 音频:"audio/mp3"
# mp4 视频:"video/mp4"
Content(b"image/png", b"*** bytes data ***")
)
直接下载
# 在响应头中指定 Content-Disposition
# 意思就是告诉 Chrome,你不要解析了,直接下载下来
# filename 后面跟的就是文件下载之后的文件名
return Response(
# 既然都下载下来了,也就不需要 Chrome 解析了
200, [(b"Content-Disposition", b"attachment; filename=main.py")],
# 将响应类型指定为 application/octet-stream
# 表示让 Chrome 以二进制格式直接下载
Content(b"application/octet-stream", data)
)
中间件
中间件在 web 开发中可以说是非常常见了,说白了中间件就是一个函数或者一个类。
在请求进入视图函数之前,会先经过中间件(被称为请求中间件),在里面我们可以对请求进行一些预处理,或者实现一个拦截器等等;同理当视图函数返回响应之后,也会经过中间件(被称为响应中间件),在里面我们也可以对响应进行一些润色。
blacksheep 也支持像 Flask 一样自定义中间件,在 Flask 里面有请求中间件和响应中间件,但在 blacksheep 里面这两者合二为一了,
async def middleware(request: Request, handler):
# 请求到来时会先经过这里的中间件
if request.headers.get("ping", "") != "pong":
result = orjson.dumps({"error": "请求头中缺少指定字段"})
response = Response(
200, None,
Content(b"application/json", result)
)
# 当请求头中缺少 "ping": "pong"
# 在中间件这一步就直接返回了,就不会再往下走了
# 所以此时相当于实现了一个拦截器
return response
# 如果条件满足,则执行await handler(request),关键是这里的 handler
# 如果该中间件后面还有中间件,那么 handler 就是下一个中间件
# 如果没有,那么 handler 就是对应的视图函数
# 这里显然是视图函数,因此执行之后会拿到视图函数返回的 Response 对象
response: Response = await handler(request)
# 我们对 response 做一些润色,比如设置一个响应头
# 所以我们看到在 blacksheep 中,请求中间件和响应中间件合在一起了
response.set_header(b"status", b"success")
return response
# 将中间件添加进去
app.middlewares.append(middleware)
作用与单个请求
from blacksheep.server.normalization import ensure_response
def add_response_header(headers: Dict[str, str]):
def decorator(handler):
@wraps(handler)
async def wrapped(*args, **kwargs):
# 注意:此处拿到的就是视图函数的返回值
# 如果视图函数返回的不是 Response 对象,那么这里不会自动包装
# 因此调用 ensure_response,将其转成 Response 对象
response = ensure_response(await handler(*args, **kwargs))
for key, val in headers.items():
response.set_header(key.encode("utf-8"), val.encode("utf-8"))
return response
return wrapped
return decorator
@app.router.get("/")
@add_response_header({"Token": "abcdefg"})
# 这里的 view_func 就是 add_response_header 里面的 wrapped
async def view_func(request: Request):
通过装饰器的方式实现了响应头的添加,虽然它不是中间件,但在最小范围内实现了中间件的效果。
websocket
注意:使用 WebSocket 功能之前需要安装相关的库,直接 pip install websockets。
from blacksheep import Application, WebSocket
@app.router.ws("/ws/{client_id}")
async def ws(websocket: WebSocket, client_id: str):
# accept 方法接收两个参数:headers 和 subprotocol
# headers 表示响应头,subprotocol 表示应用程序接受的子协议
# 它们将和握手响应一起发送给客户端
await websocket.accept() # 这里不需要指定
# 或者也可以这么注册
"""
app.router.add_ws("/ws/{client_id}", ws)
# add_ws 方法本质上也是调用了 add 方法
app.router.add("GET_WS", "/ws/{client_id}", ws)
"""
一旦连接建立成功,那么就可以和客户端收发消息了,方法有以下几种:
receive_bytes 和 send_bytesreceive_text 和 send_textreceive_json 和 send_json
await websocket.accept()
await websocket.send_text(f"客户端{client_id} 来连接啦")
while True:
msg = await websocket.receive_text()
await websocket.send_text(f"收到客户端发送的消息: {msg}")
认证(Authentication)
使用Session验证
解决多台服务器session共享
-
session复制
-
session粘黏
Nginx 的 sticky 模块可以支持这种方式,支持按 ip 或 cookie 粘连等等,比如按 ip 粘连
-
session共享
redis
使用Token认证
这个 Token 必须要在每次请求时都传递给服务端,它应该保存在请求头里。 如果保存在 Cookie 里面,服务端还要支持 CORS(跨来源资源共享)策略
而基于 Token 认证一般采用 JWT,它由三段信息构成,将这三段信息用 . 连接起来就构成了 JWT 字符串。就像如下这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称之为头部(Header)、第二部分我们称之为载荷(Payload)、第三部分我们称之为签名(Signature),下面来分别介绍
总结:Token 解决什么问题(为什么要用 Token)?
- 1)完全由应用管理,可以避开同源策略;
- 2)支持跨域访问,Cookie 不支持。Cookie 跨站是不能共享的,这样的话如果你要实现多应用(多系统)的单点登录(SSO),使用 Cookie 来做的话就很困难了。但如果用 Token 来实现 SSO 会非常简单,只要在 header 中的 Authorize 字段(或其他自定义)加上 Token 即可完成所有跨域站点的认证;
- 3)Token 是无状态的,可以在多个服务器间共享,因为服务端不需要保存 Token,只需要对它进行解析即可;
- 4)Token 可以避免 CSRF 攻击(跨站请求攻击);
- 5)易于扩展,在移动端的原生请求是没有 Cookie 之说的,而 Sessionid 依赖于 Cookie,所以 SessionID 就不能用 Cookie 来传了。如果用 Token 的话,由于它是随着 header 的 Authorization 传过来的,也就不存在此问题,换句话说 Token 天生支持移动平台,可扩展性好;
解决跨域问题
app = Application()
app.use_cors(
# 允许跨域请求的 HTTP 方法列表
# ["*"] 表示支持所有请求
allow_methods=["GET", "POST"],
# 允许跨域请求的 HTTP 请求头列表
# 可以使用 ["*"] 表示允许所有的请求头
# 当然下面几个请求头总是被允许的
# Accept、Accept-Language、Content-Language、Content-Type
allow_headers=["*"],
# 允许跨域的源列表,例如 ["http://localhost:8080"]
# ["*"] 表示允许任何源
allow_origins=["*"],
# 跨域请求是否支持 cookie,默认是 False
# 如果为 True,allow_origins 必须为具体的源,不可以是 ["*"]
allow_credentials=False,
# 可以被浏览器访问的响应头, 一般很少指定
expose_headers=["*"],
# 设定浏览器缓存 CORS 响应的最长时间,单位是秒,一般也很少指定
max_age=1000
)
# 但有些请求访问的资源非常重要,我们需要做一些限制
# 创建一个新的 CORS 规则,名称为 allow_some
app.add_cors_policy(
"allow_some",
# 只允许 POST 请求
allow_methods=["POST"],
# 源必须是 http://127.0.0.1
allow_origins=["http://127.0.0.1"],
)
# 规则可以创建多个,如果 allow_* 相关参数不指定
# 那么表示拒绝所有请求
app.add_cors_policy(
"deny"
)
@app.cors("allow_some")
@app.router.post("/payment")
async def payment():
return "支付相关, 很重要的接口"
JWT
ALGORITHM = 'HS256'
SECRET = 'somepassword'
import jwt
def jwt_b_decode(st):
try:
return_data = jwt.decode(st, SECRET, algorithms=[ALGORITHM])
except Exception as e:
logging.exception(e)
return_data = str(e)
return return_data
挂载 Application 对象
类似flask蓝图
# views/__init__.py
from .products import app as products_app
from .users import app as users_app
# main.py
from views import (
products_app,
users_app
)
app = Application()
app.mount("/products", products_app)
app.mount("/users", users_app)
#如果是在子 app 中就需要注意了,子 app 创建路由时的视图函数只能接收一个 Request 对象,然后返回一个 Response 对象。
# 将该属性设置为 True 即可
# 此时参数和返回值不再受到限制,所有的视图函数的处理逻辑都一样
app.mount_registry.auto_events = True
app.mount("/", child)
依赖注入
class GenerateRandom:
def __init__(self):
self.number = random.randint(1, 100)
@app.router.get("/get_random")
async def index(request: Request,
gn: GenerateRandom):
return {"number": gn.number}
# Application 对象公开了一个 services 属性,可以将类型注册为服务
# 当视图函数的参数签名引用了注册的类型时,该类型的实例会在调用视图函数时自动注入
app.services.add_exact_scoped(GenerateRandom)
# 这样只有在第一次请求的时候实例化,后续就一直使用第一次创建的实例
app.services.add_exact_singleton(GenerateRandom)
# 直接添加一个实例进去, 可以加参数, 也是单例的
app.services.add_instance(GenerateRandom(1, 100))
app.services.add_transient(A) # 每次调用生成一个实例
app.services.add_scoped(B) # 每个请求生成一个实例
app.services.add_singleton(C) # 所有请求使用一个实例
数据库连接池
from sqlalchemy.ext.asyncio import (
create_async_engine, AsyncEngine
)
from sqlalchemy.engine.url import URL
from sqlalchemy import text
# 创建一个数据库引擎,它一般会定义在单独的文件中
def create_database_engine():
engine = create_async_engine(
URL.create("mysql+asyncmy", username="root", password="123456",
host="82.157.146.194", port=3306, database="mysql")
)
return engine
@app.router.get("/get_data")
async def index(request: Request, engine: AsyncEngine):
# sqlalchemy 中的异步引擎的类型是 AsyncEngine
# 将它注册在 services 中,这里就可以直接用了
query = text("SELECT name, age, address FROM girl")
# 引擎维护了一个连接池,通过 async with 取出一个连接
# 执行完毕之后,再放回连接池
async with engine.connect() as conn:
rows = await conn.execute(query)
# 获取返回的字段名
columns = rows.keys()
# 遍历 rows,将字段名和每一行数据拼接成字典,然后返回
results = [dict(zip(columns, row)) for row in rows]
return results
app.services.add_instance(create_database_engine())
像数据库连接这种,它应该在应用程序启动的时候就创建好,所以 blacksheep 提供了两个触发器列表,分别表示在程序启动和关闭时执行的一系列操作。
async def create_database_engine(app: Application):
print("我要创建引擎啦")
engine = create_async_engine(
URL.create("mysql+asyncmy", username="root", password="123456",
host="82.157.146.194", port=3306, database="mysql")
)
# 将引擎注册进去
app.services.add_instance(engine)
async def dispose_database_engine(app: Application):
# 通过 app.services.add_instance 将实例注册进去之后
# 在视图函数中只要指定好类型,那么就会自动取出来,这没有问题
# 但如果不在视图函数中呢?所以 app 提供了一个 service_provider
# 注册在 services 里面的实例,都会保存在 service_provider 中
# 我们直接像访问字典一样获取实例即可,那么问题来了,key 是啥?
# 很简单,我们在注册实例(假设叫 obj)的时候,会执行以下操作
"""
app.service_provider[type(obj)] = obj
app.service_provider[type(obj).__name__] = obj
app.service_provider[type(obj).__name__.lower()] = obj
"""
engine: AsyncEngine
# 所以可以这么获取
engine = app.services.provider[AsyncEngine]
engine = app.services.provider["AsyncEngine"]
engine = app.services.provider["asyncengine"]
# 以上几个 engine 都指向同一个对象
# 最后,如果是驼峰命名法,那么还可以这么获取
engine = app.service_provider["async_engine"]
# 方式比较多,我们以类型作为 key 去获取实例即可
# 关闭引擎,并关闭底层连接池
await engine.dispose()
print("引擎关闭啦")
# app.on_start 和 app.on_event 都是 ApplicationEvent 实例
# 前者保存了程序启动时要执行逻辑,后者保存了程序关闭时要执行的逻辑
app.on_start += create_database_engine
app.on_stop += dispose_database_engine
SQLAlchemy
from blacksheepsqlalchemy import use_sqlalchemy
if settings.db_connection_string:
use_sqlalchemy(app, connection_string="sqlite+aiosqlite:///torino.db")
from sqlalchemy.ext.asyncio import AsyncSession
@post("/user")
async def create_user(req: FromJSON[CreateUser], session: AsyncSession):
data = req.value
async with session:
session.add()
await session.commit()
# 查询
stmt = select(User).where(User.username == 'example')
# 执行查询
result = await session.execute(stmt)
# 获取查询结果
user = result.scalars().first()
交互式文档
docs = OpenAPIHandler(
info=Info(title="Example API", version="0.0.1"),
# 以下几个参数都是默认的,可以自由调整
# ui_path="/docs",
# json_spec_path="/openapi.json",
# yaml_spec_path="/openapi.yaml",
# preferred_format=Format.JSON,
# anonymous_access=True
)
docs.bind_app(app)
Application 对象
# 开启错误详细信息
app = Application(show_error_details=True)
异常处理
class CustomException(Exception):
pass
async def custom_exception_handler(
self, request: Request, exc: CustomException
) -> Response:
errors = orjson.dumps({"error": "出现了 CustomException",
"exc": str(exc)})
return Response(404, None, Content(b"application/json", errors))
# 如果视图函数执行出现 IndexError,那么就去执行 indexerror_handler
# 里面的 self 是 app,request 就是本次请求对应的 Request 对象,exc 就是异常
app.exceptions_handlers[IndexError] = indexerror_handler
# 如果视图函数执行出现 CustomException,那么就去执行 custom_exception_handler
app.exceptions_handlers[CustomException] = custom_exception_handler
# 将异常和 handler 进行绑定还可以使用装饰器的方式
@app.exception_handler(IndexError)
async def indexerror_handler(
self, request: Request, exc: IndexError
) -> Response:
errors = orjson.dumps({"error": "出现了 IndexError",
"exc": str(exc),
"index": request.query["index"][0]})
return Response(404, None, Content(b"application/json", errors))
Application 对象还暴露了三种事件:分别是 on_start、after_start、on_stop

@app.after_start
async def after_start_print_routes(app: Application) -> None:
print(app.router.routes)
uvicorn 部署 blacksheep 服务
import uvicorn
if __name__ == '__main__':
uvicorn.run("main:app", port=5555)
还可以通过命令行的方式启动
uvicorn main:app --port 5555 --reload
如果是通过命令行的方式启动,那么参数要将参数中的
_替换成-,比如 reload_dirs 要改成 --reload-dirs


浙公网安备 33010602011771号