python3_网络编程

    • 网络编程
      • socket标准库
      • TCP编程
      • UDP编程
      • SocketServer
      • zerorpc
      • 异步编程
        • selectors库
        • aiohttp
      • WSGI协议 Web Server Gateway Interface
      • 类Flask框架实现
      • django
        • 管理程序django-admin :
        • 数据库配置
        • 创建应用
        • 路由, url函数
        • 模型Model, Django ORM
        • 迁移Migration
        • Django后台管理
        • 模板 template
          • **DTL语法**Django Template Language
          • 过滤器
        • 处理请求函数views
          • 返回数据
        • 认证
          • cookie和session
          • 无session方案, JWT(Json WEB Token)
          • 密码加密bcrypt
        • 缓存Django-Redis

网络编程

socket标准库

socket标准库是非常底层的接口库,socket是一种通用的网络编程接口
协议族Address Family
* AF_INET: IPV4
* AF_INET6: IPV6
* AF_UNIX: Unix Domain Socket,unix系统主机的socket
socket类型
* SOCK_STREAM: 面向连接的流socket, TCP协议
* SOCK_DGRAM: 无连接的datagram数据报socket, UDP协议


TCP编程

服务器

  1. 创建socket对象, sock= socket.socket(family, type)
  2. 绑定ip地址和端口, sock.bind(laddr: str,port: int)
  3. 开始监听, sock.listen()
  4. 接受accept连接,创建用于传输数据的socket对象
    s, addr(raddr,port) = sock.accept()
    阻塞,直到有连接创建,如果阻塞过程中被另一个线程close(),抛出OSError在非套接字上尝试操作
  5. 接收receive和发送send数据

    • recv(bufsize), 阻塞,直到从socket接收最大bufsize大小的data,返回bytes对象,如果remote end(远端) 正常closed, 函数返回b”, 强制关闭会抛异常; 如果在阻塞过程中,被另一个线程close(),抛出OSError”中止了一个连接”“, bufsize的大小最好是2的幂, 例如4096
    • send(data: bytes), 发送数据
    • close(), 关闭连接,

其他一些方法:

socket.makefile(mode='rw', buffering=None, *,encoding=None, errors=None,newline=None) 返回一个与该socket**相关联**的类文件对象,recv和send方法被read和write方法代替
socket.getpeername() 返回socket remote end地址,元组(rattr, port)
socket.getsockname() 返回socket 自己的地址,(lattr, port)
socket.setblocking(flag) flag为0,将socket设置为非阻塞模式,recv()不阻塞,没有数据就抛异常

客户端

  1. 创建socket,
  2. 创建连接 connect((raddr, port))
  3. 传输数据 ,send(),recv()
  4. 停止发送或接受数据, shutdown(flag), flag等于socket.SHUT_RD | SHUT_WR | SHUT_RDWR,关闭接收数据,关闭发送数据,全部关闭.关闭发送数据(SHUT_WR)后,对端每次recv立刻返回一个空bytes,
  5. close(),关闭连接,只是释放和连接相关的资源,要明确关闭连接,请先使用shutdown后再close
    close() releases the resource associated with a connection but does not necessarily close the connection immediately. If you want to close the connection in a timely fashion, call shutdown() before close().

UDP编程

服务端

  1. 创建socket, type=socket.SOCK_DGRAM
  2. bind((hostaddr, port)),绑定IP端口
  3. 接收数据recvfrom(),返回data和对端地址;
    发送数据sendto(bytes, (raddr, port))
    可以使用connect((raddr, port)),添加远端地址, 表示只接受指定地址的消息
  4. close(),关闭连接

客户端

  1. 新建socket, type=socket.SOCK_DGRAM
  2. 接收数据recvfrom(),返回data和对端地址; 发送数据sendto(bytes, (raddr, port))
    connect((raddr, port)),仅添加远端地址,只接受指定地址的消息
  3. close(),关闭连接

SocketServer

SocketServer简化了网络服务器的编写
4个同步类:TCPServer,UDPServer,UnixStreamServer,UnixDatagramServer
2个Mixin类:ForkingMixIn和ThreadingMixIN,用来支持异步

class ForkingTCPServer(ForkingMixIn, TCPServer): pass
class ForkingUDPServer(ForkingMixIN, UDPServer): pass
class ThreadingUDPServer(ThreadingMixIN, UDPServer): pass
class ThreadingTCPServer(ThreadingMixIN, TCPServer): pass

fork创建多进程, thread是创建多线程
编程接口
socketserver.BaseServer(server_address, RequestHandlerClass)
RequestHandlerClass类必须是BaseRequestHandler类的子类,每一个请求会实例化对象,处理请求,拥有以下属性和方法:

self.request 是和客户端连接的socket对象
self.server 是server自己
self.client_address 是客户端地址
setup() 连接初始化
handle() 处理连接
finish() 连接清理

创建服务器步骤:

  1. 派生BaseRequestHandler子类,覆盖其中的,三个方法
  2. 实例化服务器类,传入服务端地址和请求处理类
  3. 启动服务器处理请求,处理一次handle_request()和永远处理serve_forever()
  4. 关闭服务器器,server_close()

server的方法和属性

address_family
socket_type
shutdown_request(self,request) shutdown并close一个独立的请求
close_request(self,request) close一个独立的请求
get_request(self) -> (request, client_addr) 获得一个独立的请求,会阻塞, 相对于accept
fileno(self) -> server.socket 的文件描述符, selector使用


zerorpc

是一个非常轻巧的, 跨语言的通信模块, 官网


异步编程

同步和异步
函数和方法被调用时,调用者是否直接得到最终的结果
直接得到就是同步调用,不直接得到就是异步调用,
阻塞和非阻塞
函数和方法被调用时,是否立即返回,
立即返回就是非阻塞,不立即返回就是阻塞
同步异步和阻塞非阻塞不相干

读取(read)IO两个阶段:
1. 数据准备阶段,内核从输入设备读取数据
2. 内核空间数据复制到用户进程缓冲区阶段

IO模型
同步IO模型包括: 阻塞IO, 非阻塞IO

  1. 同步阻塞IO,进程阻塞,直到拿到数据
  2. 同步非阻塞IO,进程在数据准备阶段不阻塞,但时不时会询问内核数据是否准备好,第二阶段还是会阻塞
  3. IO多路复用,同步非阻塞IO的增强,不用进程自己询问,而是实现一个应用程序接口,作为系统调用,同时监控多个IO请求(通过检查文件描述符状态),进程阻塞在接口处,一旦有一个数据可读,就通知进程来读取数据,即进入第二阶段,同样要阻塞.
    • select 数组,线性遍历O(n),有连接上限1024(x86)
    • poll 链表,线性遍历O(n),无连接上限
    • epoll 哈希表,时间通知机制,增加回调机制,O(1),无连接上限,fd一次拷贝
      异步IO模型: 进程不阻塞,内核完成数据准备后,直接将数据放入用户空间缓冲区,再通知进程进行后续操作

Python中的IO多路复用
select库,实现了select,poll系统调用,通用性好,操作系统都支持,但性能较差,部分实现了epoll

selectors库

实现了kqueque,epoll,devpoll,poll,select,
selectors.DefaultSelector会选择,当前系统性能最优的实现
编程步骤:

  1. 创建selector对象,DefaultSelector
  2. 创建文件对象sock,绑定端口,开启监听,设置非阻塞setblocking(False)
  3. 注册sock文件对象, selector.register(fileobj,event,data=None)返回一个SelectorKey对象,其实是一个namedtuple
    SelectorKey = namedtuple(‘SelectorKey’, [‘fileobj’, ‘fd’, ‘events’, ‘data’])
    fileobj文件对象,
    event事件selector.EVENT_READ(ob1) | selector.EVENT_WRITE(ob10),
    fd 文件描述符,
    data 回调函数或数据,当事件被触发时使用
  4. 开启selector监控循环(while True),
    ready = selector.select() -> 阻塞的,返回一个list,表示准备好的事件列表,每一个元素是一个二元组,(selectorkey,mask),mask表示发生的事件,1表示可读, 2表示可写, 3表示读写都可
    selectorkey存储了注册时的所有信息,fileobj,fd,events,data
  5. 反注册所有已注册fileobj,并关闭fileobj,fileobj存储在selector.get_map()中,最后关闭selector,

selector对象方法和属性
selector.get_map() -> dict, {fd:selectorkey, …}, key是文件描述符


aiohttp

aiohttp则是基于asyncio 模块实现的HTTP框架
可以使用aiohttp实现异步爬虫
例子:

from aiohttp import web
import asyncio
from aiohttp import ClientSession

async def handle(request: web.Request):
    print(request.match_info)
    print(request.query_string)
    return web.Response(text=request.match_info.get('id', '0000'), status=200)

app = web.Application()
app.router.add_get('/{id}', handle)
web.run_app(app, host='0.0.0.0', port=9977)

# client
async def get_html(url: str):
    async with ClientSession() as session:
        async with session.get(url) as res:
            print(res.status)
            text = await res.text()
            with open('bai', 'w', encoding='utf8') as f:
                f.write(text)

url = 'http://www.baidu.com'
loop = asyncio.get_event_loop()
loop.run_until_complete(get_html(url))
loop.close()

另一个例子


WSGI协议 Web Server Gateway Interface

流程:
Browser—(http request)—>WSGI Server—(解包,封装eviron)—>WSGI App处理数据
Browser<—(http response body)—WSGI Server<—(http response body)—WSGI App
Browser<—(http status, response header)—WSGI App使用WSGI Server提供的方法(start_response)

WSGI服务器参考库wsgiref

from wsgiref.simple_server import make_server, demo_app
server = make_server(ip, port, demo_app)
try:
    server.serve_forever()    # 另server.handle_request()
except:
    server.shutdown()
    server.server_close()

def demo_app(environ,start_response):
from io import StringIO
    stdout = StringIO()
    print("Hello world!", file=stdout)
    print(file=stdout)
    h = sorted(environ.items())
    for k,v in h:
        print(k,'=',repr(v), file=stdout)
    start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
    return [stdout.getvalue().encode("utf-8")]
# demo_app 接收两个参数
# environ: dict,保存的是http请求头部信息,key: REQUEST_METHOD, PATH_INFO, QUERY_STRING....
# start_response: 向Browser发送response status和header的方法,接收两个参数
#           start_response(status:str, response_header:二元组, exc_info=None)
# return必须是iterable, return要在start_response调用之后

linux测试命令: curl -I url 获得请求头
curl -X POST -d data -X指定方法,-d传输数据

urllib库解析QUERY_STRING
urllib.parse.parse_qs(environ.get(‘QUERY_STRING’))
webob environ的解析库

request = webob.Request(environ) #将environ解析成request对象
request属性headers/method/path/query_string/GET/POST/params
GET返回url中的数据, POST返回body中提交的数据, params返回所有数据
webob.multidict.MultiDict
多值字典,add(key, value), key不能为int, 相同的key可以共存,
md.getone(k)有且只有一个, md.getall()返回所有

res = webob.Response() # 实例化response对象,参数可以定义响应的status, body等
return res(environ, start_response) # response实例可调用,返回一个可迭代对象

# 使用 webob.dec装饰器实现app,一个request一个reponse
from webob.dec import wsgify
@wsgify
def app(request: webob.Request) -> webob.Response:
    res = webob.Response('body')
    return res         # return 可以是str,bytes,或者Response对象

类Flask框架实现

  1. 路由功能实现:
    将不同的(method, url_pattern, handler)注册到列表lst(有序), request到了后,遍历注册的列表,method匹配,并且url匹配就执行handler(request),返回response_body
    url匹配使用正则表达式,使用命名分组可以表示匹配字段含义,
    三部分:
    • class App, 使用__call__方法作为调用函数,接收request
    • class Router, 路由方法定义,路由信息收集
    • handlers, 定义及注册, 装饰器
  2. 路由分组,实现一级目录,即url前缀
    Route类实例添加前缀信息, 实例分别存放对应的注册信息, Route实例注册到App中, 遍历route实例
  3. 正则表达式的化简
    化简注册时使用的正则表达式为,对应的字段名和取值类型,{name:type},简化注册,同时提高匹配效率,可以将url匹配到的数据,转换成注册时设置的类型,传给handler处理
  4. 模板技术
    将数据填入html模板中作为response, 使用jinja2模块

    # index.html
    
      {% for id, name, age in userlist %}
    • {{loop.index}} {{id}} {{name}} {{age}}
    • {% endfor %} {{usercount}}}
    # template.py from jinja2 import Environment, PackageLoader, FileSystemLoader env = Environment(loader=FileSystemLoader('/web/templates')) # 添执行时主模块的相对路径,或绝对路径 template = env.get_template('index.html') # 搜索env中loader文件夹下的index.html res = template.render(d) # 渲染返回填好数据str, d为dict, key对应模板中的{{key}}
  5. 拦截器
    Preinterceptor / Postinterceptor
    全局interceptor, app中
    局部interceptor, router中
  6. json支持
    response = webob.Response(json=d) d为dict类型
  7. 模块化, 发布
    setup.py sdist

django

Django是采用MVC框架设计的开源WEB快速开发框架
自带ORM, Template, Form, Auth核心组件
Django 1.11版本同时支持python2和3
安装pip install django==1.11

管理程序django-admin :

/LIb/site-packages/django/bin/django-admin
django-admin startproject –help

创建项目:
django-admin startproject projectname .

个文件用途:
manage.py, 命令行工具, 应用创建, 数据库迁移等
blog/settings.py, 项目配置文件, 数据库参数等
blog/urls.py, URL路径映射配置
blog/wsgi, 定义WSGI接口信息, 一般无须改动

数据库配置

setting.py配置数据库连接

DATABASES={
    'ENGINE': 'django.db.backends.mysql'
    'NAME': 'blog',
    'USER': 'peijun',
    'PASSWORD': 'centos',
    'HOST': '192.168.10.129',
    'PORT': '3306'
}

安装数据库驱动, mysqlclient
pip install mysqlclient
windows下使用二进制包whl包

创建应用

python manage.py startapp appname
在setting.py中INSTALLED_APPS添加刚创建的appname应用
文件作用:

admin.py 管理站点模型的声明文件,用于后台管理
models.py 模型层model类定义
views.py 定义URL响应函数
migrations包 数据迁移文件生成目录
apps.py 应用的信息定义文件

路由, url函数

url(regex_pattern, view, kwargs=None, name=None)
url(r'^user/', include('user.urls')) 
**include**动态导入指定app下的urls模块, 二级匹配使用

模型Model, Django ORM

django.db.models column类型:

AutoField
BooleanField
NullBooleanField
CharField
TextField
IntegerField/BigIntegerField
DecimalField 使用python的Decimal实例表示十进制浮点数, max_digits/decimal_places
FloatField python的Float实例表示浮点数
DateField 使用python的datetime.date实例表示的日期, auto_now/auto_now_add
TimeField 使用python的datetime.time实例表示的时间
DateTimeField 使用python的datetime.datetime实例表示的时间

字段选项

db_column 表中字段的名称, 未指定, 使用属性名
primary_key 设置主键
unique 设置唯一键
default 设施缺省值
null 设置null
blank Django表单验证中, 是否可以不填写, 默认为False
db_index 设置索引

关系类型字段Relationships
ForeignKey 表示many to one多对一, ForeignKey(mode_class, on_delete=models.CASCADE), 推荐外键属性使用小写的类名作为标识符
ManyToManyField 表示多对多
intermediary model,可以添加中间模块, 参数添加through='Membership', 构成多对多Unlike normal many-to-many fields, you can’t use add(), create(), or set() to create relationships:
OneToOneField 表示一对一
不设置主键, 默认添加主键表名_id
一对多时, 一端自动创建_id后缀属性
定义端访问,使用对象.定义属性名(一般使用modles)
相关端访问,使用对象.小写模型类名_set

创建Model类

from django.db import models
class User(models.Model):
    class Meta:     # metadate 设置表名,联合主键等, 不同应用的表名不要相同
        db_table = 'user'
    column1 = models.CharField(max_length=128)

Meta “anything that’s not a field”
default_related_name related_query_name
db_table
ordering []
unique_together
indexs

模型操作:
模型类的objects属性, 是一个默认的manager, 用于和数据库交互, 也可以手动指定管理器
定义模型实例使用user = User()
user.save() INSERT
user.delet() DELETE
User.objects.create(name=’tom’) 创建记录并save()
模型类实例调用save(), delete()的时候,事务自动提交, save()和delete()都可以被覆盖, 增强功能
add() authors.add(author)
set() authors.set(authors)
remove()
clear() (https://docs.djangoproject.com/en/2.1/topics/db/queries/#additional-methods-to-handle-related-objects)
复制记录, 添加一条相同的记录, 仅主键不同
blog.pk = None; blog.save() # 多对多, 多对一是需要额外设置相关属性
querySet.update(content=’abc’) # 批量设置, 不会执行save(),
Making Queries

F 使用当前模块的字段值作为比较值(https://docs.djangoproject.com/en/2.1/topics/db/queries/#filters-can-reference-fields-on-the-model)

Field lookup
field__lookuptype=value

QuerySets Caching
limit 切片不会用的cache, 所有的元素都被迭代使用, 才会生成缓存
获取Related objects

QuerySet API reference 相关对象使用

查询集,QuerySet: 1.惰性求值, 只有查询集被使用是才查询数据库 2.拥有缓存,一个查询集可以被一个变量保存
返回查询集的方法叫做过滤器:

User.objects.all() :select * from User:
User.objects.all()[20:40] :limit offset查询集切片:
filter(column=’name’, pk=10) 筛选满足条件的记录
exclude()排除满足条件的记录 :where子句:
order_by(‘table_column’) :
Company.object.order_by(F(‘last_contacted’).desc(nulls_last=True))
null_last表示null排在最后
values() :记录用字典表示(column:value), 放入列表返回
pk总是表示主键
返回单值的方法:
get() 只返回一条记录, 多或者少都会抛异常
count() 返回查询总条数
first() 返回第一条记录
last() 返回最后一条记录
exists() 查询是否有数据, 有则返回True
in_bulk([1, 2], field_name=’pk’)
some_queryset.filter(pk=entry.pk).exists():

字段查询表达式(Field Lookup)

语法: filter(foreignmodes__colname__method=value)
exact: filter(isdeleted__exact=False) 严格等于, 可省略不写
contains: exclude(title__contains=”b”) 等价于 not like ‘%b%’
startswith: filter(title__startwith=”w”)
endswith:
isnull/isnotnull: 是否为None
iexact/icontains/istartwith/iendswith: 忽略大小写
in: filter(pk__in=[1,2,3])
year,month, day, week_day, hour, minute, second: 对日期类型指定具体时间

Q对象

from django.db.models import Q
Q类接收条件, Q对象可以使用&(and), |(or), ~(not)操作
filter函数,如果混用关键字参数和Q对象, Q对象必须位于关键字参数前面,所有参数条件都会被and到一起

迁移Migration

生成迁移文件
python manage.py makemigrations
执行迁移, 在数据库中生成表
python manage.py migrate

Django后台管理

  • 创建管理员
    python manage.py createsuperuser
  • 本地化 在settings.py中设置
    LANGUAGE_CODE = ‘zh-Hans’
    USE_TZ = True
    TIME_ZONE = ‘Asia/Shanghai’
  • 启动WEB Server
    python manage.py runserver
  • 登录后台管理
  • 注册应用模块 admin.py中添加
    admin.site.register(User)

模板 template

新建模板目录template, settings.py配置模板路径

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATES  = [{'DIRS': [os.path.join(BASE_DIR, 'templates')],},]

模板使用分为两步:

  • 加载模板, template = django.template.loader.get_template(‘index.html’)
  • 渲染
    context = django.template.RequestContext(request, {‘content’: ‘something’})
    return django.http.HttpResponse(template.render(context))
    快捷方式渲染
    from django.shortcuts import render
    return render(request, ‘indext.html’, {‘content’: ‘something’})
**DTL语法**Django Template Language
  • 变量: {{variable}}, 使用.点号访问容器内元素或对象内属性和方法, 调用方法不加括号, 变量未定义使用””
    模板标签
  • if/else标签:
    {% if condition %}
    …display
    {% elif condition %}
    …display
    {% else %}
    …display
    {% endif %}
    条件支持and, or, not
  • for标签
    {% for athlete in athlete_list %}
    {{athlete.name}}
    {% endfor %}
    for标签内变量:

  • forloop.counter 从1开始计数 forloop.revcounter 倒计数到1

  • forloop.counter0 从0开始计数 forloop.recounter0 倒计数到0
  • forloop.first 是否是循环的第一次 forloop.last 循环的最后一次
  • forloop.parentloop 嵌套循环时, 内层循环使用外层循环

{% for athlete in athlete_list reversed %} 反向迭代
{% ifequal val1 val2 %}{% ifnotequal val1 val2 %} 比较相等
{% csrf_token %} 跨站请求保护, 防止跨站攻击
CSRF(Cross-site request forgery) 跨站请求伪造, cookie授权, 伪造受信任用户
{# comment statement #} 单行注释
{% comment %} statement {% endcomment %} 多行注释

过滤器

在变量被显示前修改它, 语法{{variable|handler}}|两边没有空格
有的过滤器可以传参, :"para"

{{name|lower}}
{{name|first|upper}}
{{my_list|join:”,”}}
value|divisibleby:”2”|yesno:”True,False,None”, 能否被2整除, yesno可以只有两个参数true_false
value|add:”100”
|addslashes 在反斜杠和单引号或者双引号前面加上反斜杠
|length 容器的长度, str的长度
|default:”” 变量等价False则使用缺省值
|default_if_none:”” 变量为None则使用缺省值
|date:”n j Y” 格式化日期, n月j日Y年

处理请求函数views

第一个参数: request: HttpRequest
url中匹配到的分组也会依次按位置传入
获取request数据:

  • request.GET query字符串, 多值字典对象
  • request.body 请求方法为POST时, 获取数据
  • request.META 请求头中的数据
  • request.POST 表单提交的数据
返回数据

Response, JsonResponse,Django中有许多错误类, 实例可以作为view函数的返回值

from django.http import HttpResponseBadRequest, JsonResponse, HttpResponse
HttpResponse(status=401) # 添加状态码

认证

cookie和session

cookie存储session id, 每一次请求都被附带,
session id有过期的机制, 过期后session和cookie分别在服务端和客户端被清除
session信息会消耗服务器内存, 同时在多服务器部署时, 要考虑session共享的问题redis,memcached
memcached: 数据库查询缓存, 提高网站访问速度
cookie_session的使用

无session方案, JWT(Json WEB Token)

参考初步理解JWT并实践使用
服务器生成一个标识(代表客户端ID), 并对这个标识使用算法签名(防止数据被客户端篡改), 组成JWT数据
下次客户端将JWT数据发回, 服务端就可以确认是否是认证过的用户

pip install pyjwt
jwt_token = jwt.encode({'payload': 'id'}, key, 'HS256') -> bytes

key是编码和解码用的密钥, 尽可能复杂, settings.py 中的SECRET_KEY是一个强密码,导入使用
from django.conf import settings 导入setting模块

设置超时
"exp": int(datetime.datetime.now().timestamp()) + TOKEN_EXPIRE 加入payload, TOKEN_EXPIRE是超时时间

token以b'.'分为三部分,header(jwt,algorithm), payload, signature
前两部分使用base64编码的, 用base64解码后可以得到源信息, 所有jwt不是用来加密的, 仅保证数据不被修改

alg = algorithms.get_default_algorithms()\['HS256']
newkey = alg.prepare_key(SECRET_KEY)
signing_input, \_, _ = token.rpartition(b'.')
sign = alg.sign(signing_input, newkey)
signature = base64.urlsafe_b64encode(sign)

生成token的过程:
前面两部分字典转换成json格式的字符串(注意冒号两边没有空格),再encode成bytes, 再用base64编码,
header和payload两部分由b'.'相连, 再有这个部分和密钥key生成的签名(bytes), 再用base64编码,与前面相连

jwt解码

payload = jwt.decode(jwt_token, key, algorithms=\['HS256']) -> payload_data

RSA是公开的密钥密码体制, 有PK和SK, 非对称算法, 所有人都可以使用PK加密,只有拥有SK,才能对消息解密. 对极大整数做因数分解的难度决定了RSA算法的可靠性.
HMAC数字签名, 对称算法, 一个密钥和一个消息作为输入, 输入一个签名
消息双方都有密钥, 用于验证连接是否合法, (发送随机数, 双方算出结果, 验证)
或者用于确保消息不被篡改, (消息和签名绑定)

密码加密bcrypt

慢算法, 耗时长, 不同密码使用不同盐

bcrypt.gensalt() 生成盐
bcrypt.hashpw(password, salt) 生成加密密钥
password是bytes, 密钥由盐决定, 生成的加密密钥也是bytes, 盐就是前22个字节
bcrypt.checkpw(password, enci_password)


缓存Django-Redis

文档

你可能感兴趣的:(Python,网络,web开发)