很久没写了,本来打算就“谢邀”,写点爬虫的内容,但似乎这块比较敏感,就不单独开篇写了,以后写其他内容或群里遇到讨论,就瞎聊两句算了。而这篇,也是源自群友一个偶然的“谢邀”(调百度接口,指定用Flask做web服务器做个展示),于是趁周日有空,瞎写一篇,顺便聊聊“架构”、“工具选择”,以及学习方向和学习成本的问题。
首先,直接上作业题,给代码,然后再聊废话。
作业题:调用一个免费外部接口,NLP或图像方面的都行,然后用Flask做一个服务器,语言当然是用python,把接口返回的东西展示出来。
需求给的很明确,那咱就挑个简单的,聊天机器人呗,百度、黄小鸭、图灵都有接口,当然是选“大厂”百度了。如果没有百度账号,就注册一个,进百度AI平台:https://ai.baidu.com/,登录好账号之后,在控制台界面,寻找并点击这个红框里面的东西:智能对话定制与服务平台UNIT。
创建应用,拿到API Key和Secret Key:
点击上面图片中的“UNIT配置平台”,并点击下个页面中的“进入UNIT”
然后新建一个机器人,记住ID号:
点击刚刚新建的机器人,在技能管理里面,添加技能,在列表中选取免费的(大部分免费),然后绑定给机器人
之前已经绑定了2个技能了,现在新增一个“电影”技能
选择技能,添加给机器人,实现绑定
技能列表中已经出现新技能,电影,这些技能的ID号要记住,调用接口的时候要用。
准备工作做完了,现在进入写代码环节,一个Flask主程序作为后端,一个前端(使用Flask官方定义的目录结构,放置静态页面和JS脚本,以及CSS样式)。
数据流程是这样的:
1、用JS的ajax技术,发请求(本例请求中主要是对话内容)给Flask后端;
2、再由Flask后端调用百度的API;
3、Flask后端收到百度API的返回数据后,返回给前端的ajax;
4、再由ajax渲染到静态页面上。
这是一个数据流程。N轮及多用户对话,就是重复这个流程。
整个项目的文件结构如下:
jquery-3.5.1.min.js需要自己下载,网上很多资源,下载好了直接用就行,本例用它是因为要用ajax提交请求,否则用form提交的话,稍微有点low,实在拿不出手……,放在static/JS目录下
sytle.css放在static/CSS目录下,文件内容如下:
@charset "utf-8";
body { margin:0; padding:0; width:100%;}
html { padding:0; margin:0;}
.shadow-box {width: 100%;height: 100%;position: fixed;top: 0;left: 0;background: rgba(0,0,0,0.3);z-index: 9999;}
.shadow {position: absolute;top: 50%;left: 50%;margin: -380px 0 0 -400px;}
.shadow-bg {width: 800px;height: 800px;position: absolute;background: #ffffff;border-radius: 5px;padding: 5px 5px 5px 5px;}
.shadow-bg .echart_bar{position: relative;border: 1px solid #CCC;margin: 10px 0 0 10px;padding-top: 0px;}
.shadow-bg .echart_block_1{position: relative;border: 1px solid #CCC;margin: 10px 0 0 10px;padding-top: 0px;float: left;width: 286px;height:440px;}
.shadow-bg .echart_block_1 ul { text-align:right; padding:0; margin:0; list-style:none; border:0;}
.shadow-bg .echart_block_1 ul li { float:left; margin:0; padding:0 5px; border:0; height:30px;font-size:12px;}
.shadow-bg .echart_block_2{position: relative;border: 1px solid #CCC;margin: 10px 0 0 10px;padding-top: 0px;float: left;width: 784px;height:300px;}
.shadow-bg .title{float: left;width: 800px;height: 30px;line-height: 30px;position: relative;}
.shadow-assis {position: absolute;top: 50%;left: 50%;margin: -180px 0 0 -200px;}
.shadow-bg-assis {width: 400px;height: 360px;position: absolute;background: #ffffff;border-radius: 5px;padding: 5px 5px 5px 5px;}
.shadow-bg-assis .title{float: left;width: 400px;height: 30px;line-height: 30px;position: relative;}
/* .assis-box {position: relative;width: 300px;height: 310px;margin: 10px 0 0 10px;border: 1px solid cornflowerblue;padding-top: 20px;} */
/* .shadow-bg-assis .assis-box {position: relative;width: 300px;height: 310px;border: 1px solid #CCC;margin: 10px 0 0 10px;padding-top: 0px;} */
.shadow-bg-assis .assis-box01 {width: 380px;height: 250px;overflow: auto;margin: 10px;border: 1px solid #CECECE;padding-top: 5px;}
.shadow-bg-assis .assis-txt {width: 330px;height: 18px;margin-left: 10px;}
如果不怎么讲究的话,完全可以不用CSS,都是样式,没啥好说的
index.html文件,放置在templates目录下,内容如下:
<html>
<head>
<title>proj_1_flask</title>
<link rel="stylesheet" type="text/css" href="/CSS/style.css"/>
<script src="/JS/jquery-3.5.1.min.js"></script>
</head>
<body>
<!-- 百度UNIT调用 -->
<div class="shadow-box" id="assistantArea" style="display: block;">
<div class="shadow-assis">
<div class="shadow-bg-assis" id="containerArea">
<div class="title-assis">
<span class="t_txt" id="assistantArea_title">百度UNIT调用</span>
</div>
<div id="box01" class="assis-box01"></div>
<input type="text" id="txt" class="assis-txt" placeholder="ctrl+Enter发送" />
<input type="button" id="btn" value="发送" />
</div>
</div>
</div>
<script type="text/javascript">
var oBox = document.getElementById("box01");
var oTxt = document.getElementById("txt");
var oBtn = document.getElementById("btn");
function msg() {
var val = oTxt.value;
// oBox.innerHTML += '<p>' + val + '</p>'
// oBox.scrollTop = oBox.scrollHeight; //让滚动条一直处于底部
oTxt.value=""; //发送完成侯情况输入框
$.ajax({
url: "/Baidu_Unit_Chat",
type: "POST",
dataType: "json",
async: true,
data: {"sender_name": 'your_name', "send_text": val},
beforeSend: function(XMLHttpRequest){
// Handle the beforeSend event
var p = document.createElement("p");
p.setAttribute("style", "color:#0080C1;");
p.innerHTML = 'From me:' + val
oBox.appendChild(p);
oBox.scrollTop = oBox.scrollHeight; //让滚动条一直处于底部
console.time('global');
},
success: function(res){
console.log(res);
var str = res.text;
var p = document.createElement("p");
// p.setAttribute("style", "color:#0080C1;");
p.innerHTML = 'From AI:' + str
oBox.appendChild(p);
oBox.scrollTop = oBox.scrollHeight; //让滚动条一直处于底部
console.timeEnd('global');
}
});
}
oBtn.onclick = msg;
document.onkeydown = function(e) {
var e = e || window.event;
if (e.ctrlKey && e.keyCode == 13 ||e.keyCode ==13) {
msg();
}
}
</script>
</body>
</html>
这段里面,ajax的部分稍微讲讲,对ajax不熟的人可以了解一下
红框和线条画出来的地方注意一下:
冒号前面的,是ajax内置的属性,不能改,照写上去。
url是待会提交给Flask处理的地址
type是post,这是百度api要求的
async: true,代表是“异步”提交请求。效果是你说的话,提交后马上显示在页面上,百度api返回的话,等它返回之后再显示在页面上。
如果是同步,效果就是,你要等百度返回值之后,把你说的话和百度返回的话,一起显示出来。
data就是你提交的一个字典结构,可以定义你想定义的所有内容进去,这里只定义2个作为示例,而且主要是send_text有用,send_name提交后并未做处理,就直接返回了,如果要做用户管理和鉴权,需要用到这个,可以自己扩充一下。
beforeSend和async: true配合,就是上面说的那个效果。
success是返回后发生的事,参数res就是Flask处理后,返回的内容。
最后,就是Flask了,这里的文件是Flask_Main.py,代码如下:
# coding:utf-8
import requests
import json
from flask import Flask, render_template, request
app = Flask(__name__, static_url_path='')
class UNIT:
def __init__(self, api_key, api_secret):
self.access_token = None
self.url = None
self.set_access_token(api_key, api_secret)
def set_access_token(self, api_key, api_secret):
host = 'https://aip.baidubce.com/oauth/2.0/token?' \
'grant_type=client_credentials&' \
'client_id={0}&' \
'client_secret={1}'.format(api_key, api_secret)
response = requests.post(host)
if response:
self.access_token = response.json()['access_token']
def query(self, query_text):
self.url = 'https://aip.baidubce.com/rpc/2.0/unit/service/chat?access_token=' + self.access_token
'''
提交字段说明:详情见百度AI开发中心UNIT文档中的【请求参数详细说明】
"log_id": "UNITTEST_10000", # 随便写
"version": "2.0", # 必须写2.0
"service_id": "Sxxxx", # 填你自己百度AI平台申请UNIT的机器人id
"skill_ids": ["1076657", "1076658"], # 申请机器人的技能,把技能id写这里,这两个技能是尬聊和问候,需要其他技能自己去百度拿,大部分免费
"session_id": "", # ssession保存机器人的历史会话信息,由机器人创建,客户端从上轮应答中取出并直接传递,不需要了解其内容。如果为空,则表示清空session
"request": {
"query": "%s", # 你的输入
"user_id": "88888" # 与技能对话的用户id
},
"dialog_state": {
"contexts": {"SYS_REMEMBERED_SKILLS": ["1076657"]} # dialog_state.contexts["SYS_REMEMBERED_SKILLS"]中设定一个技能ID列表,避免处于列表中的技能session被清空,就可以基于历史会话信息与机器人进行多轮对话,而无需接触复杂的原始session。
}
'''
post_data = """{
"log_id": "UNITTEST_10000",
"version": "2.0",
"service_id": "Sxxxx",
"skill_ids": ["1076657", "1076658"],
"session_id": "",
"request": {
"query": "%s",
"user_id": "88888"
},
"dialog_state": {
"contexts": {"SYS_REMEMBERED_SKILLS": ["1076657"]}
}
}""" % query_text
post_data = post_data.encode('utf-8')
headers = {'content-type': 'application/x-www-form-urlencoded'}
response = requests.post(self.url, data=post_data, headers=headers)
print(response.json())
if response:
return response.json()['result']['response_list'][0]['action_list'][0]['say']
unit = UNIT('your_api_key', 'your_api_secret')
@app.route('/')
def index():
return render_template('index.html')
@app.route('/Baidu_Unit_Chat', methods=['post'])
def ai_talk():
rst_dict = {}
send_text = request.form.get('send_text')
sender_name = request.form.get('sender_name')
out_str = unit.query(send_text) # 调用api的返回值
if out_str is None:
rst_dict['status'] = 'failure'
else:
rst_dict['status'] = 'success'
rst_dict['from_whom'] = sender_name
rst_dict['text'] = out_str
ai_say = json.dumps(rst_dict)
return ai_say
if __name__ == '__main__':
app.run(debug=True)
注释都写得很清楚了,需要找百度拿的就是下面几个值
类UNIT内的函数:
your_api_key和your_api_secret,文章开头有怎么拿的方法
函数set_access_token在UNIT类实例化的时候执行,根据上面两个值,生成token,这个token下面要用,这个token值用函数获取,是实时变化的。
函数query的参数query_text,就是前端ajax送过来的对话信息
类UNIT在Flask运行之初即实例化加载,然后提供持久服务。
类UNIT外的函数:
index函数不用说了
ai_talk函数,处理前端提交到/Baidu_Unit_Chat的请求,本例中,是和ajax配合工作。该函数中send_text = request.form.get('send_text')获得ajax送过来的对话信息,交给unit.query(send_text)处理,并赋值给out_str,返回的字典经过json打包后,发送给前端ajax,并由ajax解析后渲染到页面上。
至此,完成一个数据流程。(对应看文章前半部分关于流程的描述,观察程序实现的过程)。
最后效果放一个,用图片形式偷偷说下“爬虫”,悟一下……
因为加了电影技能,试了下,然后……百度api的效果怎样,就不讨论了……
Flask上的提示
至此,正文就完了,下面开始讲废话:
简单说说这个nlp的制作原理,我也做过内核部分,流程类似,算是番外篇吧,有兴趣就了解一下:
1、设置一些列场景,就是百度定义的“技能”,在每个场景中,找打量相关语料,进行训练,每个场景一个对话模型
2、对输入的文本,先进行场景判断,场景的数量是有限的,用文本分类做模型,判断输入文本属于哪个场景,就调用哪个场景的对话模型
3、多轮对话方面,设置session保存,同一个用户,保存前几轮的对话内容,以便判断聊天主题需要转换,也可以提供同一用户在停止聊天后,保持上一轮的语境,以便下次聊天时迅速切入,或判断是否需要转换话题
4、以上是用模型进行的选择和对话,我称之为“软切换”
5、设计“词槽”,做关键字匹配,我称之为“硬切换”,这个可以迅速提升效果,缺点是,太low……
6、还有其他很多办法,各自开脑洞
……
差不多就这样
1、Flask是不是一个好web服务器?
是个好web服务器,但由于和python绑定太深,缺点也很多,比如如果一个项目是由多人合作完成,项目组内有人用java,有人用jsp,有人用C/C++,Flask就不是一个好的选择了,而且速度方面,一直是个问题。当然,有人觉得,对于学习来说,无所谓,可以用就行。嗯,学习阶段是可以用,但熟练以后,我相信自然就会放弃Flask,至少是主框架不会再用Flask。
2、前面我重点介绍的其实是数据流这个问题,无论是对这种小demo,还是非常大的项目,都要习惯用数据流来思考架构问题,把复杂的问题,拆解成简单问题,然后每个问题,用最适合的工具去处理,最后就形成了所谓的架构。
3、在选用工具方面,要尽量的选择通用性较强的工具,学习新工具,也要学习通用性强的工具,长期坚持下去,自己的工具箱才会强大,才会在处理问题的时候,信手拈来,搭建靠谱的架构。因为,无论是通用性强还是弱,对于工程问题来说,学习成本差别不会太大。
废话也讲完了,打完收工。