django框架,内置组件多,自身功能强大,是一个大而全的框架,ORM、Admin、中间件、Form、ModelFrom、信号、缓存、csrf等
flask框架,内置组件少,但第三方丰富,可扩展性强,是一个微小型框架,组件有flask-session、flask-SQLAlchemy、wtforms、flask-migrate、flask-script、blinker
相同点:
两个框架都是基于wsgi协议实现的,只是默认使用的wsgi模块不同。 django:wsgiref模块 flask:werkzurg模块
不一样点:他们各自处理请求的方式不一样:
django: 经过将请求封装成Request对象,在依次经过中间件,在视图中经过参数进行传递。
flask:经过上下文管理实现。
Tornado框架:
Tornado是一个轻量级的Web框架,主要功能:异步非阻塞+内置WebSocket
a. wsgi, 建立socket服务端,用于接收用户请求并对请求进行初次封装。
b. 中间件,对全部请求在到来以前,响应以前定制一些操做。
c. 路由匹配,在url和视图函数对应关系中,根据当前请求url找到相应的函数。
d. 执行视图函数,业务处理【经过ORM去数据库中获取数据,再去拿到模板,而后将数据和模板进行渲染】
e. 再通过全部中间件
f. 经过wsgi将响应返回给用户。javascript
一、进行域名解析获取ip, 先去本地域名服务器,若是没有再去根域名服务器
二、链接成功
三、浏览器发送数据
四、服务器接受到数据后处理并响应给浏览器
-服务器在次过程当中处理流程较多,-django,flask等
WSGI是web服务网关接口--->应用程序(web框架)与web服务器之间的一种接口css
实现了wsgi协议的模块本质:编写了socket服务端,用来监听用户请求,若是有请求到来,则将请求进行一次封装,而后交给 web框架来进行下一步处理。html
模块有:-wsgiref -werkzurg -uwsgi前端
uWSGI是一个Web服务器,它实现了WSGI协议、uwsgi、http等协议。Nginx中HttpUwsgiModule的做用是与uWSGI服务器进行交换vue
代码上线时,使用uWSGI:java
一、nginx 作为代理服务器:负责静态资源发送(js、css、图片等)、动态请求转发以及结果的回复;python
二、uWSGI 作为后端服务器:负责接收 nginx 请求转发并处理后发给 Django 应用以及接收 Django 应用返回信息转发给 nginx;mysql
三、Django 应用收到请求后处理数据并渲染相应的返回页面给 uWSGI 服务器。jquery
做用:对全部的请求进行批量处理,能够在视图函数执行先后进行自定义操做linux
应用:
-用户登陆验证 --->若是使用装饰器,就必须给每一个函数都添加,太繁琐
-权限处理 --->用户登陆成功,将该用户全部的权限写入session中,每次访问时判断该用户是否有权限访问当前的url,这样就能够将判断用户是否用权限的操做放入中间中
-内置应用
-session
-csrf --->跨站请求伪造,防止用户直接向服务端发送POST请求。中间件拦截检验是否携带crsf_token
-全局缓存 --->若是设置了缓存,则请求进来,经过中间件后,则直接去缓存中取数据,而后响应,若是此时的缓存中没有数据,则走路由匹配、视图函数,可是在响应时会先将数据放入到缓存当中
-跨域
-cors --->浏览器的同源策略(不一样的域名或不一样的端口)先后端分离时,本地开发测试使用
一、浏览器向服务端发送GET请求,获取csrf_token: form表单中隐藏的input标签+保存到cookie中(经过算法)
二、再次发送POST请求时,须要携带以前发给浏览器的csrf_token,用次crsf_token与cookie中的crsf_token作验证
三、进行验证:在process_view中验证(由于只有在process_view中,才能获得视图函数,判断函数是否须要验证(加装饰器能够避免验证))
porcess_request
porcess_view
porcess_template_response 只有在视图函数的返回值中有render方法时才调用此方法
porcess_excepion 处理异常
porcess_response
方法一: $.ajax({ url:'/index', type:'POST', #携带csrf_token data:{csrfmiddlewaretoken:'{{ csrf_token }}',name:'alex'} }) 方法二: 前提:引入jquery + 引入jquery.cookie $.ajax({ url: 'xx', type:'POST', data:{name:'oldboyedu'}, #添加请求头 headers:{ X-CSRFToken: $.cookie('csrftoken') }, dataType:'json', // arg = JSON.parse('{"k1":123}') success:function(arg){ } }) 方法三:使用ajaxSetup, <body> <input type="button" onclick="Do1();" value="Do it"/> <input type="button" onclick="Do2();" value="Do it"/> <input type="button" onclick="Do3();" value="Do it"/> <script src="/static/jquery-3.3.1.min.js"></script> <script src="/static/jquery.cookie.js"></script> <script> $.ajaxSetup({ beforeSend: function(xhr, settings) { xhr.setRequestHeader("X-CSRFToken", $.cookie('csrftoken')); } }); function Do1(){ $.ajax({ url:"/index/", data:{id:1}, type:'POST', success:function(data){ console.log(data); } }); } function Do2(){ $.ajax({ url:"/index/", data:{id:1}, type:'POST', success:function(data){ console.log(data); } }); } function Do3(){ $.ajax({ url:"/index/", data:{id:1}, type:'POST', success:function(data){ console.log(data); } }); } </script> </body>
data headers ajaxSetup()
一、路由分发include:二级路由
二、路由系统中name的做用:反向解析
url(r'^home', views.home, name='home')
在模板中使用 {% url 'home' %}
在视图中使用 reverse(“home”)
MVC: model view(模块) controller (视图)
MTV: model tempalte view
一、CBV和FBV区别:
本质上是没有什么区别的,由于它们都是经过对函数进行操做的,而CBV是经过.as_views()方法返回view函数,view函数再调用dispatch(), 在dispstch方法中在经过反射执行get、post、put、patch、delete方法的
二、处理csrf认证及CBV添加装饰器
普通装饰器能够加在get、post等方法上
csrf装饰器放在dispatch()上或直接加在类上
局部避免csrf的方式: from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator 针对FBV: @csrf_exempt def foo(request): return HttpResponse("foo") 针对CBV: # 方式1 @method_decorator(csrf_exempt,name="dispatch") class IndexView(View): # 方式2 @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): print("hello world") # 执行父类的dispatch方法 res=super(IndexView,self).dispatch(request, *args, **kwargs) print("hello boy") return res @method_decoretor(装饰器函数) 添加装饰器 def post(self,request,*args,**kwargs): return HttpResponse('OK')
当请求进来时,将请求相关的数据封装到environ中,django项目启动时,执行__call__,将environ赋值给request对象
wsgi: from wsgiref.simple_server import make_server def run_server(environ, start_response): """ 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, run_server) httpd.serve_forever() Django源码: class WSGIHandler(base.BaseHandler): request_class = WSGIRequest def __init__(self, *args, **kwargs): super(WSGIHandler, self).__init__(*args, **kwargs) self.load_middleware() def __call__(self, environ, start_response): # 请求刚进来以后 # set_script_prefix(get_script_name(environ)) signals.request_started.send(sender=self.__class__, environ=environ) request = self.request_class(environ) response = self.get_response(request) response._handler_class = self.__class__ status = '%d %s' % (response.status_code, response.reason_phrase) response_headers = [(str(k), str(v)) for k, v in response.items()] for c in response.cookies.values(): response_headers.append((str('Set-Cookie'), str(c.output(header='')))) start_response(force_str(status), response_headers) if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'): response = environ['wsgi.file_wrapper'](response.file_to_stream) return response
- CBV时添加csrf装饰器,是添加在dispatch上
- 多数据库配置 allow_relation方法 进行连表
一、增、删、改、查
增: models.UserInfo.objects.create() obj = models.UserInfo(name='xx') obj.save() models.UserInfo.objects.bulk_create([models.UserInfo(name='xx'),models.UserInfo(name='xx')]) 删: models.UserInfo.objects.all().delete() 改: models.UserInfo.objects.all().update(age=18) #在原来的基础上添加1000 models.UserInfo.objects.all().update(salary=F('salary')+1000) 查: filter()· 找不到返回[] exclude() 排除 values() 字典 values_list() 元祖 order_by() order_by('-id') anotate() 用于实现聚合group by查询 aggregate() 聚合函数 exsit() 是否有结果 reverse() 反转 distinct() 去重 返回具体的对象 first() laste() get() 找不到报错
二、value余value_list
value 返回一个字典
value_list 返回一元祖 (flat=Ture 此时返回一个列表)
三、F、Q
(1)F 用于比较,数字自增
# 查询评论数大于收藏数的书籍 from django.db.models import F Book.objects.filter(commnetNum__lt=F('keepNum')) #将每一本书的价格提升30元 Book.objects.all().update(price=F("price")+30)
(2)Q 主要是构造复杂的查询条件。查询条件为or(|),and($)
查询做者名是小仙女或小魔女的 models.Book.objects.filter(Q(authors__name="小仙女")|Q(authors__name="小魔女"))
四、性能优化
(1)select related
一、select_related主要针一对一和多对一关系进行优化。
二、select_related使用SQL的JOIN语句进行优化,经过减小SQL查询的次数来进行优化、提升性能。
class Usertype(models.Model): title = models.CharField(max_length=32) class UserInfo(models.Model): name = models.CharField(max_length=32) email = models.CharField(max_length=32) ut = models.ForeignKey(to='UserType') # 1次SQL # select * from userinfo objs = UserInfo.obejcts.all() for item in objs: print(item.name) # n+1次SQL # select * from userinfo objs = UserInfo.obejcts.all() for item in objs: # select * from usertype where id = item.id print(item.name,item.ut.title) # 1次SQL # select * from userinfo inner join usertype on userinfo.ut_id = usertype.id objs = UserInfo.obejcts.all().select_related('ut') for item in objs: print(item.name,item.ut.title)
(2)prftatch related
一、对于多对多字段(ManyToManyField)和一对多字段,可使用prefetch_related()来进行优化
二、prefetch_related()的解决方法是,分别查询每一个表,而后用Python处理他们之间的关系。(若是链表过多,也会影响效率)
(3)only 仅取一条记录中指定的数据(queryset[obj,obj,obj]) models.UserInfo.objects.only('username','id')
(4)defer 排除一条记录指定的数据 (queryset[obj,obj]) models.UserInfo.objects.defer('username','id')
五、执行原生SQL
(1)extra 构造查询条件,如子查询
Entry.objects.extra(select={'new_id': "select id from tb where id > %s"}, select_params=(1,), order_by=['-nid'])
(2)raw 执行sql语句
models.UserInfo.objects.raw('select * from userinfo')
(3)execute
1.执行自定义SQL # from django.db import connection, connections # cursor = connection.cursor() # cursor = connections['default'].cursor() # cursor.execute("""SELECT * from auth_user where id = %s""", [1]) # row = cursor.fetchone()
五、using 指定数据库
使用信号
db first :先建立数据库,再更新表模型
code first:先写表模型,再更新数据库
一、settings中设置链接数据库
二、python manage.py inspectdb > app/models.py
一、模板继承:{% extends 'layouts.html' %}
二、自定义方法
三、xss攻击:|safe mark_safe
做用: -用户请求数据格式验证
-生产HTML标签
区别:-From须要本身写字段
-ModelFrom经过Meta定义
场景:凡是须要进行表单数据验证的 如:登陆验证
重写__init__和使用ModelChoiceField字段类型
from django.forms import Form from django.forms import fields 方法一:从新__init__ class UserForm(Form): name = fields.CharField(label='用户名',max_length=32) email = fields.EmailField(label='邮箱') ut_id = fields.ChoiceField( # choices=[(1,'二笔用户'),(2,'闷骚')] choices=[] ) def __init__(self,*args,**kwargs): super(UserForm,self).__init__(*args,**kwargs) # 每次实例化,从新去数据库获取数据并更新 self.fields['ut_id'].choices = models.UserType.objects.all().values_list('id','title') def user(request): if request.method == "GET": form = UserForm() return render(request,'user.html',{'form':form}) 方法二:使用ModelChoiceField字段 from django.forms import Form from django.forms import fields from django.forms.models import ModelChoiceField class UserForm(Form): name = fields.CharField(label='用户名',max_length=32) email = fields.EmailField(label='邮箱') ut_id = ModelChoiceField(queryset=models.UserType.objects.all())
表关系是OneToOne,ForeignKey时,有on_delete参数,主要为了不两个表里的数据不一致问题(关联表中的对象被删除后,此时表中的对象状况)
Django2.0里model外键和一对一的on_delete参数 在django2.0后,定义外键和一对一关系的时候须要加on_delete选项,此参数为了不两个表里的数据不一致问题,否则会报错: TypeError: __init__() missing 1 required positional argument: 'on_delete' 举例说明: user=models.OneToOneField(User) owner=models.ForeignKey(UserProfile) 须要改为: user=models.OneToOneField(User,on_delete=models.CASCADE) --在老版本这个参数(models.CASCADE)是默认值 owner=models.ForeignKey(UserProfile,on_delete=models.CASCADE) --在老版本这个参数(models.CASCADE)是默认值 参数说明: on_delete有CASCADE、PROTECT、SET_NULL、SET_DEFAULT、SET()五个可选择的值 CASCADE:此值设置,是级联删除。 PROTECT:此值设置,是会报完整性错误。 SET_NULL:此值设置,会把外键设置为null,前提是容许为null。 SET_DEFAULT:此值设置,会把设置为外键的默认值。 SET():此值设置,会调用外面的值,能够是一个函数。
将经常使用且不太频繁修改的数据放入缓存。
之后用户再来访问,先去缓存查看是否存在,若是有就返回
不然,去数据库中获取并返回给用户(再加入到缓存,以便下次访问)
Django中提供了6种缓存方式:
--开发调试(不加缓存)
--内存
--文件
--数据库
--Memcache缓存(python-memcached模块)
--Memcache缓存(pylibmc模块)
安装第三方组件支持redis:
django-redis组件 设置settings文件
设置缓存:
-全站缓存(中间件)
-视图函数缓存
-局部模板缓存
信号:django框架内部为开发者预留下的一些自定制的钩子,只要在某个信号中注册了函数,则django内部执行时会自动触发注册在信号中的函数
Model signals pre_init # django的modal执行其构造方法前,自动触发 post_init # django的modal执行其构造方法后,自动触发 pre_save # django的modal对象保存前,自动触发 post_save # django的modal对象保存后,自动触发 pre_delete # django的modal对象删除前,自动触发 post_delete # django的modal对象删除后,自动触发 m2m_changed # django的modal中使用m2m字段操做第三张表(add,remove,clear)先后,自动触发 class_prepared # 程序启动时,检测已注册的app中modal类,对于每个类,自动触发 Management signals pre_migrate # 执行migrate命令前,自动触发 post_migrate # 执行migrate命令后,自动触发 Request/response signals request_started # 请求到来前,自动触发 request_finished # 请求结束后,自动触发 got_request_exception # 请求异常后,自动触发 Test signals setting_changed # 使用test测试修改配置文件时,自动触发 template_rendered # 使用test测试渲染模板时,自动触发 Database Wrappers connection_created # 建立数据库链接时,自动触发
应用场景:数据库中表中的数据发生变化时,日志记录
内置:
from django.core import serializers #queryset = [obj,obj,obj] ret = models.BookType.objects.all() data = serializers.serialize("json", ret)
json:
- json.dumps(ensure_ascii=False) #中文乱码
- json.dumps( cls=JSONEncoder) #自定义JSONEncoder,序列化特殊数据类型
对表数据进行增删改查 知识点:单例模式
- 为公司定制更适用于本身的组件: stark组件
contenttype是django中的一个app,它能够将django下全部app下的表记录下来
一张表能够动态的和N张表进行FK
应用:课程和专题课与间隔策略进行关联
1、查看访问的速度、数据库的行为、cache命中等信息。
2、尤为在Mysql访问等的分析上大有用处(sql查询速度)
单元测试(Unittest)文本测试(Doctest)
- restful就是一套编写接口的协议,协议规定了如何规范编写接口以及设置返回值、状态码等信息。 - 最显著的特色: restful: 给定一个url,根据method不一样在后端作不一样的处理,好比:post 建立数据、get获取数据、put和patch修改数据、delete删除数据。 使用django中的URL: 就必须给调用者不少url,每一个url表明一个功能,好比:add_user/delte_user/edit_user/ - 其余的: - 版本,来控制让程序有多个版本共存的状况,版本能够放在 url、请求头(accept/自定义)、GET参数 - 状态码,200/300/400/500 - url中尽可能使用名词,restful也能够称为“面向资源编程” - api标示: api.luffycity.com (专用域名) www.luffycity.com/api/(主域名下,api功能简单时)
- 协议 https - 域名 - www.oldboy.com/api 主域名 - api.oldboy.com 子域名 - 版本: - url:www.oldboy.com/api/v1 - 请求头中也能够加 - URL资源,名词 - www.oldboy.com/api/v1/student - 请求方式: - GET/POST/PUT/DELETE/PATCH/OPTIONS/HEADERS/TRACE - 返回值: - www.oldboy.com/api/v1/student/ -> 结果集 - www.oldboy.com/api/v1/student/1/ -> 单个对象 - URL添加条件 - www.oldboy.com/api/v1/student?page=11&size=9 - 状态码: - 200 - 300 - 301 - 302 - 400 - 403 - 404 - 500 - 错误信息 { code:1000, meg:'xxxx' } - hyperlink { id:1 name: ‘xiangl’, type: http://www.xxx.com/api/v1/type/1/ }
- -由于以前公司要写一个先后端分离的项目
- 因此就经过查看有关restful相关的技术类文档和视频,如: 阮一峰的博客学
---rest—framework框架内部帮助咱们提供了不少组件,咱们只须要经过配置就能够完成相应操做,如: - 序列化,能够作用户请求数据校验+queryset对象的序列化称为json - 解析器,获取用户请求数据request.data,会自动根据content-type请求头的不能对数据进行解析 - 分页,将从数据库获取到的数据在页面进行分页显示。 还有其余: - 认证 - 权限 - 访问频率控制
---也可使用django的CBV来实现,只是须要编写大量的代码,下降开发效率。
一、编写:写类并实现authticate()
- 方法中能够定义三种返回值:
-(user,auth),认证成功
- None , 匿名用户
- 异常 ,认证失败
二、流程:
- 请求进来走dispatch方法,在dispatch方法中,执行initial()中执行perform_authentication(request)方法,
- 在认证中执行request.user
- 请求进来走dispatch方法,在dispatch方法中,执行initial()中执行check_throttles(request)方法,
def check_throttles(self, request): """ Check if request should be throttled. Raises an appropriate exception if the request is throttled. """ #遍历throttle对象列表 for throttle in self.get_throttles(): #根据allow_request()的返回值进行下一步操做,返回True的话不执行下面代码,标识不限流,返回False的话执行下面代码,还能够抛出异常 if not throttle.allow_request(request, self): #返回False的话执行 self.throttled(request, throttle.wait())
- 匿名用户,根据用户IP或代理IP做为标识进行记录,为每个用户在redis中建立一个列表
每一个用户再来访问时,须要先去记录中剔除以及过时时间,再根据列表的长度判断是否能够继续访问。 匿名用户IP在防火墙中进行设置
- 注册用户,根据用户名或邮箱进行判断
每一个用户再来访问时,须要先去记录列表中剔除过时时间,再根据列表的长度判断是否能够继续访问。
1分钟:40-60次
rest-frmawork视图中能够直接继承10种类,外加View(object),能够大体分为三大部分
一、 继承 APIView(View) 次类属于rest framework中原始类,内部只是帮助咱们实现了基本功能:认证、权限、频率控制,但凡涉及到数据库、分页等操做都须要手动去完成 二、 继承 GenericViewSet(ViewSetMixin, generics.GenericAPIView)
class GenericAPIView(APIView)
def post(...):
pass
继承此类,路由中的as_view()须要填写对应关系 .as_view({'get':'list','post':'create'}),通常使用此类进行编写,可扩展性强 在内部也帮助咱们提供了一些方便的方法: - get_queryset - get_object - get_serializer 注意:要设置queryset字段,不然会跑出断言的异常。 三、 继承 ModelViewSet()
对数据库和分页等操做不用咱们在编写,只须要继承相关类便可。此类封装了大量的组件,操做起来简单,可是扩展性较差 示例:若是只提供增长功能,只继承一下类便可
- mixins.CreateModelMixin,GenericViewSet class TestView(mixins.CreateModelMixin,GenericViewSet): serializer_class = XXXXXXX
对一个接口经过1次访问以后,再对该接口进行N次相同的访问时,对资源不造影响,那么就认为接口具备幂等性。(主要是第二次访问时是否会形成伤害)
好比:
GET, 第一次获取结果、第二次也是获取结果对资源都不会形成影响,幂等。
POST,第一次新增数据,第二次也会再次新增,非幂等。
PUT, 第一次更新数据,第二次不会再次更新,幂等。
PATCH,第一次更新数据,第二次不会再次更新,非幂等。
DELTE,第一次删除数据,第二次不在再删除,幂等。
条件成立,程序继续执行,不然抛异常
应用场景:rest_framework中继承类时,一、定义queryset 二、渲染器使用JSON
0、基于wsgi协议下werkzurg模块
一、路由 @app.route("/login",method=["GET","POST"])
二、视图 使用FBV
三、session 将签名的session保存到cookie中
四、特殊装饰器(相似于中间件) @before_request @after_request
五、message(闪现) 基于Session(先将数据写入session,在session.pop("xx"))实现的用于保存数据的集合,其特色是:使用一次就删除。
六、模板 使用jinja2
七、Blueprint(蓝图) 一、项目问价目录的划分 二、能够统一划分一类url 三、基于before_request(装饰器)现实一类url的功能
flask:
-flask-session 默认放入cookie,能够放入redis
-flask-migrate 数据化迁移
-flask-script 自定义命令
-blinker 信号
公共: DBUtils 数据库链接池
wtforms 表单验证+生成HTML标签
sqlalchemy 相似于django的orm
自定义:Auth 参考falsk-login
为每个线程开辟一条内存空间,用来存储数据
做用:为每个线程开辟一块空间进行数据存储 from threading import local from threading import Thread import time # 示例化local对象 ret=local() def task(s): global ret ret.value=s time.sleep(2) print(ret.value) # 开启10个线程 for i in range(10): t=Thread(target=task,args=(i,)) t.start()
简单来讲,falsk上下文管理能够分为三个阶段: 1、请求进来时,将请求相关的数据放入上下文管理中 2、视图函数中,去上下文管理中取值,并操做 3、请求响应,保存session,要将上下文管理中的数据清除 详细点来讲: 1、请求刚进来,将request,session封装在RequestContext类中,将app,g封装在AppContext类中,并经过LocalStack将requestcontext和appcontext放入Local类中 2、视图函数中,经过localproxy类中调用偏函数--->localstack--->local取值 3、请求响应应时,先执行save.session()再各自执行pop(),将local中的数据清除
threading local和封装
一、当请求进来时,会将requset和session封装为一个RequestContext对象,经过LocalStack将RequestContext放入到Local对象中,
二、由于请求第一次来session是空值,因此执行open_session,给session(uuid4())赋值,再经过视图函数处理,
三、请求响应时执行save.session,将签名session写入cookie中,再去Local中的将数值pop掉。
将local对象中的数据维护成一个栈【ctx,ctx】(先进后出) { “协程或线程的惟一标识”: { stack:[ctx,ctx,ctx,] } } 为何维护成一个栈? 一、当是web应用时:无论是单线程仍是多线程,栈中只有一个数据 - 服务端单线程: { 111:{stack: [ctx, ]} } - 服务端多线程: { 111:{stack: [ctx, ]} 112:{stack: [ctx, ]} } 二、离线脚本和多app嵌套时:能够在栈中放入多个数据,在任何状况下均可以获取到当前app请求对应的响应 with app01.app_context(): print(current_app) with app02.app_context(): print(current_app) print(current_app)
g 至关于一次请求的全局变量,能够对g进行相应的扩展(将用户的权限赋值给g; 认证,将登录用户的标识赋值给g)
-请求进来时,将g和app封装为一个APPContext类,在经过LocalStack将Appcontext放入Local中,
-视图函数中,经过localproxy-->偏函数-->LocalStack-->local中取值,
-响应时将local中的g数据删除:
RequestContext
AppContext
LocalStack
Local
LocalProxy
#对url进行处理和分发 from flask import Flask from werkzeug.wsgi import DispatcherMiddleware from werkzeug.serving import run_simple app01 = Flask('app01') app02 = Flask('app02') @app01.route('/login') def login(): return 'app01.login' @app02.route('/index') def index(): return 'app02.index' # 访问"login"--->"http://localhost:5000/login" # 访问"index"--->"http://localhost:5000/app02/index" dm = DispatcherMiddleware(app01, { '/app02': app02, }) if __name__ == '__main__': run_simple('localhost', 5000, dm)
orm关系对象映射
sql语句编写较为复杂,开发效率低,可是查询速度快
orm操做方便,可提升开发效率,相对sql语句查询速度低
一、直接建立session,多线程时须要为每一个线程建立session
二、基于scoped_session建立 session = scoped_session(Session) ,多线程时内部会自动为每一个线程建立session (好像是threading.local)
Local中可使用协程中的惟一标识做为栈中的key,粒度更细
信号
request_started = _signals.signal('request-started') # 请求到来前执行 request_finished = _signals.signal('request-finished') # 请求结束后执行 before_render_template = _signals.signal('before-render-template') # 模板渲染前执行 template_rendered = _signals.signal('template-rendered') # 模板渲染后执行 got_request_exception = _signals.signal('got-request-exception') # 请求执行出现异常时执行 request_tearing_down = _signals.signal('request-tearing-down') # 请求执行完毕后自动执行(不管成功与否) appcontext_tearing_down = _signals.signal('appcontext-tearing-down')# 请求上下文执行完毕后自动执行(不管成功与否) appcontext_pushed = _signals.signal('appcontext-pushed') # 请求上下文push时执行 appcontext_popped = _signals.signal('appcontext-popped') # 请求上下文pop时执行 message_flashed = _signals.signal('message-flashed') # 调用flask在其中添加数据时,自动触发
class Stack(object): def __init__(self,size): self.stack=[] self.size=size def isfull(self): """ 判读栈空 :return: """ if len(self.stack)==0: return True else: return False def isempty(self): """ 判断栈满 :return: """ if len(self.stack)==self.size: return False else: return True def top(self,*args): if not self.isempty(): raise Exception("已满") else: self.stack.append(*args) def pop(self): if self.isfull(): raise Exception("已空") else: self.stack.pop() if __name__=="__main__": s=Stack(4) # for i in range(7): # s.top(i) # print(s.stack) for j in range(6): s.pop() print(s.stack)
import heapq class PriorityQueue(object): #实现一个优先级队列,每次pop优先级最高的元素 def __init__(self): self.queue = [] self.index = 0 def push(self,item,priority): # 将priority和index结合使用,在priority相同的时候比较index,pop先进入队列的元素 heapq.heappush(self.queue,(priority,self.index,item)) self.index += 1 def pop(self): return heapq.heappop(self.queue)[-1] if __name__ == '__main__': pqueue = PriorityQueue() pqueue.push('d',2) pqueue.push('f',3) pqueue.push('a',6) pqueue.push('s',4) pqueue.push('cao',9) print(pqueue.queue) print(pqueue.pop()) print(pqueue.queue) print(pqueue.pop()) print(pqueue.pop()) print(pqueue.pop())
异步非阻塞+websocket
异步非阻塞本质:装饰器+Futrue
目标:经过一个线程处理N个并发请求。 使用支持tornado异步非阻塞的单独模块: MySQL Redis SQLALchemy Tornado异步非阻塞本质:
视图函数yield一个futrue对象,
futrue对象默认: self._done = False ,请求未完成 self._result = None ,请求完成后返回值,用于传递给回调函数使用。 tornado就会一直去检测futrue对象的_done是否已经变成True。 若是IO请求执行完毕,自动会调用future的set_result方法: self._result = result self._done = True
如: <link href="{{static_url("commons.css")}}" rel="stylesheet" />
static_url()自动去配置的路径下找commons.css文件
torndb、mysqldb
Tornado-redis
web聊天室,在线投票,处理高并发任务
redis它是将数据放在缓存中的,相比将数据放入到硬盘上,他的访问速度更快,而且能够将缓存中的数据定时更新到硬板中,同时增长了数据的安全性,它支持五大数据类型,字符串、数组、哈希、集合、有序集合
1)、存储方式 Memecache把数据所有存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis有部份存在硬盘上,这样能保证数据的持久性。 2)、数据支持类型 Memcache只支持字符串类型 Redis支持五大数据类型
3)持久化、高可用、分布式
redis支持数据持久化(RDB,AOF)、高可用、分布式
memcached不支持,本身搭建
应用场景:
字符串:rest farmework中的session, open_session.setex() save_session.get()
列表 消息队列、频率、调度器、
哈希 购物车
集合 url去重
有序集合 调度器的有优先级,排行榜
-购物车信息(商品设置超时时间)
-django-session
-rest frmawork 中的访问频率
-基于flask的websocket作的实时投票,使用redis作消息队列
-scrapy框架
-URL去重 set()
-调度器 先进先出、后进先出、优先级
-pipelines 相似于生产者消费者模型
-起始url 将起始url放入缓存中
-商品的热点信息
-计数器 将修改的数据放入redis中再定时更新到数据库中
-排序 有序集合
0-15个库,默认的db0单库
CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": {"max_connections": 100} # "PASSWORD": "密码", } }, } SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # 引擎 SESSION_CACHE_ALIAS = 'default' # 使用的缓存别名(默认内存缓存,也能够是memcache),此处别名依赖缓存的设置
目的:是对redis作高可用,为每个redis实例建立一个备份称为slave,主redis进行写操做,从redis作读操做,而且让主和从之间进行数据同步,
优势:
- 性能提升,从分担了主的压力。保证了数据的安全性
- 高可用,一旦主redis挂了,直接让从代替主。
存在问题:当主挂了以后,须要人为操做将从变成主。
数据同步机制:
主从刚刚链接的时候,进行全量同步;全同步结束后,进行增量同步。固然,若是有须要,slave 在任什么时候候均可以发起全量同步。
redis 策略是,不管如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
一、自动在主从redis之间进行切换
二、检测主从中 主是否挂掉,且超过一半的sentinel检测到主挂掉以后才进行切换将从redis变为主redis。
三、若是主修复好了,再次启动时候,会变成从。
集群方案: - redis cluster 官方提供的集群方案。 - codis,豌豆荚技术团队。 - twemproxy,Twiter技术团队。
-程序 (一致性哈希 hash-ring) redis cluster的原理? - 基于分布式集群来完成。(不一样任务交给不一样的redis处理) - redis将全部能放置数据的地方建立了 16384 个哈希槽。 - 若是设置集群的话,就能够为每一个实例分配哈希槽: - 192.168.1.20【0-5000】 - 192.168.1.21【5001-10000】 - 192.168.1.22【10001-16384】 - 之后想要在redis中写值时, set k1 123 将k1经过crc16的算法,将k1转换成一个数字。而后再将该数字和16384求余,若是获得的余数 3000,那么就将该值写入到 192.168.1.20 实例中。
实现redis分布式集群
一致性哈希:是一种分布式算法,将任务均匀的分布到不一样的服务器上,经常使用于负载均衡,
hash_ring 将key利用crc32------>数字------>数字和服务器数取余-------->放入服务器对应的数值区间
RDB持久化 -每隔一段时间对redis进行一次持久化(基于时间点快照的方式,复用方式进行数据持久化) -效率较高,数据不完整,安全性不高 AOF持久化
-把全部命令保存起来,若是想到从新生成到redis,那么就要把命令从新执行一次。
-效率相对较低,安全性较高
.一、MySQL ⾥里里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中都是热点数据
voltile-lru: 从已设置过时时间的数据集(server.db[i].expires)中挑选最近频率最少数据淘汰 volatile-ttl: 从已设置过时时间的数据集(server.db[i].expires)中挑选将要过时的数据淘汰 volatile-random:从已设置过时时间的数据集(server.db[i].expires)中任意选择数据淘汰 allkeys-lru: 从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰 allkeys-random: 从数据集(server.db[i].dict)中任意选择数据淘汰 no-enviction(驱逐):禁止驱逐数据
单太redis时,可使用watch
对多台redis进行操做时,加锁并设置超时时间,保证在此期间只有你一我的对redis操做,
----流程
- 写值{key:"sfdfff"}加锁并设置超时时间 - 超过一半的redis实例设置成功,就表示加锁完成。
-解锁 执行lua脚本,用key检测每一个redis中是否有次key,有则删除
- 使用:安装redlock-py from redlock import Redlock Redlock算法 dlm = Redlock( [ {"host": "localhost", "port": 6379, "db": 0}, {"host": "localhost", "port": 6379, "db": 0}, {"host": "localhost", "port": 6379, "db": 0}, ] ) # 加锁,acquire my_lock = dlm.lock("my_resource_name",10000) if my_lock: # J进行操做 # 解锁,release dlm.unlock(my_lock) else: print('获取锁失败')
监听一个数据,下次提交时,若是中间有人对此数据修改,则会报错
- 经过redis的watch实现 import redis conn = redis.Redis(host='127.0.0.1',port=6379) # conn.set('count',1000) val = conn.get('count') print(val) with conn.pipeline(transaction=True) as pipe: # 先监视,本身的值没有被修改过 conn.watch('count') # 事务开始 pipe.multi() old_count = conn.get('count') count = int(old_count) print('如今剩余的商品有:%s',count) input("问媳妇让不让买?") pipe.set('count', count - 1) # 执行,把全部命令一次性推送过去 pipe.execute()
将一部分执行命令进行批量操做,
import redis pool = redis.ConnectionPool(host='10.211.55.4', port=6379) conn = redis.Redis(connection_pool=pool) # pipe = r.pipeline(transaction=False) pipe = conn.pipeline(transaction=True) # 开始事务 pipe.multi() pipe.set('name', 'alex') pipe.set('role', 'sb') pipe.lpush('roless', 'sb') # 提交 pipe.execute()
发布者: import redis conn = redis.Redis(host='127.0.0.1',port=6379) conn.publish('104.9MH', "hahahahahaha") 订阅者: import redis conn = redis.Redis(host='127.0.0.1',port=6379) pub = conn.pubsub() pub.subscribe('104.9MH') while True: msg= pub.parse_response() print(msg)
发布订阅:只要发布者发布任务则全部订阅者都会接受到此任务。
消息队列:队列中放一个任务,则只有一个进程取任务
发布订阅和简单的消息队列区别在于,发布订阅会将消息发送给全部的订阅者,而消息队列中的数据被消费一次便消失。
因此,RabbitMQ实现发布和订阅时,会为每个订阅者建立一个队列,而发布者发布消息时,会将消息放置在全部相关队列中。
在线投票
import heapq class PriorityQueue(object): """实现一个优先级队列,每次pop优先级最高的元素""" def __init__(self): self._queue = [] self._index = 0 def push(self,item,priority): # 将priority和index结合使用,在priority相同的时候比较index,pop先进入队列的元素 heapq.heappush(self._queue,(-priority,self._index,item)) self._index += 1 def pop(self): return heapq.heappop(self._queue)[-1] if __name__ == '__main__': pqueue = PriorityQueue() pqueue.push('d',2) pqueue.push('f',3) pqueue.push('a',6) pqueue.push('s',2) print(pqueue.pop()) print(pqueue.pop()) print(pqueue.pop()) print(pqueue.pop())
def list_scan_iter(name,count=3): start = 0 while True: result = conn.lrange(name, start, start+count-1) start += count if not result: break for item in result: yield item for val in list_scan_iter('num_list'): print(val)
列表、集合、有序集合使用scan_iter
keys(pattern="*") # 根据模型获取redis的name # 更多: # KEYS * 匹配数据库中全部 key 。 # KEYS h?llo 匹配 hello , hallo 和 hxllo 等。 # KEYS h*llo 匹配 hllo 和 heeeeello 等。 # KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo
一、Http协议是基于tcp协议之上的短链接、无状态请求,规定数据之间\r\n分割,请求头与请求体\r\n\r\n分割
二、请求头:Host Content-Type User_Agent method referer Date cookie
三、状态码
成功 200 成功 202 服务器已接受请求,但还没有处理 重定向: 301 永久重定向 302 临时重定向 客户端: 400 客户端请求有语法错误,不能被服务器所理解 401 请求未经受权,这个状态代码必须和WWW-Authenticate报头域一块儿使用 403 服务器收到请求,可是拒绝提供服务 404 请求资源不存在,eg:输入了错误的URL 服务器: 500 服务器发生不可预期的错误 503 服务器当前不能处理客户端的请求,一段时间后可能恢复正常
四、请求方式
1 GET 请求指定的页面信息,并返回实体主体。 2 HEAD 相似于get请求,只不过返回的响应中没有具体的内容,用于获取报头 3 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会致使新的资源的创建和/或已有资源的修改。 4 PUT 从客户端向服务器传送的数据取代指定的文档的内容。 5 DELETE 请求服务器删除指定的页面。 6 CONNECT HTTP/1.1协议中预留给可以将链接改成管道方式的代理服务器。 7 OPTIONS 容许客户端查看服务器的性能。 8 TRACE 回显服务器收到的请求,主要用于测试或诊断。
Http: 80端
https: 443端口
- 自定义证书
- 服务端:建立一对证书
- 客户端:必须携带证书
- 购买证书
- 服务端: 建立一对证书,-将公钥交给机构
- 客户端: 去机构获取证书,数据加密后发给我们的服务单
一、websocket协议是基于http的协议之上的,只是客户端与服务端创建链接以后将再也不断开,实现服务端向客户端主动发送请求
-进行数据通讯前先进行校验(握手)
-发送的数据都是加密的
二、websocket本质
1、建立一个创建链接以后不断开的socket 2、建立链接(握手) -客户端向服务端发送请求 -服务端获取请求头中的Sec-WebSocket-key的值,将此值+magic_string(魔法字符串)进行hashlib和base64加密 -构造响应头,里面包含Sec-WebSocket-Accept: 加密后的值 -返送给客户端 -客户端再拿到加密的数据,解密进行验证 3、链接创建成功后:创建双工通道(同一时间,便可发送数据也可接受数据),进行数据通讯 -发送的数据都是加密的,解密后,根据payload_len的值获取内容(payload_len的值至关于报头) -payload_len <=125 -payload_len ==126 -payload_len ==127 -将获取的内容分为 -mask_key -数据 根据mask_key和数据进行位运算,最后解析出数据
三、websocket的应用----但是实现客户端实时监听服务端的数据变化的操做
如:实时消息推送
- 轮询 优势:代码简单; 缺点:请求次数多,服务器压力大,消息延迟。 - 长轮询 优势:实时接收数据,兼容性好; 缺点:请求次数相对轮询减小。 - websocket 优势:代码简单,再也不反复建立链接。 缺点:兼容性差,不支持IE。
相同点:
一、websocket和http都是基于TCP协议的
二、都是属于应用层的
不一样点:
联系:
一、WebSocket在创建握手时,数据是经过HTTP传输的。可是创建以后,在真正传输时候是不须要HTTP协议的。
- django: channel
- flask: gevent-websocket
- tornado: 内置
一、轮询
经过定时器让客户端每隔几秒就向服务端发送一次请求
二、长轮询
客户端向服务端发送一次请求,浏览器会将次请求夯住一段时间,若是有数据返回则当即响应,若是在此时间内没有数据返回则断开链接,客户端再次发送请求
ps:利用queue和redis实现夯住请求
三、轮询的目的
由于http请求是短链接无状态的,服务端没法实时向客户端发送请求
因此客户端能够利用轮询和长轮询向服务端实时发送请求
一个网站能够兼容多个终端(根据分辨率不一样自动匹配)
@media (min-width: 768px){ .pg-header{ background-color: green; } } @media (min-width: 992px){ .pg-header{ background-color: pink; } }
Jquery
BootStrap
vue.js Angular.js React.js
ajax:异步请求+局部刷新
jquery:$.ajax()
xml : var xmlHttp = new XMLHttpRequest()
vuex维护了一个“全局变量" 基于vue_cookies能够作用户登陆、注销
使用Vuex只需执行 Vue.use(Vuex),并在Vue的配置中传入一个store对象的示例(vue.store)
能够作权限
相似于ajax
v-html 插入html
v-text 在元素中插入值
v-if v-else
v-show
v-for
v-on 监听
v-bind 绑定
v-model 把input的值和变量绑定了,实现了数据和视图的双向绑定
自定义一个script标签,调用回调函数, 后端返回一个函数()
jsop只能get请求
添加响应头
https://www.cnblogs.com/caochao-/articles/8823080.html
merge:会将不一样分支的提交合并成一个新的节点,以前的提交分开显示,注重历史信息、能够看出每一个分支信息,基于时间点 , 遇到冲突,手动解决,再次提交
rebase:将两个分支的提交结果融合成线性,不会产生新的节点,注重开发过程, 遇到冲突,手动解决,继续操做
1、大家公司的代码review分支怎么作?谁来作? 答:组长建立review分支,咱们小功能开发完以后,合并到review分支 交给老大(小组长)来看, 你组长不开发代码吗? 他开发代码,可是它只开发核心的东西,任务比较少。 或者抽出时间,咱们一块儿作这个事情 2、大家公司协同开发是怎么协同开发的? 每一个人都有本身的分支,阶段性代码完成以后,合并到review,而后交给老大看
在命令行中,使用“git tag –a tagname –m “comment”能够快速建立一个标签。须要注意,命令行建立的标签只存在本地Git库中,还须要使用Git push –tags指令发布到TFS服务器的Git库中
gitlab是公司本身搭建的项目代码管理平台
gitlab是公司本身搭建的项目托管平台
设置哪些文件不须要添加到版本管理中 (好比Python的.pyc文件和一些包含密码的配置文件等)
敏捷开发:是一种以人为核心、迭代、按部就班的开发方式。
它并非一门技术,而是一种开发方式,也就是一种软件开发的流程。它会指导咱们用规定的环节去一步一步完成项目的开发。由于它采用的是迭代式开发,因此这种开发方式的主要驱动核心是人
Jenkins 是一个可扩展的持续集成引擎。
主要用于:
nginx+uwsgi+django
为了预防消息丢失,rabbitmq提供了ack,即工做进程在收到消息并处理后,发送ack给rabbitmq,告知rabbitmq这时候能够把该消息从队列中删除了。若是工做进程挂掉 了,rabbitmq没有收到ack,那么会把该消息 从新分发给其余工做进程。不须要设置timeout,即便该任务须要很长时间也能够处理。
ack默认是开启的,工做进程显示指定了no_ack=True
一、建立队列和发送消息时将设置durable=Ture,若是在接收到消息尚未存储时,消息也有可能丢失,就必须配置publisher confirm
channel.queue_declare(queue='task_queue', durable=True)
二、返回一个ack,进程收到消息并处理完任务后,发给rabbitmq一个ack表示任务已经完成,能够删除该任务
三、镜像队列:将queue镜像到cluster中其余的节点之上。在该实现下,若是集群中的一个节点失效了,queue能自动地切换到镜像中的另外一个节点以保证服务的可用性
默认消息队列里的数据是按照顺序被消费者拿走,例如:消费者1 去队列中获取 奇数 序列的任务,消费者2 去队列中获取 偶数 序列的任务。
channel.basic_qos(prefetch_count=1) 表示谁来谁取,再也不按照奇偶数排列(同时也保证了公平的消费分发)
amqp协议中的核心思想就是生产者和消费者隔离,生产者从不直接将消息发送给队列。
生产者一般不知道是否一个消息会被发送到队列中,只是将消息发送到一个交换机。
先由Exchange来接收,而后Exchange按照特定的策略转发到Queue进行存储。
同理,消费者也是如此。Exchange 就相似于一个交换机,转发各个消息分发到相应的队列中。
type=fanout 相似发布者订阅者模式,会为每个订阅者建立一个队列,而发布者发布消息时,会将消息放置在全部相关队列中
type=direct 队列绑定关键字,发送者将数据根据关键字发送到消息exchange,exchange根据 关键字 断定应该将数据发送至指定队列。
type=topic 队列绑定几个模糊的关键字,以后发送者将数据发送到exchange,exchange将传入”路由值“和 ”关键字“进行匹配,匹配成功,则将数据发送到指定队列。
发送者路由值 队列中 old.boy.python old.* -- 不匹配 *表示匹配一个 old.boy.python old.# -- 匹配 #表示匹配0个或多个
celery是python开发的一个分布式任务队列模块,自己本不支持消息传递,依赖于redis、rabbitmq(官方推荐)
一、当用户触发一个操做须要较长时间才能执行完成的任务时,就能够交给Celery异步执行,执行完再返回给用户。这段时间用户不须要等待,提升了网站的总体吞吐量和响应时间。
二、定时任务,好比天天检测一下大家全部客户的资料,若是发现今天 是客户的生日,就给他发个短信祝福
任务模块 Task
包含异步任务和定时任务。其中,异步任务一般在业务逻辑中被触发并发往任务队列,而定时任务由 Celery Beat 进程周期性地将任务发往任务队列。
消息中间件 Broker
Broker,即为任务调度队列,接收任务生产者发来的消息(即任务),将任务存入队列。Celery 自己不提供队列服务,官方推荐使用 RabbitMQ 和 Redis 等。
任务执行单元 Worker
Worker 是执行任务的处理单元,它实时监控消息队列,获取队列中调度的任务,并执行它。
任务结果存储 Backend
Backend 用于存储任务的执行结果,以供查询。同消息中间件同样,存储也可以使用 RabbitMQ, redis 和 MongoDB 等。
一、配置文件,设置beat_schedule
二、程序控制 经过 crontab 的对象
@shared_task为全部的celery对象,建立任务
@app.task为单个celery对象建立任务
获取网站html或xml文本
将html或xml标签进行解析
Selenium 是一个用于Web应用程序测试的工具,他的测试直接运行在浏览器上,模拟真实用户,按照代码作出点击、输入、打开等操做
爬虫中使用他是为了解决requests没法解决javascript动态问题
爬虫spiders中 yield item
raise DropItem()
实现了分布式爬虫,有url去重、调度器、数据持久化
广度优先:先进先出(默认)有序集合
深度优先:先进后出
vitualenv 是一个独立的python虚拟环境
如:当前项目依赖的是一个版本,可是另外一个项目依赖的是另外一个版本,这样就会形成依赖冲突,而virtualenv就是解决这种状况的,virtualenv经过建立一个虚拟化的python运行环境,将咱们所需的依赖安装进去的,不一样项目之间相互不干扰
能够经过对项目目录扫描,自动发现使用了那些类库,而且自动生成依赖清单。
pipreqs ./ 生成requirements.txt
pylint
选择、插入、
没有
公司开发环境: 1. windows - 在windows上开发【坑】 - 代码部署在linux :centos 2. 双系统 - windows - linux: ubuntu+桌面版 - linux: centos+桌面版 - 代码部署在linux :centos 3. mac - linux:mac - 代码部署在linux :centos 4. vim开发 - 经过vim在:centos - 代码部署在linux :centos
PV:访问量, 即页面浏览量或点击量,衡量网站用户访问的网页数量;在必定统计周期内用户每打开或刷新一个页面就记录1次,屡次打开或刷新同一页面则浏览量累计。
UV:独立访客,统计1天内访问某站点的用户数(以cookie为依据);访问网站的一台电脑客户端为一个访客
QPS:每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,做为域名系统服务器的机器的性能常常用每秒查询率来衡量
TPS:是软件测试结果的测量单位。一个事务是指一个客户机向服务器发送请求而后服务器作出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。
Supervisor是一个进程管理工具
用途就是有一个进程须要每时每刻不断的跑,可是这个进程又有可能因为各类缘由有可能中断。当进程中断的时候我但愿能自动从新启动它,此时,用到Supervisor的fork/exec的方式
将传输的数据加密并压缩,客户端提供了口令验证和密钥验证
stackoverflow
git
知乎
思否
bing
python之禅
码农翻身
django官方文档
rabbiitMQ官方文档
django rest framework 官方文档
opentack
docker
人工智能API,实现小功能。智能语音
区块链