最近我自己搞了个简单的聊天机器人(类似淘宝机器客服一般的),他可以帮你查询疫情的最新消息、汇报天气情况、给你讲笑话、陪你聊天等一些基本的功能。下面就来介绍一下它。
一、制作流程
-
制作思路
要做一个聊天机器人那么首先你要有一个聊天的界面,设计这个聊天界面,让它尽量好看,符合你的想法就好。然后就要想一下这个聊天界面需要什么内容:a.图标,我们打开这个页面左上方应该要有一个小图标。b.要有显示聊天记录的地方。c.要有一个输入框。d.要有一个发送的按钮。再其次是设计这个聊天机器人及其功能。 -
再次完善
a.聊天页面:这里就看自己的设计了,我自己设计的比较一般般,效果就会在最后给大家展示。
b.界面的内容:在聊天记录的那一栏,我们要知道是谁发的消息,所以我选择了使用头像这样子来表现。然后就是在发送按钮上面,我设计了一个功能,当你鼠标放在上面的时候可以显示出 “点击发送” 的字样
c.聊天机器人:我是打算接入一个百度unit平台的API,借助百度来使我的机器人更加智能化。当然也有一些部分的应答打算我自己来写函数。 -
程序框架
二、代码分析
要使用的库:
import json
import sys
import requests
from PyQt5 import QtWidgets, QtGui
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import *
以下是主函数的部分
# 实例化一个应用对象app = QtWidgets.QApplication(sys.argv)
先建立一个应用对象app
# 实例化聊天窗win = ChatBox()win.show()sys.exit(app.exec_())
然后我们创建了一个窗口类ChatBox(具体这个类如何写的后面会有介绍,这边先这样子说),建立了一个对象win。利用show函数把这个窗口显示出来。然后因为要实现这个窗口的关闭所以要用sys库中的exit函数。
以下是函数封装部分讲解
ChatBox(建立的python类)
class ChatBox(QWidget):
这是一个子类,父类是QWidget。
def __init__(self):# 初始化父类构造函数# super会找到ChatBox继承的父类QWindegt, 去实例化父类的构造函数super(ChatBox, self).__init__()# 绘制界面方法# 初始化界面self.AI_robot = Chat_robot()self.initUI()
super(ChatBox, self).init()这句的意思是:super会找到ChatBox继承的父类QWindegt, 去实例化父类的构造函数。self.AI_robot = Chat_robot()这里我实例化了一个AI机器人,Chat_robot也是一个类。self.initUI()这是初始化窗口的函数。
下面是initUI()函数的内部组成
self.setWindowTitle("快来聊天啊!")
self.setGeometry(500, 100, 800, 700)
# 美化窗口+添加控件
# 窗口图标
self.icon = QtGui.QIcon()
self.icon.addPixmap(QtGui.QPixmap("picture.png"), # 图标路径QtGui.QIcon.Normal,QtGui.QIcon.Off)
self.setWindowIcon(self.icon)
我们先设置窗口的标题,然后就是设置窗口的位置和大小,然后我们用picture这张图做了一个图标(这里要注意这个图片要放在这个工程下)。
self.left_box = QWidget(self)
self.left_box.setGeometry(10, 10, 200, 680)
self.left_box.setStyleSheet("background-color: rgb(200, 200, 169)")
# 设置背景色
self.AI = QLabel("专属机器人在线中", self)
self.AI.setGeometry(11, 11, 200, 120)
self.AI.setStyleSheet("background-color: rgb(0, 245, 255); color: black; font-size:22px")
我设计了一个边框,位置大概是在左边区域(setGeometry这个函数是确定位置的,后面的也是如此。),里面我就做了简单的一个背景色设置和一个标签“专属机器人在线中”以及标签的背景颜色和字的颜色、大小。
self.chatBox = QListWidget(self)
# 设置位置
self.chatBox.setGeometry(210, 10, 590, 600)
# 设置样式
self.chatBox.setStyleSheet("background-image: url(background.png);border:2px solid #c4c4c4; font-size:30px")
# 设置图标大小
self.chatBox.setIconSize(QSize(40, 40))
我这里绘制了一个聊天窗口,显示聊天内容的地方,setIconSize这个函数是设置说话的人的头像大小,我这里还用background这张图片做了个背景图。
self.char_input = QLineEdit(self)
self.char_input.setGeometry(210, 615, 480, 80)
self.char_input.setStyleSheet("color:black; font-size:30px; border: 10px solid #f4f4f4;""background-color: rgb(255, 255, 255);")
这个是发送消息的部分,设置了位置,字的颜色大小,待发消息框的背景色。
self.submit = QPushButton('发送', self) # 按钮显示的文字
self.submit.setToolTip('点击发送') # 当鼠标放上去后显示的内容
self.submit.setGeometry(695, 615, 100, 80) # 位置
self.submit.setStyleSheet("color:black; font-size:20px; font-weight:bold; border-radius:2;""background-color: rgb(131, 175, 155);")
这是发送按钮的设计,包括了按钮上面的文字,位置,字体的颜色,大小,背景色。
item = QListWidgetItem(QIcon("AI_robot.png"), "你好!有什么可以为你服务的?", self.chatBox)
self.submit.clicked.connect(self.send_message) # 信号与槽的连接
第一句代码是,当你启动程序的时候这个机器人会自动的说一句“你好!有什么可以为你服务的?”。QIcon(“AI_robot.png”)这个是设置机器人的头像。self.chatBox这个是说,显示在chatBox这一部分(即聊天记录框)。后面一句代码是说当你点击这个按钮是程序的反应,反应函数是send_message。
对send_message这个函数的讲解:
def send_message(self):# 用户输出什么信息content = self.char_input.text()if len(content) == 0:return # 函数终结# 把输入的信息显示在聊天区item = QListWidgetItem(QIcon("USER.png"), content, self.chatBox)# 清空输入框self.char_input.clear()
我们先从待发消息去获取你要发的消息给content。当然如果是空消息就发不出去了,所以会有个判断。然后就是把这个消息放在聊天区中,同时显示头像,并清除待发消息区的消息。
robot_reply = self.AI_robot.get_reply(content)
我同过AI_robot这个对象里面的get_reply函数来得到机器人的回答。robot_reply 就是机器人要回答的内容部分。
# 当询问天气等缺少地点元素的时候
global connect_flag, word_flag
if connect_flag:content = content + connect_word # 连接两个字符串robot_reply = self.AI_robot.get_reply(content)connect_flag = False
这里做的处理是说,当我们问“天气如何”时机器人会回答一个“你要问的是哪里的天气呢”,那么我就要将这两次的消息进行拼接,在发给机器人,不然机器人没办法做到将这两次的消息连接起来,他只会当成一个问题来回答。connect_flag这是一个标志位,当有连接的时候是True,不需要时是False。
self.deal_message(robot_reply, content)
# 下面的函数是另外定义的,放在这里只是为了一次解释清楚罢了def deal_message(self, robot_reply, content):# 处理句子的连接for index, item in enumerate(key_word):if item in robot_reply:global connect_flagconnect_flag = True# 处理重复输入global word_flag, connect_wordif word_flag == 0:connect_word = contentif connect_word == content:word_flag = word_flag + 1 # word_flag 记录出现的次数else:word_flag = 0# 处理背景的改变global bg_flagfor index, item in enumerate(weather):if item in robot_reply:bg_flag = index+1 # bg_flag 这是背景的选择,0~4,分别对应着不同的天气背景breakelse:bg_flag = 0
这个函数是用来处理句子连接、重复输入以及背景的改变这三个功能的标志位,具体函数的处理在后面。哦对了,这里使用的标志位都是全局变量。
robot_reply = self.deal_reprtion(robot_reply) # 处理重复输入多次函数
# 下面的函数是另外定义的,放在这里只是为了一次解释清楚罢了def deal_reprtion(self, robot_reply):global word_flagif word_flag == 2:robot_reply = '这个问题我已经回答过了'elif word_flag == 3:robot_reply = '你是憨憨嘛?都说了回答了你还问,再问就不理你了。'elif word_flag == 4:robot_reply = '不理你了'elif word_flag >= 5:robot_reply = Nonereturn robot_reply
当你重复输入同一个问题时,机器人就不会再回答你一边,而是会说你了
# 改变背景
self.change_background()
# 下面的函数是另外定义的,放在这里只是为了一次解释清楚罢了def change_background(self):global bg_flagif bg_flag == 1:self.chatBox.setStyleSheet("background-image: url(sunny.png);border:2px solid #c4c4c4; font-size:30px")elif bg_flag == 2:self.chatBox.setStyleSheet("background-image: url(foggy.png);border:2px solid #c4c4c4; font-size:30px")elif bg_flag == 3:self.chatBox.setStyleSheet("background-image: url(rainy.png);border:2px solid #c4c4c4; font-size:30px")elif bg_flag == 4:self.chatBox.setStyleSheet("background-image: url(cloudy.png);border:2px solid #c4c4c4; font-size:30px")else:self.chatBox.setStyleSheet("background-image: url(background.png);border:2px solid #c4c4c4; font-size:30px")
每一个标志位对应着一种天气背景,在信息处理函数时,会去遍历机器人回答的字符串,当有涉及到天气的因素时就会记录下来,同时会改变标志的值。效果展示,就像是那个询问天气的图片。
self.reply(robot_reply) # 消息的回复
# 下面的函数是另外定义的,放在这里只是为了一次解释清楚罢了def reply(self, robot_reply):if robot_reply is None:returnif len(robot_reply) >= 16:count = int(len(robot_reply)/16)for i in range(0, count):if i ==0:res = robot_reply[16*i:(16*(i+1)-1)]item = QListWidgetItem(QIcon("AI_robot.png"), res, self.chatBox)else:res = robot_reply[16*i-1:(16 * (i+1)-1)]item = QListWidgetItem(res, self.chatBox)if len(robot_reply)/16 > count:res = robot_reply[16*count-1:]item = QListWidgetItem(res, self.chatBox)returnitem = QListWidgetItem(QIcon("AI_robot.png"), robot_reply, self.chatBox)
消息的回复这个函数,我计算了一下它的长度,如果长度超过窗口长度则它会分行、多次输出,同时只有第一次输出的才会带头像。最后一句话是保证不超行的时候可以正常输出。开头的if robot_reply is None:判断是为了配合之前那个输入多次后不理你这个功能的。中间的就是用来处理字符长度的。
下面讲解机器人这个对象:
class Chat_robot:def __init__(self):self.AK = AKself.SK = SKself.access_token = self.get_access_token()
先建立一个机器人类。__init__这是一个构造函数,里面放的是这个机器人的属性。因为我们的机器人是用百度unit平台的机器人,所以这里要调用人家的API,AK,SK对应这API Key 和 Secret Key 。access_token 对应这token,具体怎么获取百度unit平台的token,参考百度的文档获取access_token。当然这里我也会介绍python获取token的方法。
def get_access_token(self):host = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' +\self.AK + '&client_secret=' + self.SKresponse = requests.get(host).json()return response['access_token']
上面的函数就是python获取token带的方式。要详细了解里面的参数的意义,要通读上面的文档。
def get_reply(self, user_input):post_data = json.dumps({"log_id": "UNITTEST_10000","version": "2.0","service_id": "S29968","session_id": "","request": {"query": user_input,"user_id": "8888",},"dialog_state": {"contexts": {"SYS_REMEMBERED_SKILLS": ["1028652"]}}})# json.dumps() 用于将dict类型的数据转成str,因为如果直接将dict类型的数据写入json文件中会发生报错,因此在将数据写入时需要用到该函数url = 'https://aip.baidubce.com/rpc/2.0/unit/service/chat?access_token=' + self.access_tokenheaders = {'content-type': 'application/x-www-form-urlencoded'}response = requests.post(url, data=post_data, headers=headers).json()if response:return response['result']['response_list'][0]['action_list'][0]['say']
上面这个函数是,用来得到机器人的回答内容的。这里要注意的是,我们在API文档中他的post_data 这个数据是采用字符串的形式写的,我们在python中没办法识别它,所以我们要用上面的这个dumps函数写入json文件之中。
三、整体代码展示
import json
import sysimport requests
from PyQt5 import QtWidgets, QtGui
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import *
# 表示从PyQt5的QtWidgets中引用全部函数(*表示全部)
# 1.应答核心--in判断成员是否在list中
# 2. 百度unitAPI接进来# 常量
AK = 'LNsYReyUKb9idkO9OHHnanm0'
SK = '28iTb65vBPmSR05vyNU6pnYjIa739KP7'key_word = ('你要找', '你要查')
weather = ('晴', '雾', '雨', '阴')
connect_flag = False
connect_word = '初始化'
word_flag = 0
bg_flag = 0# 继承
class ChatBox(QWidget):def __init__(self):# 初始化父类构造函数# super会找到ChatBox继承的父类QWindegt, 去实例化父类的构造函数super(ChatBox, self).__init__()# 绘制界面方法# 初始化界面self.AI_robot = Chat_robot()self.initUI()def initUI(self):self.setWindowTitle("快来聊天啊!")self.setGeometry(500, 100, 800, 700)# 美化窗口+添加控件# 窗口图标self.icon = QtGui.QIcon()self.icon.addPixmap(QtGui.QPixmap("picture.png"), # 图标路径QtGui.QIcon.Normal,QtGui.QIcon.Off)self.setWindowIcon(self.icon)# 左侧栏self.left_box = QWidget(self)self.left_box.setGeometry(10, 10, 200, 680)self.left_box.setStyleSheet("background-color: rgb(200, 200, 169)") # 设置背景色self.AI = QLabel("专属机器人在线中", self)self.AI.setGeometry(11, 11, 200, 120)self.AI.setStyleSheet("background-color: rgb(0, 245, 255); color: black; font-size:22px")# 右上方聊天区self.chatBox = QListWidget(self)# 设置位置self.chatBox.setGeometry(210, 10, 590, 600)# 设置样式self.chatBox.setStyleSheet("background-image: url(background.png);border:2px solid #c4c4c4; font-size:30px")# 设置图标大小self.chatBox.setIconSize(QSize(40, 40))# 右下方内容准备self.char_input = QLineEdit(self)self.char_input.setGeometry(210, 615, 480, 80)self.char_input.setStyleSheet("color:black; font-size:30px; border: 10px solid #f4f4f4; ""background-color: rgb(255, 255, 255);")# 发送按钮self.submit = QPushButton('发送', self)self.submit.setToolTip('点击发送') # 当鼠标放上去后显示的内容self.submit.setGeometry(695, 615, 100, 80)self.submit.setStyleSheet("color:black; font-size:20px; font-weight:bold; border-radius:2;""background-color: rgb(131, 175, 155);")# 点击发送按钮,发送消息item = QListWidgetItem(QIcon("AI_robot.png"), "你好!有什么可以为你服务的?", self.chatBox)self.submit.clicked.connect(self.send_message) # 信号与槽的连接def send_message(self):# 用户输出什么信息content = self.char_input.text()if len(content) == 0:return # 函数终结# 把输入的信息显示在聊天区item = QListWidgetItem(QIcon("USER.png"), content, self.chatBox)# 清空输入框self.char_input.clear()robot_reply = self.AI_robot.get_reply(content)# 当询问天气等缺少地点元素的时候global connect_flag, word_flagif connect_flag:content = content + connect_wordrobot_reply = self.AI_robot.get_reply(content)connect_flag = Falseself.deal_message(robot_reply, content)## 下面这个是处理重复输入多次函数robot_reply = self.deal_reprtion(robot_reply)# 改变背景self.change_background()self.reply(robot_reply)# 消息回复def reply(self, robot_reply):if robot_reply is None:returnif len(robot_reply) >= 16:count = int(len(robot_reply)/16)for i in range(0, count):if i ==0:res = robot_reply[16*i:(16*(i+1)-1)]item = QListWidgetItem(QIcon("AI_robot.png"), res, self.chatBox)else:res = robot_reply[16*i-1:(16 * (i+1)-1)]item = QListWidgetItem(res, self.chatBox)if len(robot_reply)/16 > count:res = robot_reply[16*count-1:]item = QListWidgetItem(res, self.chatBox)returnitem = QListWidgetItem(QIcon("AI_robot.png"), robot_reply, self.chatBox)def deal_message(self, robot_reply, content):# 处理句子的连接for index, item in enumerate(key_word):if item in robot_reply:global connect_flagconnect_flag = True# 处理重复输入global word_flag, connect_wordif word_flag == 0:connect_word = contentif connect_word == content:word_flag = word_flag + 1else:word_flag = 0# 处理背景的改变global bg_flagfor index, item in enumerate(weather):if item in robot_reply:bg_flag = index+1breakelse:bg_flag = 0def deal_reprtion(self, robot_reply):global word_flagif word_flag == 2:robot_reply = '这个问题我已经回答过了'elif word_flag == 3:robot_reply = '你是憨憨嘛?都说了回答了你还问,再问就不理你了。'elif word_flag == 4:robot_reply = '不理你了'elif word_flag >= 5:robot_reply = Nonereturn robot_replydef change_background(self):global bg_flagif bg_flag == 1:self.chatBox.setStyleSheet("background-image: url(sunny.png);border:2px solid #c4c4c4; font-size:30px")elif bg_flag == 2:self.chatBox.setStyleSheet("background-image: url(foggy.png);border:2px solid #c4c4c4; font-size:30px")elif bg_flag == 3:self.chatBox.setStyleSheet("background-image: url(rainy.png);border:2px solid #c4c4c4; font-size:30px")elif bg_flag == 4:self.chatBox.setStyleSheet("background-image: url(cloudy.png);border:2px solid #c4c4c4; font-size:30px")else:self.chatBox.setStyleSheet("background-image: url(background.png);border:2px solid #c4c4c4; font-size:30px")class Chat_robot:def __init__(self):self.AK = AKself.SK = SKself.access_token = self.get_access_token()def get_access_token(self):host = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' +\self.AK + '&client_secret=' + self.SKresponse = requests.get(host).json()return response['access_token']def get_reply(self, user_input):post_data = json.dumps({"log_id": "UNITTEST_10000","version": "2.0","service_id": "S29968","session_id": "","request": {"query": user_input,"user_id": "8888",},"dialog_state": {"contexts": {"SYS_REMEMBERED_SKILLS": ["1028652"]}}})# json.dumps() 用于将dict类型的数据转成str,因为如果直接将dict类型的数据写入json文件中会发生报错,因此在将数据写入时需要用到该函数url = 'https://aip.baidubce.com/rpc/2.0/unit/service/chat?access_token=' + self.access_tokenheaders = {'content-type': 'application/x-www-form-urlencoded'}response = requests.post(url, data=post_data, headers=headers).json()if response:return response['result']['response_list'][0]['action_list'][0]['say']# 控件 位置 样式
# 程序的主入口会有main函数
# python把每一个py脚本看成模块,可以单独运行
if __name__ == "__main__":# 实例化一个应用对象app = QtWidgets.QApplication(sys.argv)# 实例化聊天窗win = ChatBox()win.show()sys.exit(app.exec_())
四、不足之处
这个项目还有很多待改进之处,下面就来说说:
- 在拼接字符串的那个功能的时候,你会发现它单纯的拼接,不能做到比较智能化,就是把你输入的这两句话组合成一句比较通顺的话。
- 在处理消息很长分段时,你会发现每一行并不是满的,有时候会出现上面一行很短,下面一行是长的。
- 背景的变化,这里识别多个的天气关键词的时候(例如阴雨天气)他会默认跑出天气标志位比较大的那个对应的图片,可以考虑完善一下天气的关键词和图片。
- 界面可以在变的美观一些。
- 可以利用PyQt5这个库设计更多的功能
五、收获
通过这次的小项目,让我更加熟练的掌握了python的用法和基础语法以及利用类去封装函数,还学会了如何去读API文档,以及对API的调用。更加的我还认识到了PyQt5这个库。