刘悦的技术博客

刘悦的技术博客 查看完整档案

北京编辑北京交通大学  |  计算机科学与技术 编辑  |  填写所在公司/组织 v3u.cn 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

刘悦的技术博客 发布了文章 · 11月24日

金瓯无缺江河一统|Win10系统基于Docker和Python3搭建并维护统一认证系统OpenLdap

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_180

OpenLdap(Lightweight Directory Access Protocol)是什么?它其实是一个开源的、具备工业标准特性的应用协议,可以使用TCP协议提供访问控制和维护分布式信息的目录信息。这是一个传统意义上的书面解释,是的,毫无疑问,你会一脸懵逼。好吧,让我们变得感性一点,假如我每天早上使用Twitter想听听懂王又吹了什么牛,登录Twitter账号密码,紧接着又想上Instagram看看女神又post了什么新靓照,好的,登录Instagram账号密码,摸了一上午的鱼之后,突然想起来要登录公司的邮箱,看看有没有新需求,是的,又需要那该死的账号和密码,甚至于查询社保、公积金提取、交罚款都需要各自系统的账号和密码。想象一下,如果有一套系统可以统一管理和维护所有下游应用的账号和权限,我们不需要花时间重复的注册新应用的账号,而只需要关注应用本身,从而实现账号集中认证管理,此时作为账号管理员的我们只须维护OpenLDAP 服务器条目即可,金瓯无缺江山一统,这就是openladp能够带给我们的好处。

LDAP是非常典型的层级结构,信息模型是建立在属性条目(entries)的基础上。一个属性条目是一些属性的集合,并且具有一个全局唯一的"可区分名称"DN,一个条目可以通过DN来引用。每一个条目的属性具有一个类型和一个或者多个值。类型通常是容易记忆的名称,比如"cn"是通用名称(common name) ,或者"mail"是电子邮件地址。条目的值的语法取决于属性类型。比如,cn属性可能具有一个值"jack joe" 。一个mail属性可能包含"admin@v3u.cn" 。一个pngphoto属性可能包含一幅PNG(二进制)格式的图片。

这里简单介绍一下openldap常用的层级关键字的解释:

dc:Domain Component 域名的范围,其格式是将完整的域名分成几部分,如域名为v3u.cn则写成dc=v3u,dc=cn。

uid:User Id 用户ID,比如自增长“1”。

ou:Organization Unit 组织单位,类似于文件系统中的子目录,它是一个容器对象,组织单位可以包含其他各种对象(包括其他组织单元),如“newgroup”。
cn:Common Name 公共名称,如“jack joe”。
sn: Surname 姓,如“joe”。
dn :Distinguished Name 惟一辨别名,类似于文件系统中的绝对路径,每个对象都有一个惟一的名称,类似于mysql的全局唯一索引,如“uid= tom,ou=market,dc=example,dc=com”,记住在一个目录树中DN总是惟一的。

理解了概念,让我们来实操一把,因为实践永远是检验真理的唯一标准,首先安装Docker,参照:win10系统下把玩折腾DockerToolBox以及更换国内镜像源(各种神坑)

随后拉取openldap镜像:

docker pull osixia/openldap:1.3.0

这里我们使用1.3稳定版,拉取成功后查看本地镜像

docker images

可以看到只有200mb左右,非常小巧:

liuyue:~ liuyue$ docker images  
REPOSITORY                  TAG                   IMAGE ID            CREATED             SIZE  
osixia/openldap             1.3.0                 faac9bb59f83        6 months ago        260MB

启动容器:

docker run -p 389:389 --name myopenldap --network bridge --hostname openldap-host --env LDAP_ORGANISATION="v3u" --env LDAP_DOMAIN="v3u.cn" --env LDAP_ADMIN_PASSWORD="admin" --detach osixia/openldap:1.3.0

这里我们通过端口映射将389端口作为链接桥梁,同时配置LDAP组织者:--env LDAP\_ORGANISATION="v3u",配置LDAP域:--env LDAP\_DOMAIN="v3u.cn",配置LDAP密码:--env LDAP\_ADMIN\_PASSWORD="admin",默认登录用户名:admin,并且开启后台守护进程。

查看容器运行状态:

docker ps

可以看到已经在后台启动了:

liuyue:~ liuyue$ docker ps  
CONTAINER ID        IMAGE                   COMMAND                 CREATED             STATUS              PORTS                           NAMES  
b62d1f66c2b8        osixia/openldap:1.3.0   "/container/tool/run"   2 days ago          Up 2 days           0.0.0.0:389->389/tcp, 636/tcp   myopenldap  
liuyue:~ liuyue$

服务确认没问题之后,我们通过python来进行逻辑的编写,首先安装依赖

pip3 install ldap3

随后编写测试脚本 test\_ldap.py ,首先测试一下链接ldap服务器:

from ldap3 import Server, Connection, ALL,MODIFY_REPLACE  
  
s = Server('localhost', get_info=ALL)    
  
c = Connection(s, user='cn=admin,dc=v3u,dc=cn', password='admin')  
c.bind()  
  
print(c.extend.standard.who_am_i())

这里的localhost是docker容器的ip,同时使用账号admin登录,注意账号(cn)以及域(dc)不要写错,不出意外的话,系统会返回当前验证的用户信息:

liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/test_ldap.py"  
dn:cn=admin,dc=v3u,dc=cn  
liuyue:mytornado liuyue$

初始状态下,LDAP是一个空目录,即没有任何数据。可通过程序代码向目录数据库中添加数据,也可使用ldap3库的ldapadd命令来完成添加数据的操作,该命令可将一个LDIF文件中的条目添加到目录:

这里我们来添加一个OU,也就是组织(OrganizationalUnit)。

#添加组织  

res = c.add('OU=v3u_users,dc=v3u,dc=cn', object_class='OrganizationalUnit')  
print(res)  
print(c.result)

可以看到添加成功:

liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/test_ldap.py"  
True  
{'result': 0, 'description': 'success', 'dn': '', 'message': '', 'referrals': None, 'type': 'addResponse'}

随后可以为该组织添加一个群组(group):

# 添加群组  
ldap_attr = {}  
ldap_attr['objectClass'] = ['top', 'posixGroup']  
ldap_attr['gidNumber'] = '1'  
  
c.add('cn=mygroup,dc=v3u,dc=cn',attributes=ldap_attr)  
print(c.result)

返回:

liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/test_ldap.py"  
{'result': 0, 'description': 'success', 'dn': '', 'message': '', 'referrals': None, 'type': 'addResponse'}  
liuyue:mytornado liuyue$

紧接着就是添加人员了:

#添加用户  
ldap_attr = {}  
ldap_attr['cn'] = "test user1"  
ldap_attr['sn'] = "测试"  
ldap_attr['userPassword'] = "1234"  
  
user_dn = "cn=testuser1,cn=mygroup,dc=v3u,dc=cn"  
  
c.add(dn=user_dn,object_class='inetOrgPerson',attributes=ldap_attr)  
print(c.result)

这里的cn可以理解为用户名,sn为姓,userPassword顾名思义就是该用户的密码,dn则是该用户在系统中的唯一标识,注意指定刚刚建立的群组mygroup,返回:

liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/test_ldap.py"  
{'result': 0, 'description': 'success', 'dn': '', 'message': '', 'referrals': None, 'type': 'addResponse'}  
liuyue:mytornado liuyue$

此时,我们可以查询一下刚刚建立好的用户:

print(c.search("dc=v3u,dc=cn", '(&(cn=testuser1))', attributes=['*']))  
print(c.entries)

就可以看到用户的具体信息:

liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/test_ldap.py"  
True  
[DN: cn=testuser1,cn=mygroup,dc=v3u,dc=cn - STATUS: Read - READ TIME: 2020-11-23T17:58:08.569044  
    cn: test user1  
        testuser1  
    objectClass: inetOrgPerson  
    sn: 测试  
    userPassword: b'1234'  
]  
liuyue:mytornado liuyue$

如果我们要修改用户信息,可以使用modify方法:

#修改用户  
c.modify('cn=testuser1,cn=mygroup,dc=v3u,dc=cn',{'uid':[(MODIFY_REPLACE, ['1'])]})  
print(c.result)

这里修改用户的uid属性,返回:

liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/test_ldap.py"  
{'result': 0, 'description': 'success', 'dn': '', 'message': '', 'referrals': None, 'type': 'modifyResponse'}  
liuyue:mytornado liuyue$

再次搜索该用户:

print(c.search("dc=v3u,dc=cn", '(&(cn=testuser1))', attributes=['*']))  
print(c.entries)

可以看到uid已经被添加好了:

liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/test_ldap.py"  
True  
[DN: cn=testuser1,cn=mygroup,dc=v3u,dc=cn - STATUS: Read - READ TIME: 2020-11-23T18:02:47.080555  
    cn: test user1  
        testuser1  
    objectClass: inetOrgPerson  
    sn: 测试  
    uid: 1  
    userPassword: b'1234'  
]

最后,如果员工离职的话,公司内所有账号和权限应该被回收,所以进行删除操作:

#删除用户  
c.delete(dn='cn=testuser1,cn=mygroup,dc=v3u,dc=cn')  
print(c.result)

返回:

{'result': 0, 'description': 'success', 'dn': '', 'message': '', 'referrals': None, 'type': 'delResponse'}

再次查询已经获取不到记录:

print(c.search("dc=v3u,dc=cn", '(&(cn=testuser1))', attributes=['*']))  
print(c.entries)

liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/test_ldap.py"  
False  
[] 

至此,我们就基于openldap的树形结构将组织以及用户信息分别进行存储和CURD(增删改查)操作,在树的root(根)一般定义总域(c=v3u)或者域名后缀(dc=cn),其次往往定义一个或多个组织(organization,o)或组织单元(organization unit,ou)。一个组织单元可以包含人员、设备信息(服务器、电脑等)相关信息。例如uid=testuser1,ou=v3u\_users,dc=v3u,dc=cn,如图所示:

除此以外,OpenLDAP 还是一种典型的分布式结构,提供复制同步,可将主服务器上的数据通过推或拉的机制实现在从服务器上更新,完成数据的同步,从而避免OpenLDAP 服务器出现单点故障,实现了高可用架构。

OpenLdap目录层级结构是一个专门为搜索和浏览而设计的数据库,虽然也支持简单的插入、删除、修改功能。但是我们可以理解为它是为浏览和搜索而生的,它的查询速度很快,相反插入速度较慢,和关系型数据库相比,它并不支持事务和回滚以及复杂的插入、更新等连贯操作功能,这一点和Elasticsearch有几分相似,但是,古人云:“射不主皮,力不同科”,如果您的系统扩容频繁,下游应用层出不穷,那么您就可以考虑用它来做统一用户管理,为您的应用保驾护航。

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_180

查看原文

赞 1 收藏 0 评论 0

刘悦的技术博客 发布了文章 · 11月17日

海纳百川无所不容,Win10环境下使用Docker容器式部署前后端分离项目Django+Vue.js

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_179

随着现代化产品研发的不断推进,我们会发现,几乎每个产品线都会包含功能各异的服务,而且服务与服务之间存在也会存在着错综复杂的依赖和被依赖关系,这就会带来一个世界性难题,项目部署的时候需要运维来手动配制服务之间通信的协议和地址,稍有不慎就会导致服务异常,同时如果服务器因为坏道或者其他原因导致更换物理机,重新部署新环境的成本也会非常之高。因此,我们就会寄希望于Docker这种的容器技术可以让我们构建产品所需要的所有的服务能够迅速快捷的重新部署,并且可以根据需求做横向扩展,且能够保证稳定的容灾性,在出现问题的时候可以利用守护进程自动重启或者启动容灾备份。

本次我们将在Win10环境下利用Docker容器技术来对前后端分离项目Django+Vue.js进行打包,分别定制化对应的项目镜像,应对快速部署以及高扩展的需求。

首先当然是安装Docker,可以参照这篇视频攻略:win10安装配置Docker并更换国内源

随后在宿主机安装gunicorn,容器内我们用异步的方式来启动Django

pip3 isntall gunicorn gevent

Django项目配置settings.py对应的应用:

# Application definition  
  
INSTALLED_APPS = [  
    'django.contrib.admin',  
    'django.contrib.auth',  
    'django.contrib.contenttypes',  
    'django.contrib.sessions',  
    'django.contrib.messages',  
    'django.contrib.staticfiles',  
    'corsheaders',  
    'rest_framework',  
    'myapp',  
    'dwebsocket',  
    'gunicorn'  
]

然后在Django项目的根目录编写gunicorn的配置文件:gunicorn.conf.py

import multiprocessing  
  
bind = "0.0.0.0:8000"   #绑定的ip与端口  
workers = 1                #进程数

这里注意一点,ip必须是0.0.0.0,不要写成127.0.0.1,否则外部环境会访问不到容器内的服务,接下来在项目的根目录编写好依赖列表:requirements.txt

Django==2.0.4  
django-cors-headers==2.5.3  
djangorestframework==3.9.3  
celery==4.4.2  
dwebsocket==0.5.12  
redis==3.3.11  
pymongo==3.8.0  
PyMySQL  
Pillow  
pyjwt  
pycryptodome  
selenium  
qiniu  
gunicorn  
gevent

这里需要注意的是,某些依赖的库最好用==标注出小版本,因为一会在容器内通过pip安装的时候,系统有可能会自动帮你安装最新版导致一些依赖报错。

下面就是老套路,在根目录编写DockerFile文件:

FROM python:3.7  
WORKDIR /Project/mydjango  
  
COPY requirements.txt ./  
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple  
  
COPY . .  
ENV LANG C.UTF-8  
  
CMD ["gunicorn", "mydjango.wsgi:application","-c","./gunicorn.conf.py"]

本次的基础镜像我们选择3.7,毕竟2020年了,与时俱进还是很必要的。

ok,万事俱备,运行命令对项目进行打包:

liuyue@DESKTOP-NVU6CCV MINGW32 ~/www/mydjango (master)  
$ docker build -t 'mydjango' .  
Sending build context to Docker daemon  17.57MB  
Step 1/7 : FROM python:3.7  
 ---> 5b86e11778a2  
Step 2/7 : WORKDIR /Project/mydjango  
 ---> Using cache  
 ---> 72ebab5770a2  
Step 3/7 : COPY requirements.txt ./  
 ---> Using cache  
 ---> b888452d1cad  
Step 4/7 : RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple  
 ---> Using cache  
 ---> a576113cff5a  
Step 5/7 : COPY . .  
 ---> 5c5247d5a743  
Step 6/7 : ENV LANG C.UTF-8  
 ---> Running in af84623622a6  
Removing intermediate container af84623622a6  
 ---> f3d876487dab  
Step 7/7 : CMD ["gunicorn", "mydjango.wsgi:application","-c","./gunicorn.conf.py"]  
 ---> Running in d9392807ae77  
Removing intermediate container d9392807ae77  
 ---> c3ffb74ae263  
Successfully built c3ffb74ae263  
Successfully tagged mydjango:latest  
SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.

这里注意一点就是要进入到项目的目录下执行

docker build -t 'mydjango' .

这里我的项目目录是mydjango。

第一次打包编译的时候,可能时间会长一点,耐心等一会就可以了,如果中途遇到网络错误导致的失败,反复执行打包命令即可,此时运行命令:

docker images

可以看到编译好的镜像大概有1g左右:

liuyue@DESKTOP-NVU6CCV MINGW32 ~  
$ docker images  
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE  
mydjango            latest              c3ffb74ae263        24 hours ago        1.04GB

随后启动镜像服务:

docker run -it --rm -p 5000:8000 mydjango

这里我们用端口映射技术将宿主机的5000端口映射到容器内的8000端口,访问Django服务,http://容器ip:5000

后端搞定,接下来轮到我们的前端服务vue.js了,首先打开vue项目的打包配置文件config/index.js:

build: {  
    // Template for index.html  
    index: path.resolve(__dirname, '../dist/index.html'),  
  
    // Paths  
    assetsRoot: path.resolve(__dirname, '../dist'),  
    assetsSubDirectory: 'static',  
    assetsPublicPath: './',  
  
    /**  
     * Source Maps  
     */  
  
    productionSourceMap: true,  
    // https://webpack.js.org/configuration/devtool/#production  
    devtool: '#source-map',  
  
    // Gzip off by default as many popular static hosts such as  
    // Surge or Netlify already gzip all static assets for you.  
    // Before setting to `true`, make sure to:  
    // npm install --save-dev compression-webpack-plugin  
    productionGzip: false,  
    productionGzipExtensions: ['js', 'css'],  
  
    // Run the build command with an extra argument to  
    // View the bundle analyzer report after build finishes:  
    // `npm run build --report`  
    // Set to `true` or `false` to always turn it on or off  
    bundleAnalyzerReport: process.env.npm_config_report  
  }  
}

将打包目录改成相对路径,同时注意路由的配置,如果曾经修改为history模式记得改回hash:

export default new Router({  
  routes:routes,  
  //mode:'history'   /*hash*/  
})

准备工作完毕,在vue的项目根目录下编写Dockerfile:

FROM node:lts-alpine  
  
# install simple http server for serving static content  
RUN npm install -g http-server  
  
# make the 'app' folder the current working directory  
WORKDIR /app  
  
# copy both 'package.json' and 'package-lock.json' (if available)  
COPY package*.json ./  
  
# install project dependencies  
RUN npm install  
  
# copy project files and folders to the current working directory (i.e. 'app' folder)  
COPY . .  
  
# build app for production with minification  
RUN npm run build  
  
EXPOSE 8080  
CMD [ "http-server", "dist" ]

这里我们选择体积更小的alpine镜像。

随后进入项目的根目录,执行打包命令:

docker build -t myvue .

这里我的前端目录是myvue

liuyue@DESKTOP-NVU6CCV MINGW32 ~/www/myvue (master)  
$ docker build -t myvue .  
Sending build context to Docker daemon  202.1MB  
Step 1/9 : FROM node:lts-alpine  
lts-alpine: Pulling from library/node  
cbdbe7a5bc2a: Pull complete  
4c504479294d: Pull complete  
1e557b93d557: Pull complete  
227291017118: Pull complete  
Digest: sha256:5a940b79d5655cc688cfb319bd4d0f18565bc732ae19fab6106daaa72aeb7a63  
Removing intermediate container 5317abe3649b  
 ---> 2ddb8a0e3225  
Successfully built 2ddb8a0e3225  
Successfully tagged myvue:latest  
SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.

系统会自动根据脚本进行安装依赖,第一次也需要等待一段时间。

打包完成后,执行:

docker images

可以看到前端镜像的体积要小一点:

liuyue@DESKTOP-NVU6CCV MINGW32 ~  
$ docker images  
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE  
myvue               latest              917d1c69f10f        23 hours ago        539MB

运行前端服务:

docker run -it --rm -p 8081:8080 myvue

同样使用端口映射,这次宿主机使用8081,当然了,如果需要可以根据喜好进行修改。

访问Vue.js服务,http://容器ip:8081

至此,通过Docker的容器技术,我们就将前后端两大服务都分别部署好了,过程并不复杂,但是意义却是里程碑式的,携此两大镜像,左牵Django,右擎Vue.js,如果哪天需要横向扩容,只需短短几分钟,我们就可以在新服务器上做到“拎包入住”,灵活方便。最后奉上项目文件,与君共勉:https://gitee.com/QiHanXiBei/... https://gitee.com/QiHanXiBei/myvue

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_179

查看原文

赞 0 收藏 0 评论 0

刘悦的技术博客 发布了文章 · 11月11日

人工智能不过尔尔,基于Python3深度学习库Keras/TensorFlow打造属于自己的聊天机器人(ChatRobot)

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_178

聊天机器人(ChatRobot)的概念我们并不陌生,也许你曾经在百无聊赖之下和Siri打情骂俏过,亦或是闲暇之余与小爱同学谈笑风生,无论如何,我们都得承认,人工智能已经深入了我们的生活。目前市面上提供三方api的机器人不胜枚举:微软小冰、图灵机器人、腾讯闲聊、青云客机器人等等,只要我们想,就随时可以在app端或者web应用上进行接入。但是,这些应用的底层到底如何实现的?在没有网络接入的情况下,我们能不能像美剧《西部世界》(Westworld)里面描绘的那样,机器人只需要存储在本地的“心智球”就可以和人类沟通交流,如果你不仅仅满足于当一个“调包侠”,请跟随我们的旅程,本次我们将首度使用深度学习库Keras/TensorFlow打造属于自己的本地聊天机器人,不依赖任何三方接口与网络。

首先安装相关依赖:

pip3 install Tensorflow  
pip3 install Keras  
pip3 install nltk

然后撰写脚本test\_bot.py导入需要的库:

import nltk  
import ssl  
from nltk.stem.lancaster import LancasterStemmer  
stemmer = LancasterStemmer()  
  
import numpy as np  
from keras.models import Sequential  
from keras.layers import Dense, Activation, Dropout  
from keras.optimizers import SGD  
import pandas as pd  
import pickle  
import random

这里有一个坑,就是自然语言分析库NLTK会报一个错误:



Resource punkt not found

正常情况下,只要加上一行下载器代码即可

import nltk  
nltk.download('punkt')

但是由于学术上网的原因,很难通过python下载器正常下载,所以我们玩一次曲线救国,手动自己下载压缩包:

https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/packages/tokenizers/punkt.zip

解压之后,放在你的用户目录下即可:

C:\Users\liuyue\tokenizers\nltk_data\punkt

ok,言归正传,开发聊天机器人所面对的最主要挑战是对用户输入信息进行分类,以及能够识别人类的正确意图(这个可以用机器学习解决,但是太复杂,我偷懒了,所以用的深度学习Keras)。第二就是怎样保持语境,也就是分析和跟踪上下文,通常情况下,我们不太需要对用户意图进行分类,只需要把用户输入的信息当作聊天机器人问题的答案即可,所这里我们使用Keras深度学习库用于构建分类模型。

聊天机器人的意向和需要学习的模式都定义在一个简单的变量中。不需要动辄上T的语料库。我们知道如果玩机器人的,手里没有语料库,就会被人嘲笑,但是我们的目标只是为某一个特定的语境建立一个特定聊天机器人。所以分类模型作为小词汇量创建,它仅仅将能够识别为训练提供的一小组模式。

说白了就是,所谓的机器学习,就是你重复的教机器做某一件或几件正确的事情,在训练中,你不停的演示怎么做是正确的,然后期望机器在学习中能够举一反三,只不过这次我们不教它很多事情,只一件,用来测试它的反应而已,是不是有点像你在家里训练你的宠物狗?只不过狗子可没法和你聊天。

这里的意向数据变量我就简单举个例子,如果愿意,你可以用语料库对变量进行无限扩充:

intents = {"intents": [  
        {"tag": "打招呼",  
         "patterns": ["你好", "您好", "请问", "有人吗", "师傅","不好意思","美女","帅哥","靓妹","hi"],  
         "responses": ["您好", "又是您啊", "吃了么您内","您有事吗"],  
         "context": [""]  
        },  
        {"tag": "告别",  
         "patterns": ["再见", "拜拜", "88", "回见", "回头见"],  
         "responses": ["再见", "一路顺风", "下次见", "拜拜了您内"],  
         "context": [""]  
        },  
   ]  
}

可以看到,我插入了两个语境标签,打招呼和告别,包括用户输入信息以及机器回应数据。

在开始分类模型训练之前,我们需要先建立词汇。模式经过处理后建立词汇库。每一个词都会有词干产生通用词根,这将有助于能够匹配更多用户输入的组合。

for intent in intents['intents']:  
    for pattern in intent['patterns']:  
        # tokenize each word in the sentence  
        w = nltk.word_tokenize(pattern)  
        # add to our words list  
        words.extend(w)  
        # add to documents in our corpus  
        documents.append((w, intent['tag']))  
        # add to our classes list  
        if intent['tag'] not in classes:  
            classes.append(intent['tag'])  
  
words = [stemmer.stem(w.lower()) for w in words if w not in ignore_words]  
words = sorted(list(set(words)))  
  
classes = sorted(list(set(classes)))  
  
print (len(classes), "语境", classes)  
  
print (len(words), "词数", words)

输出:

2 语境 ['告别', '打招呼']  
14 词数 ['88', '不好意思', '你好', '再见', '回头见', '回见', '帅哥', '师傅', '您好', '拜拜', '有人吗', '美女', '请问', '靓妹']

训练不会根据词汇来分析,因为词汇对于机器来说是没有任何意义的,这也是很多中文分词库所陷入的误区,其实机器并不理解你输入的到底是英文还是中文,我们只需要将单词或者中文转化为包含0/1的数组的词袋。数组长度将等于词汇量大小,当当前模式中的一个单词或词汇位于给定位置时,将设置为1。

# create our training data  
training = []  
# create an empty array for our output  
output_empty = [0] * len(classes)  
# training set, bag of words for each sentence  
for doc in documents:  
    # initialize our bag of words  
    bag = []  
  
    pattern_words = doc[0]  
     
    pattern_words = [stemmer.stem(word.lower()) for word in pattern_words]  
  
    for w in words:  
        bag.append(1) if w in pattern_words else bag.append(0)  
      
   
    output_row = list(output_empty)  
    output_row[classes.index(doc[1])] = 1  
      
    training.append([bag, output_row])  
  
random.shuffle(training)  
training = np.array(training)  
  
train_x = list(training[:,0])  
train_y = list(training[:,1])

我们开始进行数据训练,模型是用Keras建立的,基于三层。由于数据基数小,分类输出将是多类数组,这将有助于识别编码意图。使用softmax激活来产生多类分类输出(结果返回一个0/1的数组:\[1,0,0,...,0\]--这个数组可以识别编码意图)。

model = Sequential()  
model.add(Dense(128, input_shape=(len(train_x[0]),), activation='relu'))  
model.add(Dropout(0.5))  
model.add(Dense(64, activation='relu'))  
model.add(Dropout(0.5))  
model.add(Dense(len(train_y[0]), activation='softmax'))  
  
  
sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)  
model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])  
  
  
model.fit(np.array(train_x), np.array(train_y), epochs=200, batch_size=5, verbose=1)

这块是以200次迭代的方式执行训练,批处理量为5个,因为我的测试数据样本小,所以100次也可以,这不是重点。

开始训练:

14/14 [==============================] - 0s 32ms/step - loss: 0.7305 - acc: 0.5000  
Epoch 2/200  
14/14 [==============================] - 0s 391us/step - loss: 0.7458 - acc: 0.4286  
Epoch 3/200  
14/14 [==============================] - 0s 390us/step - loss: 0.7086 - acc: 0.3571  
Epoch 4/200  
14/14 [==============================] - 0s 395us/step - loss: 0.6941 - acc: 0.6429  
Epoch 5/200  
14/14 [==============================] - 0s 426us/step - loss: 0.6358 - acc: 0.7143  
Epoch 6/200  
14/14 [==============================] - 0s 356us/step - loss: 0.6287 - acc: 0.5714  
Epoch 7/200  
14/14 [==============================] - 0s 366us/step - loss: 0.6457 - acc: 0.6429  
Epoch 8/200  
14/14 [==============================] - 0s 899us/step - loss: 0.6336 - acc: 0.6429  
Epoch 9/200  
14/14 [==============================] - 0s 464us/step - loss: 0.5815 - acc: 0.6429  
Epoch 10/200  
14/14 [==============================] - 0s 408us/step - loss: 0.5895 - acc: 0.6429  
Epoch 11/200  
14/14 [==============================] - 0s 548us/step - loss: 0.6050 - acc: 0.6429  
Epoch 12/200  
14/14 [==============================] - 0s 468us/step - loss: 0.6254 - acc: 0.6429  
Epoch 13/200  
14/14 [==============================] - 0s 388us/step - loss: 0.4990 - acc: 0.7857  
Epoch 14/200  
14/14 [==============================] - 0s 392us/step - loss: 0.5880 - acc: 0.7143  
Epoch 15/200  
14/14 [==============================] - 0s 370us/step - loss: 0.5118 - acc: 0.8571  
Epoch 16/200  
14/14 [==============================] - 0s 457us/step - loss: 0.5579 - acc: 0.7143  
Epoch 17/200  
14/14 [==============================] - 0s 432us/step - loss: 0.4535 - acc: 0.7857  
Epoch 18/200  
14/14 [==============================] - 0s 357us/step - loss: 0.4367 - acc: 0.7857  
Epoch 19/200  
14/14 [==============================] - 0s 384us/step - loss: 0.4751 - acc: 0.7857  
Epoch 20/200  
14/14 [==============================] - 0s 346us/step - loss: 0.4404 - acc: 0.9286  
Epoch 21/200  
14/14 [==============================] - 0s 500us/step - loss: 0.4325 - acc: 0.8571  
Epoch 22/200  
14/14 [==============================] - 0s 400us/step - loss: 0.4104 - acc: 0.9286  
Epoch 23/200  
14/14 [==============================] - 0s 738us/step - loss: 0.4296 - acc: 0.7857  
Epoch 24/200  
14/14 [==============================] - 0s 387us/step - loss: 0.3706 - acc: 0.9286  
Epoch 25/200  
14/14 [==============================] - 0s 430us/step - loss: 0.4213 - acc: 0.8571  
Epoch 26/200  
14/14 [==============================] - 0s 351us/step - loss: 0.2867 - acc: 1.0000  
Epoch 27/200  
14/14 [==============================] - 0s 3ms/step - loss: 0.2903 - acc: 1.0000  
Epoch 28/200  
14/14 [==============================] - 0s 366us/step - loss: 0.3010 - acc: 0.9286  
Epoch 29/200  
14/14 [==============================] - 0s 404us/step - loss: 0.2466 - acc: 0.9286  
Epoch 30/200  
14/14 [==============================] - 0s 428us/step - loss: 0.3035 - acc: 0.7857  
Epoch 31/200  
14/14 [==============================] - 0s 407us/step - loss: 0.2075 - acc: 1.0000  
Epoch 32/200  
14/14 [==============================] - 0s 457us/step - loss: 0.2167 - acc: 0.9286  
Epoch 33/200  
14/14 [==============================] - 0s 613us/step - loss: 0.1266 - acc: 1.0000  
Epoch 34/200  
14/14 [==============================] - 0s 534us/step - loss: 0.2906 - acc: 0.9286  
Epoch 35/200  
14/14 [==============================] - 0s 463us/step - loss: 0.2560 - acc: 0.9286  
Epoch 36/200  
14/14 [==============================] - 0s 500us/step - loss: 0.1686 - acc: 1.0000  
Epoch 37/200  
14/14 [==============================] - 0s 387us/step - loss: 0.0922 - acc: 1.0000  
Epoch 38/200  
14/14 [==============================] - 0s 430us/step - loss: 0.1620 - acc: 1.0000  
Epoch 39/200  
14/14 [==============================] - 0s 371us/step - loss: 0.1104 - acc: 1.0000  
Epoch 40/200  
14/14 [==============================] - 0s 488us/step - loss: 0.1330 - acc: 1.0000  
Epoch 41/200  
14/14 [==============================] - 0s 381us/step - loss: 0.1322 - acc: 1.0000  
Epoch 42/200  
14/14 [==============================] - 0s 462us/step - loss: 0.0575 - acc: 1.0000  
Epoch 43/200  
14/14 [==============================] - 0s 1ms/step - loss: 0.1137 - acc: 1.0000  
Epoch 44/200  
14/14 [==============================] - 0s 450us/step - loss: 0.0245 - acc: 1.0000  
Epoch 45/200  
14/14 [==============================] - 0s 470us/step - loss: 0.1824 - acc: 1.0000  
Epoch 46/200  
14/14 [==============================] - 0s 444us/step - loss: 0.0822 - acc: 1.0000  
Epoch 47/200  
14/14 [==============================] - 0s 436us/step - loss: 0.0939 - acc: 1.0000  
Epoch 48/200  
14/14 [==============================] - 0s 396us/step - loss: 0.0288 - acc: 1.0000  
Epoch 49/200  
14/14 [==============================] - 0s 580us/step - loss: 0.1367 - acc: 0.9286  
Epoch 50/200  
14/14 [==============================] - 0s 351us/step - loss: 0.0363 - acc: 1.0000  
Epoch 51/200  
14/14 [==============================] - 0s 379us/step - loss: 0.0272 - acc: 1.0000  
Epoch 52/200  
14/14 [==============================] - 0s 358us/step - loss: 0.0712 - acc: 1.0000  
Epoch 53/200  
14/14 [==============================] - 0s 4ms/step - loss: 0.0426 - acc: 1.0000  
Epoch 54/200  
14/14 [==============================] - 0s 370us/step - loss: 0.0430 - acc: 1.0000  
Epoch 55/200  
14/14 [==============================] - 0s 368us/step - loss: 0.0292 - acc: 1.0000  
Epoch 56/200  
14/14 [==============================] - 0s 494us/step - loss: 0.0777 - acc: 1.0000  
Epoch 57/200  
14/14 [==============================] - 0s 356us/step - loss: 0.0496 - acc: 1.0000  
Epoch 58/200  
14/14 [==============================] - 0s 427us/step - loss: 0.1485 - acc: 1.0000  
Epoch 59/200  
14/14 [==============================] - 0s 381us/step - loss: 0.1006 - acc: 1.0000  
Epoch 60/200  
14/14 [==============================] - 0s 421us/step - loss: 0.0183 - acc: 1.0000  
Epoch 61/200  
14/14 [==============================] - 0s 344us/step - loss: 0.0788 - acc: 0.9286  
Epoch 62/200  
14/14 [==============================] - 0s 529us/step - loss: 0.0176 - acc: 1.0000

ok,200次之后,现在模型已经训练好了,现在声明一个方法用来进行词袋转换:

def clean_up_sentence(sentence):  
    # tokenize the pattern - split words into array  
    sentence_words = nltk.word_tokenize(sentence)  
    # stem each word - create short form for word  
    sentence_words = [stemmer.stem(word.lower()) for word in sentence_words]  
    return sentence_words

def bow(sentence, words, show_details=True):  
    # tokenize the pattern  
    sentence_words = clean_up_sentence(sentence)  
    # bag of words - matrix of N words, vocabulary matrix  
    bag = [0]*len(words)    
    for s in sentence_words:  
        for i,w in enumerate(words):  
            if w == s:   
                # assign 1 if current word is in the vocabulary position  
                bag[i] = 1  
                if show_details:  
                    print ("found in bag: %s" % w)  
    return(np.array(bag))

测试一下,看看是否可以命中词袋:

p = bow("你好", words)  
print (p)

返回值:

found in bag: 你好  
[0 0 1 0 0 0 0 0 0 0 0 0 0 0]

很明显匹配成功,词已入袋。

在我们打包模型之前,可以使用model.predict函数对用户输入进行分类测试,并根据计算出的概率返回用户意图(可以返回多个意图,根据概率倒序输出):

def classify_local(sentence):  
    ERROR_THRESHOLD = 0.25  
      
    # generate probabilities from the model  
    input_data = pd.DataFrame([bow(sentence, words)], dtype=float, index=['input'])  
    results = model.predict([input_data])[0]  
    # filter out predictions below a threshold, and provide intent index  
    results = [[i,r] for i,r in enumerate(results) if r>ERROR_THRESHOLD]  
    # sort by strength of probability  
    results.sort(key=lambda x: x[1], reverse=True)  
    return_list = []  
    for r in results:  
        return_list.append((classes[r[0]], str(r[1])))  
    # return tuple of intent and probability  
      
    return return_list

测试一下:

print(classify_local('您好'))

返回值:

found in bag: 您好  
[('打招呼', '0.999913')]  
liuyue:mytornado liuyue$

再测:

print(classify_local('88'))

返回值:

found in bag: 88  
[('告别', '0.9995449')]

完美,匹配出打招呼的语境标签,如果愿意,可以多测试几个,完善模型。

测试完成之后,我们可以将训练好的模型打包,这样每次调用之前就不用训练了:

json_file = model.to_json()  
with open('v3ucn.json', "w") as file:  
   file.write(json_file)  
  
model.save_weights('./v3ucn.h5f')

这里模型分为数据文件(json)以及权重文件(h5f),将它们保存好,一会儿会用到。

接下来,我们来搭建一个聊天机器人的API,这里我们使用目前非常火的框架Fastapi,将模型文件放入到项目的目录之后,编写main.py:

import random  
import uvicorn  
from fastapi import FastAPI  
app = FastAPI()  
  
  
def classify_local(sentence):  
    ERROR_THRESHOLD = 0.25  
      
    # generate probabilities from the model  
    input_data = pd.DataFrame([bow(sentence, words)], dtype=float, index=['input'])  
    results = model.predict([input_data])[0]  
    # filter out predictions below a threshold, and provide intent index  
    results = [[i,r] for i,r in enumerate(results) if r>ERROR_THRESHOLD]  
    # sort by strength of probability  
    results.sort(key=lambda x: x[1], reverse=True)  
    return_list = []  
    for r in results:  
        return_list.append((classes[r[0]], str(r[1])))  
    # return tuple of intent and probability  
      
    return return_list  
  
@app.get('/')  
async def root(word: str = None):  
      
    from keras.models import model_from_json  
    # # load json and create model  
    file = open("./v3ucn.json", 'r')  
    model_json = file.read()  
    file.close()  
    model = model_from_json(model_json)  
    model.load_weights("./v3ucn.h5f")  
  
    wordlist = classify_local(word)  
    a = ""  
    for intent in intents['intents']:  
        if intent['tag'] == wordlist[0][0]:  
            a = random.choice(intent['responses'])  
  
  
  
    return {'message':a}  
  
if __name__ == "__main__":  
    uvicorn.run(app, host="127.0.0.1", port=8000)

这里的:

from keras.models import model_from_json  
file = open("./v3ucn.json", 'r')  
model_json = file.read()  
file.close()  
model = model_from_json(model_json)  
model.load_weights("./v3ucn.h5f")

用来导入刚才训练好的模型库,随后启动服务:

uvicorn main:app --reload

效果是这样的:

结语:毫无疑问,科技改变生活,聊天机器人可以让我们没有佳人相伴的情况下,也可以听闻莺啼燕语,相信不久的将来,笑语盈盈、衣香鬓影的“机械姬”亦能伴吾等于清风明月之下。

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_178

查看原文

赞 0 收藏 0 评论 0

刘悦的技术博客 收藏了文章 · 10月21日

工作流表结构的设计

1. 关于工作流

工作流是由不同的人做出的一系列决策,根据定义和可重复的过程,确定其中一个人所做出的特定请求会发生什么。

2. 工作流过程(Process)的配置和用户

在我们可以在此工作流引擎数据库中设计任何其他内容之前,我们首先需要定义确切构成流程的内容,以及我们的用户是谁以及哪些人可以修改流程本身。

Process (流程表) 设计

可能包含的字段:

名称描述类型
ididinteger
name流程标识string

该表虽然非常简单,但却是其余设计的中心参考点; 此数据库中的大多数表(例如,状态(State),转换(Transition),请求(Request)等)都需要直接或间接地与此表相关联。

User (用户) 设计

在不同系统中设计可不一样

3. 工作流状态(State)和转换(Transition)的设计

在一次工作流程中会设计到不同的状态(state), 状态之间的流转需要不同的程序, 我们将之命名为转换(transition)

状态(State)的设计

工作流中的不同状态各有不同的属性类型, 在此总结出几种常见的状态类型枚举:

  • 开始(start):每个进程只应该一个。此状态是创建新请求时所处的状态。
  • 正常(normal):没有特殊名称的常规状态。
  • 完成(complete):表示此状态下的任何请求已正常完成的状态。
  • 拒绝(denied):表示此状态下的任何请求已被拒绝的状态(例如,从未开始且不会被处理)。
  • 已取消(cancelled):表示此状态下的任何请求已被取消的状态(例如,工作已开始但尚未完成)。

一个状态在一次流程中是唯一的,每个状态都有名称,描述和类型。

名称描述类型
ididinteger
process_id所属流程的idinteger
name状态名称string
state_type状态类型string
description状态描述string

转换(Transition)的设计

流程中不同的状态之间存在状态的转换(Transition)
转换在进程下也是唯一的,标记着从一个状态到另一个状态的过程, 因此转换由主键,进程ID,当前状态和下一个状态组成:

名称描述类型
ididinteger
process_id所属流程的idinteger
current_state_id当前状态idinteger
next_state_id下一状态idinteger

4. 工作流操作动作(Action)和后续事件(Activity)的设计

状态(State)之间的流转我们称之为转换(Transition), 那么促成这个转换发生的动作和转换发生后的后续影响事件.我们成为:

  • 动作(Action): 用户在某个状态节点可操作的行为
  • 活动(Activity): 转换到状态节点后产生的事件

动作(Action)的设计

同样的不同的动作也各有不同的属性类型, 在此总结出几种常见的动作类型枚举:

  • 批准(approve):操作人将请求应移至下一个状态。
  • 拒绝(deny):操作人将请求应移至上一个状态。
  • 取消(cancel):操作人将请求应在此过程中移至“已取消”状态。
  • 重新启动(restart):操作人将将请求移回到进程中的“开始”状态。
  • 解决(resolve):操作人将将请求一直移动到Completed状态。
名称描述类型
ididinteger
process_id所属流程的idinteger
action_type操作类型string
name操作名称string
description操作描述string

这里需要提及的一点是,现实工作中往往会存在着一次的审批需要多人协作上下级协作后才会生效的情景,或者是多个不同动作可以达到同样的效果.所以设计中是允许多个动作对一次转换进行操作的, 即转换(Transition)和动作(Action)之间是多对多的, 因此:
引入中间表 TransitionAction

名称描述类型
transition_idtransition_idinteger
action_idaction_idinteger

此外, 活动(Activity)是标记转换到状态节点后产生的事件.
可以是当工作进入到了某个状态, 也可以是完成了从某个状态到某个状态的转换触发的.

活动(Activity)的设计

活动(Activity)本身的设计和动作(Action)结构类似, 不同的是在不同系统中Activity是可以被多态关联到不同的动作事件的, 在这就不引申出去讲.

回到事件本身, 可以是由state触发的,也可以是Transition触发的, 他们之间亦是多对多的关系

设计表: 略

5. 工作流请求(Request)的设计

在我们的工作流引擎中,将请求定义为:在某一个流程下由某个用户发起的一次申请, 请求具有以下基本部分:

  • 基本信息:标题,创建日期,创建用户和当前状态ID。
  • 数据:与单个请求相关的可变数据集。
  • 利益相关者:一组将接收有关请求的定期更新的用户。
  • 文件:与单个请求相关的任何文件。
  • 备注:用户输入的与个人请求相关的任何注释。
  • 请求操作:可以在请求的任何给定时间执行的操作

请求(Request)的设计

请求是在流程下的一次操作所以表的设计可能包含:

名称描述类型
ididinteger
process_id关联流程的idinteger
title请求的标题string
user_id请求的用户integer
current_state_id请求的用户integer
requested_at请求的时间datetime

在不同的工作流程下, 请求需要发送的数据可能都不一样, 所以针对请求数据(RequestData)也做了一层包装:

名称描述类型
ididinteger
request_id所属请求的idinteger
key数据的keystring
value数据的valuestring

一次的请求也可能要包含通知的对象(User).利益相关者(RequestStakeHolder)

名称描述类型
request_id所属请求的idinteger
user_id所属用户的idinteger

当然一次完整的工作流程不只包含了这些, 它可能会有关联的文件(Attach), 备注(Note), 这些都可以用基础的附件,备注对象多态关联到各个步骤下所需要的地方去.

6. 工作流请求操作(RequestAction)的设计

到目前为止,我们建立的所有基础设施都已经基本完成。最后,我们可以构建模式的最后一部分:Request Actions表。

请求操作(RequestAction) 的设计

请求操作(RequestAction)

我们现在拥有用户可以执行的操作来调用Transitions, 当然请求是不能执行任何操作的, 只执行我们配置的范围内的动作.

让我们先看看RequestActions表的模式,然后展示它将如何实际使用:

名称描述类型
ididinteger
request_id所属请求的idinteger
action_id所属操作的idinteger
transition_id所属转换的idinteger
is_active是否可执行boolean
is_complete是否已完成boolean

我们是如何来使用request_action的呢

  1. 当请求进入状态时,我们从该状态获得所有符合的转换。对于那些Transitions中的每个Action,我们构建出RequestAction对象,且is_active = true,is_completed = false。
  2. 用户可以随时提交动作。每个提交的Action都包含一个RequestID和一个UserID。
  3. 提交操作时,我们检查指定请求的RequestActions。如果提交的Action与其中一个(is_active = true)的活动RequestActions匹配,设置 is_active = false 和 is_completed = true。
  4. 将提交的操作标记为已完成后,我们将检查该请求(Request)中该转换(Transition)的所有操作(RequestAction)。如果所有RequestActions都标记为已完成,那么我们将禁用所有剩余的操作(通过设置is_active = false,例如,未匹配的Transitions的所有操作)。

实例演示:

工作流有
状态(State): A(开始), B(同意), C(拒绝)

转换(Transition): A -> B(ID 1),A -> C(ID 2),B -> C(ID 3)

过渡行动(Action):
A - > B:申请人批准(ID 1)并由主管批准(ID 2)
A - > C:被高管拒绝(ID 3)
B - > C:被请求者拒绝(ID 4)

创建

申请人甲创建了一个请求(ID 1), 该请求立即被置于A状态。
此时, 系统将查找状态A的所有转换,并找到其中两个转换,转换1和2.然后,它将以下数据加载到RequestActions中:

请求ID动作ID转换IDis_activeis_completed
111truefalse
121truefalse
132truefalse

现在请求只是处于当前状态,等待提交动作。

提交

当甲提交申请后, 状态流转到A, 变成了可以同意可以拒绝的形态.

请求ID动作ID转换IDis_activeis_completed
111falsetrue
121truefalse
132truefalse
批准

领导乙批准了甲的申请

请求ID动作ID转换IDis_activeis_completed
111falsetrue
121falsetrue
132truefalse

注意,此时因为转换Transition 1 的两个动作都已完成,我们现在必须遵循转换1并将请求移动到下一个状态,即状态B.在我们转到状态B后,我们从该状态加载转换的动作并禁用任何旧的行动;
即,状态流入B后, A->B 之间的Transition(1) 的所有Action(1, 2) 都被完成了, is_completed = true

并且, 处于状态A的所有Action(1, 2, 3)都被禁用了. is_active = false

请求ID动作ID转换IDis_activeis_completed
111falsetrue
121falsetrue
132falsefalse
143truefalse

7. 结语

以上就是关于工作流引擎的表结构设计

查看原文

刘悦的技术博客 发布了文章 · 9月30日

一代版本一代神:利用Docker在Win10系统极速体验Django3.1真实异步(Async)任务

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_177

就在去年(2019年),Django官方发布3.0版本,内核升级宣布支持Asgi,这一重磅消息让无数后台研发人员欢呼雀跃,弹冠相庆。大喜过望之下,小伙伴们兴奋的开箱试用,结果却让人大跌眼镜:非但说好的内部集成Websocket没有出现,就连原生的异步通信功能也只是个壳子,内部并未实现,很明显的换汤不换药,这让不少人转身投入了FastAPI的怀抱。不过一年之后,今天8月,Django3.1版本姗姗来迟,这个新版本终于一代封神,不仅支持原生的异步视图,同时也支持异步中间件,明显整了个大活。

本次我们利用Docker制作一款基于Django3.1.1的项目镜像,实际体验一下Django原生异步的魅力。

首先在宿主机安装新版Django

pip install Django==3.1.1

新建一个项目,名字为django31

django-admin.py startproject django31 .

进入项目目录可以发现,熟悉的入口文件mange.py已经消失不见,新增了asgi.py文件用来启动项目,这里我们使用异步服务器uvicorn来启动新版Django,而uvicorn对windows系统支持不够友好,所以使用Docker来构建一个运行镜像,简单方便,进入django31目录,新建Dockerfile:

FROM python:3.7  
WORKDIR /Project/django31  
  
COPY requirements.txt ./  
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple  
  
COPY . .  
ENV LANG C.UTF-8  
WORKDIR /Project  
CMD ["uvicorn", "django31.asgi:application","--host","0.0.0.0"]

这里需要注意一点,Docker每创建一个容器,会在iptables中添加一个规则,每个容器都会在本机127.17.X.X范围内分配一个地址,容器绑定的主机端口会映射到本机的127.17.X.X的容器抛出端口上。所以容器内部的项目绑定的ip不能是127.0.0.1,要绑定为0.0.0.0,这样绑定后容器内部app的实际ip由Docker自动分配,所以这里uvicorn启动参数需要用host强制绑定为0.0.0.0。

随后在项目中创建依赖文件requirements.txt:

django==3.1.1  
uvicorn  
httpx

开始编译镜像文件:

docker build -t 'django31' .

编译成功后大概1g左右

liuyue:django31 liuyue$ docker images  
REPOSITORY                  TAG                   IMAGE ID            CREATED             SIZE  
django31                    latest                e8afbbbb9305        30 minutes ago      919MB

然后我们来启动项目:

docker run -it --rm -p 8000:8000 django31

后台显示启动顺利,绑定在容器内的0.0.0.0:

liuyue:django31 liuyue$ docker run -it --rm -p 8000:8000 django31  
INFO:     Started server process [1]  
INFO:     Waiting for application startup.  
INFO:     ASGI 'lifespan' protocol appears unsupported.  
INFO:     Application startup complete.  
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

浏览器访问:http://localhost:8000

熟悉的小火箭又起飞了,接下来我们来编写第一个异步视图views.py

from django.http import HttpResponse  
async def index(request):  
    return HttpResponse("异步视图")

修改一下路由文件urls.py

from django.contrib import admin  
from django.urls import path  
from django31.views import index  
  
urlpatterns = [  
    path('admin/', admin.site.urls),  
    path("", index)  
]

重新编译镜像:

docker build -t 'django31' .  
docker run -it --rm -p 8000:8000 django31

访问http://localhost:8000

没有问题,还记得去年我们曾经使用Siege对Django2.0版本进行压力测试吗?现在我们再来测一下

siege -c150 -t60S -v -b 127.0.0.1:8000

150个并发持续一分钟,看看新版Django的抗压能力怎么样:

liuyue:~ liuyue$ siege -c150 -t60S -v -b 127.0.0.1:8000  
  
{    "transactions":                   10517,  
    "availability":                  100.00,  
    "elapsed_time":                   59.70,  
    "data_transferred":                0.12,  
    "response_time":                0.84,  
    "transaction_rate":              176.16,  
    "throughput":                    0.00,  
    "concurrency":                  148.58,  
    "successful_transactions":           10517,  
    "failed_transactions":                   0,  
    "longest_transaction":                1.13,  
    "shortest_transaction":                0.45  
}  
liuyue:~ liuyue$

从测试结果看,整体性能虽然没有质的提高,但是也还算是差强人意,乞丐级主机在uvicorn的加持下单机200个左右并发还是能抗住的。

接下来我们来体验一下真正的技术,Django内置的原生异步任务,分别同步和异步两种方式使用httpx来请求接口,方法中人为的阻塞10秒钟:



from django.http import HttpResponse  
  
import asyncio  
from time import sleep  
import httpx

#异步请求  
async def http_call_async():  
    for num in range(10):  
        await asyncio.sleep(1)  
        print(num)  
    async with httpx.AsyncClient() as client:  
        r = await client.get("https://v3u.cn")  
        print(r)  
  
#同步请求  
def http_call_sync():  
    for num in range(10):  
        sleep(1)  
        print(num)  
    r = httpx.get("https://v3u.cn")  
    print(r)

再分别通过同步和异步视图进行调用:

async def async_view(request):  
    loop = asyncio.get_event_loop()  
    loop.create_task(http_call_async())  
    return HttpResponse("非阻塞视图")  
  
  
def sync_view(request):  
    http_call_sync()  
    return HttpResponse("阻塞视图")

修改路由:

from django.contrib import admin  
from django.urls import path  
from django31.views import index, async_view, sync_view  
  
urlpatterns = [  
    path('admin/', admin.site.urls),  
    path("", index),  
    path("async/", async_view),  
    path("sync/", sync_view),  
]

重新编译:

docker build -t 'django31' .  
docker run -it --rm -p 8000:8000 django31

访问 http://localhost:8000/sync/ 看看同步的效率

很明显过程中阻塞了10秒,然后我们才等到页面结果:

再来试试不一样的,访问http://localhost:8000/async/

16毫秒,无视阻塞,瞬间响应。

通过动图我们可以发现,后端还在执行阻塞任务,但是前段已经通过异步多路复用将请求任务结果返回至浏览器了。

虽然这已经很不错了,但是稍有遗憾的是,目前Django内置的ORM还是同步机制,也就是说当我们读写数据库的时候还是阻塞状态,此时的场景就是异步视图内塞入了同步操作,这该怎么办呢?可以使用内置的sync_to_async方法进行转化:

from asgiref.sync import sync_to_async  
async def async_with_sync_view(request):  
    loop = asyncio.get_event_loop()  
    async_function = sync_to_async(http_call_sync)  
    loop.create_task(async_function())  
    return HttpResponse("(via sync_to_async)")

由此可见,Django3.1在异步层面真的开始秀操作了,这就带来另外一个问题,既然原生异步任务已经做得这么牛逼了,我们到底还有没有必要使用Celery?

其实关于Django的异步视图只是提供了类似于任务或消息队列的功能,但功能上并没有Celery强大。如果你正在使用(或者正在考虑)Django3.1,并且想做一些简单的事情(并且不关心可靠性),异步视图是一种快速、简单地完成这个任务的好方法。如果你需要执行重得多的、长期运行的后台进程,你还是要使用Celery。

简而言之,Django3.1的异步任务目前仅仅是解决Celery过重的一个简化方案而已。

结语:假如我们说,新世纪以来在Python在Web开发界有什么成就,无疑的,我们应该说,Django和Flask是两个颠扑不破的巨石重镇,没有了它们,Python的web开发史上便要黯然失光,Django作为第一web开发框架,要文档有文档,要功能有功能,腰斩对手于马下,敏捷开发利器。Django3.1的发布仿佛把我们又拉回到了Django一统江湖的年代,那个美好的时代,让无数人午夜梦回。

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_177

查看原文

赞 0 收藏 0 评论 0

刘悦的技术博客 发布了文章 · 9月30日

利用DockerHub在Centos7.7环境下部署Nginx反向代理Gunicorn+Flask独立架构

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_165

上一篇文章:Docker在手,天下我有,在Win10系统下利用Docker部署Gunicorn+Flask打造独立镜像,是在Win10简单玩了一下Docker的镜像打包,属实玩票,娱乐属性较高。要是想真刀真枪的在生产环境部署还得是Centos。

本次使用Nginx反向代理Flask服务,为什么要加一层Nginx呢?因为Nginx可以直接处理静态文件请求而不用经过应用服务器,避免占用宝贵的运算资源,并且可以缓存静态资源,使访问静态资源的速度有效提高。同时它可以吸收一些瞬时的高并发请求,让Nginx先保持住连接(缓存http请求),然后后端慢慢消化掉这些并发。当然了,最重要的一点就是Nginx可以提供负载均衡策略,这样我们的应用服务就可以横向扩展,分担压力了。

大体架构如下:

首先出场的是贵为Docker三大核心之一的DockerHub(仓库),我们可以将打包好的镜像免费push到上面,就这样就可以随时pull自己的镜像,注册地址:https://hub.docker.com/

激活账号以后,创建仓库,这一步和github创建代码仓库差不太多。

填写仓库信息具体为仓库名称、描述以及是否公开或者私有。

创建成功之后,它就会出现在镜像列表中

此时我们需要对本地的镜像重命名,这里重命名为zcxey2911/myflask。因为要与dockerhub上的仓库对应。如果名称不对应是无法将本地镜像push到线上仓库中。

docker tag myflask zcxey2911/myflask

之后在命令行输入命令

docker login

用DockerHub的账号和密码登录

登录成功之后,用命令把本地镜像push到hub中

docker push zcxey2911/myflask

注意这里的镜像名称必须和hub中的仓库名称一致,否则将会抛出错误。

上传成功后,就可以在DockerHub中看到它了,此时就能随意pull操作了

前置操作已经完毕,此时,登录你的云服务器,这里以百度云的Centos7.7为例子,进入服务器后安装Docker服务

#升级yum  
sudo yum update  
#卸载旧版本docker  
sudo yum remove docker  docker-common docker-selinux docker-engine  
#安装依赖  
sudo yum install -y yum-utils device-mapper-persistent-data lvm2  
#设置源  
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo  
sudo yum makecache fast  
#安装docker  
sudo yum install docker-ce  
  
#启动服务  
sudo systemctl start docker

安装完成后键入 docker -v

返回Docker版本号说明没有问题。

拉取我们之前打包并且上传到hub的Flask镜像

docker pull zcxey2911/myflask

下载成功后,会展示在镜像库里

运行项目,这里我们可以采用后台守护进程的模式起服务

sudo docker run -d -p 5000:5000 --name testflask zcxey2911/myflask

使用docker ps命令可以看到是否运行成功。

使用服务器的ip访问一下Flask服务,这里有个小坑,不论是腾讯云、阿里云还是百度云亦或是各种乱七八糟的云,都需要在安全组策略中开放你需要访问的端口,比如这里我用的5000。

好了,现在我们同样利用Docker来安装Nginx服务

docker pull nginx

随后启动Nginx测试一下

docker run -d -p 80:80 nginx

现在,我们将运行Nginx容器里的配置文件copy到宿主机里面

前面是容器的路径 后面是宿主机的路径

docker cp 容器id:/etc/nginx/conf.d/default.conf /root/default.conf

容器id可以通过docker ps命令查看

复制出来之后,输入命令修改这个nginx配置

vim /root/default.conf

将Gunicorn配置加到里面

server {  
    listen       80;  
    server_name  localhost;  
  
    #charset koi8-r;  
    #access_log  /var/log/nginx/host.access.log  main;  
  
     location / {  
        proxy_pass http://你的服务器公网ip:5000; # 这里是指向 gunicorn host 的服务地址  
        proxy_set_header Host $host;  
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  
    }  
  
    #error_page  404              /404.html;  
  
    # redirect server error pages to the static page /50x.html  
    #  
    error_page   500 502 503 504  /50x.html;  
    location = /50x.html {  
        root   /usr/share/nginx/html;  
    }  
  
   
}

修改完配置文件之后,关掉运行的nginx服务容器,并且删掉它

docker stop 容器id  
docker rm $(docker ps -a -q)

随后再次启动Nginx容器,不过这次和上次不同之处就是需要用到 -v 进行挂载了,挂载简单理解就是将宿主机的文件替换Docker容器内部的文件,达到修改的效果。

docker run --name mynginx -d -p 80:80 -v /root/default.conf:/etc/nginx/conf.d/default.conf nginx

这里-v参数也遵循冒号左侧为宿主机右侧为容器的原则。

重新启动成功后,访问服务器ip

发现已经部署成功,整个流程轻松加愉快,比原始的命令行shell安装不知快了多少倍,最后奉上Dockerhub地址:https://hub.docker.com/r/zcxe... 和 Flask工程地址:https://gitee.com/QiHanXiBei/...

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_165

查看原文

赞 0 收藏 0 评论 0

刘悦的技术博客 发布了文章 · 9月30日

上穷碧落下凡尘:Win10系统下基于Docker配置Elasticsearch7配合Python3进行全文检索交互

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_166

基于文档式的全文检索引擎大家都不陌生,之前一篇文章:使用Redisearch实现的全文检索功能服务,曾经使用Rediseach来小试牛刀了一把,文中戏谑的称Rediseach已经替代了Elasticsearch,其实不然,Elasticsearch作为老牌的全文检索引擎还并没有退出历史舞台,依旧占据主流市场,桃花依旧笑春风,阿里也在其ecs服务中推出了云端Elasticsearch引擎,所以本次我们在Win10系统中依托Docker来感受一下Elasticsearch的魅力。

首先安装Docker,具体流程请参照:win10系统下把玩折腾DockerToolBox以及更换国内镜像源(各种神坑),这里不再赘述。

拉取Elasticsearch镜像,这里我们使用7.0以上的版本,该版本从性能和效率上都得到了优化。

docker pull elasticsearch:7.2.0

随后运行Elasticsearch镜像

docker run --name es -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -d elasticsearch:7.2.0

容器别名我们就用缩写es来替代,通过 9200 端口并使用 Elasticsearch 的原生 传输 协议和集群交互。集群中的节点通过端口 9300 彼此通信。如果这个端口没有打开,节点将无法形成一个集群,运行模式先走单节点模式。

启动容器成功后,可以访问一下浏览器: http://localhost:9200

OK,没有任何问题,Elasticsearch 采用 YAML 文件对系统进行配置,原理很简单,就像Django的settings或者Flask的Config,只要通知Elasticsearch服务在运行过程中一些你想要的功能,而Elasticsearch会找到elasticsearch.yml,之后按你指定的参数运行服务。

此时,我们需要将容器内部Elasticsearch的配置文件拷贝出来,这样以后启动容器就可以按照我们自己指定的配置来修改了。

docker cp 容器id:/usr/share/elasticsearch/config/elasticsearch.yml ./elasticsearch.yml

老规矩,前面的是容器内地址,后面的是宿主机地址,这里我就拷贝到当前目录下,当然了,你也可以指定绝对路径。

打开elasticsearch.yml,可以自己加一些配置,比如允许跨域访问,这样你这台Elasticsearch就可以被别的服务器访问了,这是微服务全文检索系统架构的第一步。

cluster.name: "docker-cluster"  
network.host: 0.0.0.0  
http.cors.enabled: true  
http.cors.allow-origin: "*"

然后停止正在运行的Elasticsearch容器,并且删除它。

docker stop 容器id  
docker rm $(docker ps -a -q)

再次启动Elasticsearch容器,这一次不同的是,我们需要通过-v挂载命令把我们刚刚修改好的elasticsearch.yml挂载到容器内部去,这样容器就根据我们自己修改的配置文件来运行Elasticsearch服务。

docker run --name es -v /es/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -d elasticsearch:7.2.0

这里需要注意一点,就是在Win10宿主机里需要单独设置一下共享文件夹,这里我设置的共享文件夹叫做es,如果是Centos或者Mac os就直接写真实物理路径即可。

这里再简单介绍一下Win10如何设置共享文件夹用来配合Docker的挂载,打开virtualBox设置,新建一个共享文件夹es

随后,重启Docker,输入命令进入默认容器:docker-machine ssh default

在容器根目录能够看到刚刚设置的共享文件夹,就说明设置成功了。

另外还有一个需要注意的点,就是Elasticsearch存储数据也可以通过-v命令挂载出来,如果不对数据进行挂载,当容器被停止或者删除,数据也会不复存在,所以挂载后存储在宿主机会比较好一点,命令是:

docker run --name es -v /es/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /es/data:/usr/share/elasticsearch/data -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -d elasticsearch:7.2.0

再次启动容器成功之后,我们就可以利用Python3来和全文检索引擎Elasticsearch进行交互了,安装依赖的库。

pip3 install elasticsearch

新建es_test.py测试脚本

建立Elasticsearch的检索实例

from elasticsearch import Elasticsearch  
   
es = Elasticsearch(hosts=[{"host":'Docker容器所在的ip', "port": 9200}])

这里的host指容器ip,因为可以扩展集群,所以是一个list,需要注意一点,如果是Win10就是系统分配的那个ip,Centos或者Mac os直接写127.0.0.1即可。

建立索引(Index),这里我们创建一个名为 article 的索引

result = es.indices.create(index='article', ignore=400)  
print(result)

  
{'acknowledged': True, 'shards_acknowledged': True, 'index': 'article'} 

其中的 acknowledged 字段表示创建操作执行成功。

删除索引也是类似的,代码如下:

result = es.indices.delete(index='article', ignore=[400, 404])  
print(result)  
  
{'acknowledged':True}

插入数据,Elasticsearch 就像 MongoDB 一样,在插入数据的时候可以直接插入结构化字典数据,插入数据可以调用 index() 方法,这里索引和数据是强关联的,所以插入时需要指定之前建立好的索引。

data = {'title': '我在北京学习人工智能', 'url': 'http://123.com','content':"在北京学习"}  
result = es.index(index='article',body=data)  
print(result)

{'_index': 'article', '_type': '_doc', '_id': 'GyJgb3MBuQaE6wYOApTh', '_version': 1, 'result': 'created', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 5, '_primary_term': 1} 

可以看到index()方法会自动生成一个唯一id,当然我们也可以使用create()方法创建数据,不同的是create()需要手动指定一个id。

修改数据也非常简单,我们同样需要指定数据的 id 和内容,调用 index() 方法即可,代码如下:

data = {'content':"在北京学习python"}  
  
#修改  
result = es.index(index='article',body=data, id='GyJgb3MBuQaE6wYOApTh')

{'_index': 'article', '_type': '_doc', '_id': 'GyJgb3MBuQaE6wYOApTh', '_version': 2, 'result': 'updated', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 6, '_primary_term': 1} 

删除数据,可以调用 delete() 方法,指定需要删除的数据 id 即可

#删除  
result = es.delete(index='article',id='GyJgb3MBuQaE6wYOApTh')  
print(result)

{'_index': 'article', '_type': '_doc', '_id': 'GyJgb3MBuQaE6wYOApTh', '_version': 3, 'result': 'deleted', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 7, '_primary_term': 1} 

查询数据,这里可以简单的查询全量数据:

#查询  
result = es.search(index='article')  
print(result)

{'took': 1079, 'timed_out': False, '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0}, 'hits': {'total': {'value': 5, 'relation': 'eq'}, 'max_score': 1.0, 'hits': [{'_index': 'article', '_type': 'blog', '_id': '1', '_score': 1.0, '_source': {'title': '我在北京学习人工智能', 'url': 'http://123.com', 'content': '在北京学习'}}, {'_index': 'article', '_type': 'blog', '_id': 'FyIdb3MBuQaE6wYO8JQR', '_score': 1.0, '_source': {'title': '你好', 'content': '你好123'}}, {'_index': 'article', '_type': 'blog', '_id': 'GCIeb3MBuQaE6wYOnpSv', '_score': 1.0, '_source': {'title': '你好', 'url': 'http://123.com', 'content': '你好123'}}, {'_index': 'article', '_type': 'blog', '_id': 'GSJfb3MBuQaE6wYOu5RD', '_score': 1.0, '_source': {'title': '你好', 'url': 'http://123.com', 'content': '你好123'}}, {'_index': 'article', '_type': 'blog', '_id': 'GiJfb3MBuQaE6wYO5pR4', '_score': 1.0, '_source': {'title': '你好', 'url': 'http://123.com', 'content': '你好123'}}]}} 

还可以进行全文检索,这才是体现 Elasticsearch 搜索引擎特性的地方。

mapping = {  
    'query': {  
        'match': {  
            'content': '学习 北京'  
        }  
    }  
}  
  
result = es.search(index='article',body=mapping)  
print(result)

{'took': 4, 'timed_out': False, '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0}, 'hits': {'total': {'value': 1, 'relation': 'eq'}, 'max_score': 4.075481, 'hits': [{'_index': 'article', '_type': 'blog', '_id': '1', '_score': 4.075481, '_source': {'title': '我在北京学习人工智能', 'url': 'http://123.com', 'content': '在北京学习'}}]}} 

可以看出,检索时会对对应的字段全文检索,结果还会按照检索关键词的相关性进行排序,这就是一个基本的搜索引擎雏形。

除了这些最基本的操作,Elasticsearch还支持很多复杂的查询,可以参照最新的7.2版本文档:https://www.elastic.co/guide/...

结语:体验了之后,有人说,Elasticsearch这玩意还真不错,能不能把Mysql或者Mongo全都扔了,就拿它当数据库不就完事了吗?答案当然是不可能的,因为Elasticsearch没有事务,而且是查询是近实时,写入速度很慢,只是读取数据快,成本也比数据库高,几乎就在靠吃内存提高性能,它目前只是作为搜索引擎的存在,如果你的业务涉及全文检索,那么它就是你的首选方案之一。

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_166

查看原文

赞 1 收藏 1 评论 0

刘悦的技术博客 发布了文章 · 9月30日

2020年是时候更新你的技术武器库了:Asgi vs Wsgi(FastAPI vs Flask)

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_167

也许这一篇的标题有那么一点不厚道,因为Asgi(Asynchronous Server Gateway Interface)毕竟是Wsgi(Web Server Gateway Interface)的扩展,而FastAPI毕竟也是站在Flask的肩膀上才有了突飞猛进的发展,大多数人听说Asgi也许是因为Django的最新版(3.0)早已宣布支持Asgi网络规范,这显然是一个振奋人心的消息,2020年,如果你在Web开发面试中不扯一点Asgi,显然就有点落后于形势了。

那么到底啥是Wsgi,什么又是Asgi,放心,不扯CGI,不扯各种抽象概念,简单粗暴理解:

Wsgi是同步通信服务规范,客户端请求一项服务,并等待服务完成,只有当它收到服务的结果时,它才会继续工作。当然了,可以定义一个超时时间,如果服务在规定的时间内没有完成,则认为调用失败,调用方继续工作。

Wsgi简单工作原理示意图:

简单实现:

#WSGI example   
  
  
def application(environ, start_response):  
  
  
    start_response('200 OK', [('Content-Type', 'text/plain')])  
  
  
    return b'Hello, Wsgi\n'

Asgi是异步通信服务规范。客户端发起服务呼叫,但不等待结果。调用方立即继续其工作,并不关心结果。如果调用方对结果感兴趣,有一些机制可以让其随时被回调方法返回结果。

Asgi简单工作原理示意图:

简单实现:

#Asgi example  
  
async def application(scope, receive, send):  
  
  
    event = await receive()  
  
  
    ...  
  
  
    await send({"type": "websocket.send", ...})

简单总结一下:Asgi是异步的,Wsgi是同步的,而基于Wsgi的Flask是同步框架,基于Asgi的FastAPI是异步框架,就这么简单,那么同步框架和异步框架的区别到底在哪儿?为什么要把Flask换成FastAPI?

不靠拍脑门儿、也不是道听途说、人云亦云。玩技术的应该用数据说话,论点永远依托论据,所以我们来简单对两款框架的性能做一个测试,首先分别安装依赖的库。

Flask:

pip install gunicorn  
pip install gevent  
pip install flask

FastAPI:

pip install fastapi  
pip install uvicorn

我们首先干的一件事就是,看看Flask和FastAPI如何处理来自多个客户端的多个请求。特别是当代码存在效率问题时(比如数据库查询时间长这种耗时任务),这里故意使用time.sleep()来模拟耗时任务,为什么不用asyncio呢?因为众所周知的原因:time.sleep是阻塞的。

Flask:

from flask import Flask  
from flask_restful import Resource, Api  
from time import sleep  
  
app = Flask(__name__)  
api = Api(app)  
  
class Root(Resource):  
    def get(self):  
        print('睡10秒')  
        sleep(10)  
        print('醒了')  
        return {'message': 'hello'}  
  
api.add_resource(Root, '/')  
  
if __name__ == "__main__":  
    app.run()

FastApi:

import uvicorn  
from fastapi import FastAPI  
from time import sleep  
app = FastAPI()  
  
@app.get('/')  
async def root():  
    print('睡10秒')  
    sleep(10)  
    print('醒了')  
    return {'message': 'hello'}  
  
if __name__ == "__main__":  
    uvicorn.run(app, host="127.0.0.1", port=8000)

分别启动服务

Flask:python3 manage.py

FastAPI:uvicorn manage:app --reload

同时一时间内,开启多个浏览器,分别并发请求首页 。

Flask:http://localhost:5000

FastAPI:http://localhost:8000

观察后台打印结果:

Flask:

FastAPI:

可以看到,同样的四次请求,Flask先是阻塞了40秒,然后依次返回结果,FastAPI则是第一次阻塞后直接返回,这代表了在FastAPI中阻塞了一个事件队列,证明FastAPI是异步框架,而在Flask中,请求可能是在新线程中运行的。将所有CPU绑定的任务移到单独的进程中,所以在FastAPI的例子中,只是在事件循环中sleep(所以异步框架这里最好不要使用time.sleep而是asyncio.sleep)。在FastAPI中,异步运行IO绑定的任务。

当然这不能说明太多问题,我们继续使用鼎鼎有名的ApacheBench分别对两款框架进行压测。

一共设置5000个请求,QPS是100(请原谅我的机器比较渣)。

ab -n 5000 -c 100 http://127.0.0.1:5000/  
ab -n 5000 -c 100 http://127.0.0.1:8000/

这里为了公平起见,Flask配合Gunicorn服务器,开3个worker,FastAPI配合Uvicorn服务器,同样开3个worker。

Flask压测结果:

liuyue:mytornado liuyue$ ab -n 5000 -c 100 http://127.0.0.1:5000/  
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>  
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/  
Licensed to The Apache Software Foundation, http://www.apache.org/  
  
Benchmarking 127.0.0.1 (be patient)  
Completed 500 requests  
Completed 1000 requests  
Completed 1500 requests  
Completed 2000 requests  
Completed 2500 requests  
Completed 3000 requests  
Completed 3500 requests  
Completed 4000 requests  
Completed 4500 requests  
Completed 5000 requests  
Finished 5000 requests  
  
  
Server Software:        gunicorn/20.0.4  
Server Hostname:        127.0.0.1  
Server Port:            5000  
  
Document Path:          /  
Document Length:        28 bytes  
  
Concurrency Level:      100  
Time taken for tests:   4.681 seconds  
Complete requests:      5000  
Failed requests:        0  
Total transferred:      1060000 bytes  
HTML transferred:       140000 bytes  
Requests per second:    1068.04 [#/sec] (mean)  
Time per request:       93.629 [ms] (mean)  
Time per request:       0.936 [ms] (mean, across all concurrent requests)  
Transfer rate:          221.12 [Kbytes/sec] received

FastAPI压测结果:

liuyue:mytornado liuyue$ ab -n 5000 -c 100 http://127.0.0.1:8000/  
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>  
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/  
Licensed to The Apache Software Foundation, http://www.apache.org/  
  
Benchmarking 127.0.0.1 (be patient)  
Completed 500 requests  
Completed 1000 requests  
Completed 1500 requests  
Completed 2000 requests  
Completed 2500 requests  
Completed 3000 requests  
Completed 3500 requests  
Completed 4000 requests  
Completed 4500 requests  
Completed 5000 requests  
Finished 5000 requests  
  
  
Server Software:        uvicorn  
Server Hostname:        127.0.0.1  
Server Port:            8000  
  
Document Path:          /  
Document Length:        19 bytes  
  
Concurrency Level:      100  
Time taken for tests:   2.060 seconds  
Complete requests:      5000  
Failed requests:        0  
Total transferred:      720000 bytes  
HTML transferred:       95000 bytes  
Requests per second:    2426.78 [#/sec] (mean)  
Time per request:       41.207 [ms] (mean)  
Time per request:       0.412 [ms] (mean, across all concurrent requests)  
Transfer rate:          341.27 [Kbytes/sec] received

显而易见,5000个总请求,Flask花费4.681秒,每秒可以处理1068.04个请求,而FastAPI花费2.060秒,每秒可以处理2426.78个请求。

结语:曾几何时,当人们谈论Python框架的性能时,总是不自觉的嗤之以鼻 ,而现在,Python异步生态正在发生着惊天动地的变化,新的框架应运而生(Sanic、FastAPI),旧的框架正在重构(Django3.0),很多库也开始支持异步(httpx、Sqlalchemy、Mortor)。软件科技发展的历史表明,一项新技术的出现和应用,常常会给这个领域带来深刻的变革,古语有云:察势者智,顺势者赢,驭势者独步天下。所以,只有拥抱未来、拥抱新技术、顺应时代才是正确的、可持续发展的道路。

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_167

查看原文

赞 0 收藏 0 评论 0

刘悦的技术博客 发布了文章 · 9月30日

基于Python3(Autosub)以及Ffmpeg配合GoogleTranslation为你的影片实现双语版字幕(逐字稿)

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_169

为影片加字幕其实是一件非常耗费时间的事情,尤其是对于打字慢的朋友来说。当然不光为影片加字幕,在其他领域,类似的逐字稿也是工作中避免不了的内容。比如写论文,如果内容中有访谈,就必须要附上逐字稿,又或者是会议的记录等等。本次使用基于Python3的AutoSub库对实时语音进行识别,然后再通过GoogleTranslation的在线API接口对语音识别后的内容进行翻译,这样就可以得到一份双语字幕(逐字稿),这里的双语不只针对国语+英语组合,也可以包含其他国家,包括小语种地区,非常方便。

首先需要安装ffmpeg,这个软件在之前有过介绍:Python3利用ffmpeg针对视频进行一些操作,Win10用户可以根据这篇文章进行安装,如果是Mac用户则非常简单,使用Homebrew就可以非常方便的进行安装

brew install ffmpeg

其后安装autosub,这个库其实就是针对Google的语音识别封装而成的,最早基于Python2,近几年也出现很多“魔改版”,这里推荐尽量安装原版的基于Python3的最新版,而使用pip直接install往往无法安装最新版,所以这里推荐用git版本库地址的方式进行安装,这样可以避免很多坑:

pip3 install git+https://github.com/agermanidis/autosub.git

安装成功后,输入命令:

autosub -h

就可以看到使用说明:

liuyue:myr liuyue$ autosub -h
usage: autosub [-h] [-C CONCURRENCY] [-o OUTPUT] [-F FORMAT] [-S SRC_LANGUAGE]
[-D DST_LANGUAGE] [-K API_KEY] [--list-formats]
[--list-languages]
[source_path]

positional arguments:
source_path Path to the video or audio file to subtitle

optional arguments:
-h, --help show this help message and exit
-C CONCURRENCY, --concurrency CONCURRENCY
Number of concurrent API requests to make
-o OUTPUT, --output OUTPUT
Output path for subtitles (by default, subtitles are
saved in the same directory and name as the source
path)
-F FORMAT, --format FORMAT
Destination subtitle format
-S SRC_LANGUAGE, --src-language SRC_LANGUAGE
Language spoken in source file
-D DST_LANGUAGE, --dst-language DST_LANGUAGE
Desired language for the subtitles
-K API_KEY, --api-key API_KEY
The Google Translate API key to be used. (Required for
subtitle translation)
--list-formats List all available subtitle formats
--list-languages List all available source/destination languages

使用方法非常简单,将你的视频或者音频放入项目文件夹,输入命令

autosub -S zh-CN -D zh-CN 视频/音频路径

这里假设你视频中的语言是国语,当然也可以是其他国别,这里是支持语言代码:

af Afrikaans
ar Arabic
az Azerbaijani
be Belarusian
bg Bulgarian
bn Bengali
bs Bosnian
ca Catalan
ceb Cebuano
cs Czech
cy Welsh
da Danish
de German
el Greek
en English
eo Esperanto
es Spanish
et Estonian
eu Basque
fa Persian
fi Finnish
fr French
ga Irish
gl Galician
gu Gujarati
ha Hausa
hi Hindi
hmn Hmong
hr Croatian
ht Haitian Creole
hu Hungarian
hy Armenian
id Indonesian
ig Igbo
is Icelandic
it Italian
iw Hebrew
ja Japanese
jw Javanese
ka Georgian
kk Kazakh
km Khmer
kn Kannada
ko Korean
la Latin
lo Lao
lt Lithuanian
lv Latvian
mg Malagasy
mi Maori
mk Macedonian
ml Malayalam
mn Mongolian
mr Marathi
ms Malay
mt Maltese
my Myanmar (Burmese)
ne Nepali
nl Dutch
no Norwegian
ny Chichewa
pa Punjabi
pl Polish
pt Portuguese
ro Romanian
ru Russian
si Sinhala
sk Slovak
sl Slovenian
so Somali
sq Albanian
sr Serbian
st Sesotho
su Sudanese
sv Swedish
sw Swahili
ta Tamil
te Telugu
tg Tajik
th Thai
tl Filipino
tr Turkish
uk Ukrainian
ur Urdu
uz Uzbek
vi Vietnamese
yi Yiddish
yo Yoruba
zh-CN Chinese (Simplified)
zh-TW Chinese (Traditional)
zu Zulu

也就是说,如果你下载了小语种国家的电影,但是不知道里面在讲些什么,也可以依赖这个库进行语音识别。

识别过程可能会有些慢,这取决于你的视频/音频的体积大小

如果想快一点,可以为autosub库手动加上自己的代理服务,打开autosub源码中的__init__.py文件,大概在99行左右使用requests库请求接口时加上proxies。

try:  
    resp = requests.post(url, data=data, headers=headers, proxies={  
        'http': 'http://127.0.0.1:4780',  
        'https': 'https://127.0.0.1:4780'  
    })  
except requests.exceptions.ConnectionError:  
    continue

识别结束后,就会将语音转储成为可见的字幕文件:

0
00:00:00,150 --> 00:00:04,380
比如现在线上怎么样是可以访问的的

1
00:00:04,381 --> 00:00:08,520
但是假设我干嘛改了你怎么办

2
00:00:08,521 --> 00:00:09,660
你还得重新打吧

3
00:00:09,661 --> 00:00:15,930
其实并不需要对有点像我们手机应用有些应用

4
00:00:15,931 --> 00:00:17,160
它是更新版本的时候

5
00:00:17,161 --> 00:00:18,660
你说要重复重新安装

6
00:00:20,010 --> 00:00:20,610
没印象

当然了,有些句子或者词汇并不准确,可能需要手工修改一下,为了让你的字幕更加精准,这样的修改工作是避免不了的。

我们得到了识别字幕后,就可以着手进行双语字幕的制作了,首先注册https://cloud.google.com/

这里新用户注册成功后,都会赠送300美金,其实就是大概可以使用一年,此时点击控制台。

在默认项目中,确保你启用了谷歌翻译服务

随后,点击凭据,生成一个新的API秘钥,该秘钥在调用接口时需要通过参数进行传递。

现在前置任务搞定了,我们来写个测试脚本

import  requests  
import json  
   
content = "Several years ago,i went to study python in beijing"  
   
language_type = "en"  
url = "https://translation.googleapis.com/language/translate/v2"  
data = {  
    'key': 'API秘钥', #你自己的api密钥  
    'source': language_type,  
    'target': 'zh-cn',  
    'q': content,  
    'format': 'text'  
}  
headers = {'X-HTTP-Method-Override': 'GET'}  
response = requests.post(url, data=data, headers=headers)  
res = response.json()  
text = res["data"]["translations"][0]["translatedText"]  
print(text)

这里我们将英文翻译成国语,可以看到速度还是蛮快的。

那如果针对字幕,则是针对国语翻译为英文,再通过文件追加的方式将英文写入到字幕每一行的下方。

经过翻译的字幕就是下面这样:

0
00:00:00,150 --> 00:00:04,380
For example, what is accessible online now
比如现在线上怎么样是可以访问的的

1
00:00:04,381 --> 00:00:08,520
But suppose I changed what about you
但是假设我干嘛改了你怎么办

2
00:00:08,521 --> 00:00:09,660
You'll have to try again
你还得重新打吧

3
00:00:09,661 --> 00:00:15,930
It doesn't have to be a little bit like we have apps on our phones, some apps
其实并不需要对有点像我们手机应用有些应用

4
00:00:15,931 --> 00:00:17,160
It's time to update the version
它是更新版本的时候

5
00:00:17,161 --> 00:00:18,660
You said you'd have to reinstall it
你说要重复重新安装

6
00:00:20,010 --> 00:00:20,610
No impression?
没印象

看起来还不错,但是现在双语字幕和视频还是分离的状态,我们需要将它们进行合并,于是又到了ffmpeg闪亮登场的时刻了。

ffmpeg -i test.mp4 -i my.srt -c:s mov_text -c:v copy -c:a copy output.mp4

上面的命令就是将目标视频和目标字幕合并为一个新的视频output.mp4

效果是这样的:

是不是感觉有点高大上,又或者,你想让字幕也炫酷一点

ffmpeg -i test.mp4 -vf "subtitles=my.srt:force_style='Fontsize=24,PrimaryColour=&H0000ff&'" -c:a copy output.mp4

这里使用force_style过滤器中的subtitles选项。使用字幕文件subs.srt并使用红色字体颜色制作字体大小为24的示例。

效果是这样的:

关于字幕更多的设置方案请参照官方文档:http://ffmpeg.org/ffmpeg-all....

结语:双语字幕可以轻松的让影片的播放量得到稳定的增长,同时也可以吸引到其他国别的观众,何乐而不为,由此可见,技术改变生活的同时,也可以改变我们工作。

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_169

查看原文

赞 0 收藏 0 评论 0

刘悦的技术博客 发布了文章 · 9月30日

基于Docker在Win10平台搭建Ruby on Rails 6.0框架开发环境

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_170

2020年,“非著名Web框架”--Ruby on Rails已经15岁了。在今年,Rails 6.0趋于完善,除了拿掉讨厌的Jquery,Webpacker 也成为默认前端打包方案,Sprockets 开始软着陆,未来很可能会和Jquery一样被彻底废弃,这就是历史的进程。

由于历史原因,本身就由Ruby撰写的HomeBrew在Mac os系统上大行其道,所以大部分Rails程序员的主力电脑都是Mac book pro,而使用Windows系统的用户就没那么好运了,比如Rails 6.0开始启用Webpacker,这就需要用户安装yarn,由此带来一系列的连锁反应,还有令人绝望的Win10系统下的CoffeeScript问题,这也是Rails包括Jekyll时常令人诟病因素之一:搭个环境都这么费劲,我为啥不用Django、Laravel亦或者是Springboot呢?为什么非得受这份罪呢?因为.......热爱,本次我们使用Docker来简化Rails环境的搭建,让它能够在各个操作系统下做到无缝开发。

当然了,第一步还是安装Docker,没有安装的朋友请参照:win10系统下把玩折腾DockerToolBox以及更换国内镜像源(各种神坑)

然后在电脑内建立一个rails项目的目录

mkdir myr  
cd myr

第三步,拉取基础镜像,该镜像内置了ruby2.5.1和node11为我们安装Rails6.0打好基础

docker pull starefossen/ruby-node

拉取镜像成功后,启动容器并且进入命令行,记住用挂载命令把当前目录共享到Docker容器内部,不会设置共享文件夹的同学可以参照这篇文章:上穷碧落下凡尘:Win10系统下基于Docker配置Elasticsearch7配合Python3进行全文检索交互

docker run --rm -v /myr:/usr/src -w /usr/src -ti starefossen/ruby-node /bin/bash

由于在容器内部已经安装好ruby2.5.1,所以gem也随之安装好,那么我们可以在容易内部安装Rails

gem install -v 6.0.2 rails

这里用-v参数可控制版本号。

在容器内安装Rails6.0成功之后,直接在容器内建立项目

rails new .

项目建立好以后,你会发现在windows目录会同步出现Rails项目文件

此时,在容器命令行内输入exit退出容器,此时容器就会停止并且删除,这个容器也完成了它的历史任务,它的存在就是帮我们创建好一个Rails项目,并且通过共享文件的形式在宿主机同步。

下一步,为了能在宿主机运行我们的Rails服务,需要一个Dockerfile文件来定制我们自己的镜像

FROM starefossen/ruby-node  
  
# 设置项目目录  
WORKDIR /usr/src/app  
  
# 设置配置文件  
COPY Gemfile* ./  
RUN bundle install  
  
# 拷贝文件  
COPY . ./  
  
# 暴露端口  
EXPOSE 3000  
  
# 启动服务命令  
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

写好Dockerfile之后,我们就可以打造一个全新的镜像,这个镜像用来运行我们已经建立好的Rails项目。

docker build -t myr .

打包成功后,输入命令查看镜像

docker images

此时,启动容器

docker run -p 3000:3000 -v /myr:/usr/src/app/ myr

访问一下 http://localhost:3000

熟悉的“世界人民大团结”欢迎页面已经映入眼帘,就是这么的简单,有人说了,Python才是如今的“当红炸子鸡”,现在学习Rails会不会是“四九年入国军”?我想说的是,时至今年,全球依然有超过一亿的网站和后台服务由Rails驱动,其中不乏国际知名企业,如:Airbnb、Basecamp、Github等,尽管Php和Python的使用范围比Ruby更广,但其最受欢迎的两个框架Laravel和Django分别比Rails的代码贡献者要少很多。更多的开源代码贡献者意味着Gem的质量非常之好,俗话说,Gem为Rails倾尽了所有,而Rails经常被人们盛赞,也是因为支持它的社区正在努力创建非常多可重用的库。

我们可以看看在github上的开源代码贡献者的数量对比:

GitHub contributors to Ruby frameworks:

Rails: 4260
Padrino: 228
Hanami: 146
Sinatra: 387

GitHub contributors to Django (Python) and Laravel (PHP):

Django: 2,007
Laravel: 740

差距可见一斑,归根结底,一款框架的开发和使用还是得以“人”为本。一如既往,专注web,专注产品的Rails6.0在新的时代里一定会继往开来、再创辉煌。

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_170

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 5 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 3月28日
个人主页被 468 人浏览