SuperDL

所属分类:机器人/智能制造
开发工具:Python
文件大小:44KB
下载次数:0
上传日期:2022-10-25 15:57:25
上 传 者sh-1993
说明:  SuperDL,设计领域特定语言(DSL)解释器以实现客服机器人
(SuperDL, Design Domain Specific Language (DSL) Interpreter to Implement Customer Service Robotics)

文件列表:
code (0, 2022-10-25)
code\Sparser.py (13008, 2022-10-25)
code\Sproc.py (953, 2022-10-25)
code\Srunner.py (16913, 2022-10-25)
code\__pycache__ (0, 2022-10-25)
code\__pycache__\Sparser.cpython-38.pyc (8174, 2022-10-25)
code\__pycache__\Sproc.cpython-38.pyc (1431, 2022-10-25)
code\__pycache__\Srunner.cpython-38.pyc (12523, 2022-10-25)
code\__pycache__\auto_run.cpython-38.pyc (1204, 2022-10-25)
code\__pycache__\config.cpython-38.pyc (1886, 2022-10-25)
code\__pycache__\main.cpython-38-pytest-6.2.5.pyc (1248, 2022-10-25)
code\__pycache__\main.cpython-38.pyc (1135, 2022-10-25)
code\auto_run.py (1517, 2022-10-25)
code\config.py (2852, 2022-10-25)
code\main.py (1435, 2022-10-25)
code\out (0, 2022-10-25)
code\out\log.log (635, 2022-10-25)
code\out\test1.out (1227, 2022-10-25)
code\out\test2.out (502, 2022-10-25)
code\out\test3.out (547, 2022-10-25)
code\out\test4.out (33, 2022-10-25)
code\out\test5.out (71, 2022-10-25)
code\parser (0, 2022-10-25)
code\parser\test1.parser (2606, 2022-10-25)
code\parser\test2.parser (2069, 2022-10-25)
code\parser\test3.parser (710, 2022-10-25)
code\parser\test4.parser (275, 2022-10-25)
code\parser\test5.parser (245, 2022-10-25)
code\speak (0, 2022-10-25)
code\speak\test1.speak (65, 2022-10-25)
code\speak\test2.speak (13, 2022-10-25)
code\test1.script (1206, 2022-10-25)
code\test2.script (898, 2022-10-25)
code\test3.script (277, 2022-10-25)
code\test4.script (54, 2022-10-25)
code\test5.script (38, 2022-10-25)
code\var (0, 2022-10-25)
code\var\test1.json (25, 2022-10-25)
... ...

# SuperDL ``` author: LLeavesG date : 2021/12/19 show : 实现一个客服脚本解释器 ``` ---
---
## 一、脚本设计说明 ### 1. 编码 默认情况下,以UTF-8编码进行解析脚本 也可以为源码文件指定不同的编码解析方式(类似于Python) 在脚本开头注明以下语句代表文件以GBK编码解析 ``` ! using coding GBK ``` --- ### 2. 关键字 $\color{red}{Entry}$ : 入口过程的过程名,缺失则无法执行脚本 $\color{red}{Proc}$ : 定义过程的定义符号 $\color{red}{begin}$ : 过程结构和分支结构的开始符 $\color{red}{end}$ : 过程结构和分支结构的结束符 $\color{red}{OUT}$ : 控制台输出指令 $\color{red}{IN}$ : 控制台输入指令 $\color{red}{Switch}$ : 分支选择结构定义符 $\color{red}{Branch}$ : 分支结构内用于选择分支的关键字 $\color{red}{Default}$: 分支结构内用于默认分支的关键字 $\color{red}{Run}$ : 运行指定过程指令 $\color{red}{Loop}$ : 循环运行指定过程指令 $\color{red}{EXIT}$ : 退出程序指令 ``` 关键字不允许被标识符定义或使用 ``` #### (1) 入口过程Entry 入口过程必须在脚本中给出具体的声明与定义 程序寻找Entry作为入口解释执行 若未定义改Proc过程将会抛出错误 定义方法 ```python= Proc Entry begin # 此处需要给出过程执行语句 OUT $name + ",请问有什么可以帮您?" end ``` 若未声明定义抛出错误 ``` Can't find Entry Proc in script The program must start at Entry Proc ``` #### (2) 过程定义Proc 过程定义方式为: ```python= Proc 过程名称(需要遵守标识符规定) begin # begin 和 end是必须的,指明过程块的开始与结束 # 此处填写过程语句 end ``` example: ```python= Proc Complain begin OUT "您的意见是我们改进工作的动力,请问您还有什么补充" IN $complain_content OUT "您的建议我们已经收到,感谢您对我们工作的支持" end ``` #### (3) 输出指令OUT 输出方式为 ```python= OUT "内容" ``` 输出指令仅允许在过程中给出 这里的内容指的是一个字符串,这里可以是单独的字符串,也可以是多字符串的拼接,也可以对字符串变量进行引用 ```python= OUT "您好" + $name + ",感谢你的来电" ``` #### (4) 输入指令IN 输入指令IN后必须跟已经提取声明的变量以将内容存储于该变量 输入方式为 ```python= IN $name ``` #### (5) 分支结构定义Switch 选择分支Switch的定义方式为 ```python= Switch $需要选择模糊匹配的变量名称 begin Branch "内容1" Run 过程名 Branch "内容2" Loop 过程名 次数 Default Run 过程名 end ``` 上述说明中,Branch后面仅允许跟字符串内容,进行模糊匹配:若内容出现在输入中则走向该分支,分支仅允许单次运行某个过程或者循环运行某个过程(见 6 分支结构跳转Branch) 匹配可进行多次匹配,即可以按顺序进入多个分支进行执行 在Switch结构中必须给出Default分支,代表无匹配内容则走向默认分支(见 7 分支结构默认跳转Default) ```python= Switch $input begin Branch "查询" Loop QueryInfo 3 Branch "投诉" Run Complain Branch "退出" Run Exit Default Run Unknow end ``` #### (6) 分支结构跳转Branch Branch仅能出现在Switch结构中 Branch代表匹配分支,只能使用Run和Loop执行过程 ```python= Branch "内容1" Run 过程名 Branch "内容2" Loop 过程名 次数 ``` #### (7) 分支结构默认跳转Default Default必须存在于每个Switch结构中,否则抛出错误 ```python= Default Run Unknow ``` #### (8) 单次运行过程语句Run Run 后跟随过程名代表执行单次该过程 可以在过程中出现,可以进行嵌套调用或递归调用 ```python= Run 过程名称 ``` example ```python= Run QueryInfo ``` #### (9) 循环运行过程语句Loop Loop后跟随过程名和循环次数可以执行指定循环次数的该过程 ```python= Loop 过程名称 次数 ``` example ```python= Loop QueryInfo 3 ``` #### (10) 退出脚本指令EXIT 使用EXIT指令代表程序的退出 **PS:EXIT代表程序的强制结束,而程序实际结束点在Entry过程的end** 样例脚本 ```python= # using coding utf-8 # 样例脚本 @ 多行注释测试 author:Test date: 2021/12/29 联通客服机器人 @ $name = "Jo hn" $input = "" $month = 1 $used = 20.20 $left = 40.80 $complain_content = "" Proc Entry begin OUT $name + ",请问有什么可以帮您?" IN $input # 根据输入内容进行模糊匹配 单行注释测试 Switch $input begin Branch "查询" Loop QueryInfo 3 Branch "投诉" Run Complain Branch "退出" Run Exit Default Run Unknow end Run Entry end Proc QueryInfo begin OUT "尊敬的" + $name + ",您" + $month + "月份账单如下" OUT "话费已用" + $used + ",剩余" + $left $month = $month + 1 $used = 1 + $used $left = $left + 1 end Proc Complain begin OUT "您的意见是我们改进工作的动力,请问您还有什么补充" IN $complain_content OUT "您的建议我们已经收到,感谢您对我们工作的支持" end Proc Unknow begin OUT "您好,没有听清,请您再说一遍" OUT "您可以选择查询,投诉或者退出" end Proc Exit begin OUT "退出成功" EXIT end ``` --- ### 3. 标识符 标识符第一个字符必须为字母表中字母或下划线 _ 标识符的其他的部分由字母、数字和下划线组成 标识符对大小写敏感 --- ### 4. 变量 #### 变量声明 变量名仅允许使用所规定的标识符,禁止使用关键字作为标识符 声明方法: ```php= $varname = value ``` #### 变量类型 仅允许出现三种变量类型:整数int 浮点数float 和字符串str 声明时无需指定变量类型,解释器将自动识别类型 同一变量的类型可以在脚本中因为赋值而改变其类型(继承python) 如示例:之前为字符串类型,进行赋值后可转变为int类型 ``` $var = "test" $var = 123 ``` #### 变量声明位置 变量不允许在过程结构或者Switch中定义 推荐在脚本开始时声明变量 #### 使用范围 所有变量均为全局变量而可以被整个脚本识别(前提是在使用前必须声明) --- ### 5. 注释 单行注释以 # 开头,多行注释以 @ 开头 @ 结束 **规定仅允许注释在一行的开头指明,不允许在语句中间或结尾出现注释符(错误注释1 2)** **不允许将注释标注到过程Proc和Switch结构的begin和定义之间(错误注释3)** **由于引入编码规范# using coding 编码,不允许在第一行进行注释** 正确示范: ``` Proc Entry begin # 单行注释 @ 多行 注释 @ OUT "内容" end ``` 错误示范: ``` Proc Entry #注释1:入口 # 注释2:这里进入 begin OUT "错误示范"@ 注释3:多行注释 @ end ``` --- ### 6. 行与缩进 不采用缩进,start标记Proc结构和Switch结构的开始,end标记结构的结束。 不使用封号作为语句的结束符,使用换行符用于标记语句的结束 **缩进可用于使代码结构更清晰,但若不使用缩进对解释并无影响** ## 二、程序设计说明 ### 1. 参数设计 使用argparse进行参数解析 ```shell= python main.py -h ``` ```shell= usage: main.py [-h] [-f SCRIPTFILE] [-d [DEBUG_FLAG]] [-s speak_filedir] [-v var_filedir] [-p parser_filedir] [-o outprint_filedir] [-r [RECORD_FLAG]] [-nc [NO_COLOR_FLAG]] [-nt [NO_TIME_FLAG]] optional arguments: -h, --help show this help message and exit -f SCRIPTFILE, --scriptfile SCRIPTFILE script file to execute -d [DEBUG_FLAG], --debug [DEBUG_FLAG] DEBUG controller [DEBUG_FLAG] = TRUE/FLASE or default -s speak_filedir, --speakfile speak_filedir if use speak , need a file(example: test.speak) as speak content to test -v var_filedir, --varfile var_filedir init var from a file (example: var.json),the file has json only -p parser_filedir, --parserfile parser_filedir load parser object from pickle file(example: test.parser) -o outprint_filedir, --outfile outprint_filedir out print content to outfile -r [RECORD_FLAG], --record [RECORD_FLAG] IF use this arg , send var infomation to http server -nc [NO_COLOR_FLAG], --no_color [NO_COLOR_FLAG] IF use Auto test and don't want to print color just add this args.[NO_COLOR_FLAG] = TRUE/FLASE or default -nt [NO_TIME_FLAG], --no_time [NO_TIME_FLAG] IF use Auto test and don't want to print time just add this args.[NO_TIME_FLAG] = TRUE/FLASE or default ``` --- ### 2. 交互设计 #### 命令行交互输入输出 若不加任何输出限制(-nt 禁止输出时间 -nc 禁止输出颜色) ![](https://md.byr.moe/uploads/upload_03dc0c221061326176aef14c08667134.png) 即存在颜色区分输入与输出 #### 文件输入 文件输出 > 由于存在测试桩 模拟语音输入后保存到文件,脚本解释器输入的来源为文件。则可以使用 -s参数指定输入文件位置进行模拟输入 ![](https://md.byr.moe/uploads/upload_8a6393***fbb768d58c3c577ae43b786b.png) > 输出可以使用-o 指定输出文件以提供测试和调试,这里由于不需要颜色,所以指定参数-nc ![](https://md.byr.moe/uploads/upload_3ceeee6bd13e1dbeaf97afddabbdedc7.png) #### 远程服务器交互 为提供测试桩,发送信息等用途,定义函数send_var()和send_msg()函数,用于向远程服务器发送消息 使用recv_msg()函数接受来自远程服务器的信息等 ```python= def send_var(self): """[测试桩: 向远程服务器发送GET请求 请求内容为变量值] """ # 测试桩: 向远程服务器发送GET请求请求内容为变量值 url = 'http://xxx.xxx.xxx.xxx/test.php?var=' + json.dumps(self.var) respose = requests.get(url=url) # 响应输出 print(respose.text) def send_msg(self, msg): """[向远程服务器发送信息] :param msg: [要发送的信息] :type msg: [str] """ # 测试桩: 向远程服务器发送信息 url = 'http://xxx.xxx.xxx.xxx/test.php?msg=' + msg respose = requests.get(url=url) def recv_msg(self,request): """[从远程服务器获取信息] :return: [返回相应] :rtype: [Respose] """ # 测试桩: 从远程服务器接收信息 url = 'http://xxx.xxx.xxx.xxx/' + request respose = requests.get(url=url) return respose ``` --- ### 3. DSL解析器整体设计 程序整体流程图: ![](https://md.byr.moe/uploads/upload_feec29a8234876a351093ade6933066e.png) 在main函数(入口)中读取传入的参数决定是否读入包含语法树parser实例的缓存 若选择读入且路径正确则直接加载缓存并且将该实例传入运行时类Runner中 否则对该脚本进行parse并且在此过程中建立语法树和变量表,同时对该脚本进行检错 ```python= # 测试桩: 读取程序语法树,提供多用户方案 if config.parserfile != '': with open(config.parserfile,'rb') as fp: parser = pickle.load(fp) else: if config.scriptfile != '' and config.scriptfile[-7:] == '.script': # 解析脚本 file, parser = get_parser(config.scriptfile) parser.parse_file(file) # 测试桩: 保存程序语法树,提供多用户方案 with open('.\\parser\\' + config.scriptfile[:-7] + '.parser', 'wb') as fp: pickle.dump(parser,fp) fp.close() else: print("There is no script for parse and run") runner = get_runner(parser) runner.start() ``` 若获得parser类并且将其传入Runner运行时类中后 Runner类对语法树自顶向下从左向右按序解析执行 执行过程中不同指令调用不同的函数进行执行,在此过程中处理脚本的部分语法错误和运行错误 等待脚本执行完成 --- ### 4. 词法语法分析器设计 词法语法分析器流程图如下: ![](https://md.byr.moe/uploads/upload_b3655e749452016b19dde2c6ee2c0c48.png) 由于脚本解释器支持多编码方式,若脚本中指定该脚本编码方式 第一遍打开文件仅读取编码方式,若无指定编码则按UTF-8编码打开脚本进行解释 ```python= if encoding[0] == "#": if encoding[1] == "using" and encoding[2] == "coding": try: # 根据编码方式重打开 file.close() file = open(self.file_name, 'r', encoding=encoding[3]) except: print("Some wrong happend at line 1") else: print("error grammer at line 1") exit(0) else: file.close() file = open(self.file_name, 'r', encoding='utf-8') # 返回文件io流 return file ``` 打开文件后读取文件的全部内容存入类成员变量lines中 声明文件行数指针指向当前分析的语句的下一行 ```python= # 过程块类实例字典 self.proc_block = {} # 存储变量的dict self.var = {} # 脚本文件名 self.file_name = file_name # 待parse的下一行的行数 self.line_num = 0 # 存储文件内容 self.lines = [] ``` 从第一行开始读取脚本文件,若遇到空行和单行注释则跳过 由于解释器是过程Proc驱动的(指令只能出现在Proc中),在解析过程中若遇到Proc声明即进入parse_proc进行单独处理,遇到变量则处理加入变量表,以及遇到多行注释进行单独的函数handle_note进行处理 在parse_proc中对指令进行词法分析和语法分析(内含Switch结构的词法语法分析),模块化过程Proc,使得分析结束后的proc单独成块作为proc实例保存到proc_block字典中以便后续执行 **函数具体说明见API文档 http://xxx.xxx.xxx.xxx/index.html** ```python= # parse_file 函数(部分) while True: i = self.line_num self.line_num += 1 if i >= len(self.lines): file.close() return line = self.lines[i].lstrip().rstrip() if line != '' and self.is_linefeed(line) == False: if self.is_note(line) == False: token = line.split() # 处理变量定义并插入到符号表 if token[0][0] == '$': self.parse_var(line) # 处理过程定义 elif token[0] == 'Proc': self.parse_proc(line) if line.strip()[0] == "@": # 处理多行注释 self.handle_note() ``` ```python= # parse_proc 函数(部分) while True: line = self.lines[self.line_num] line = line.lstrip().rstrip() self.line_num += 1 if self.is_linefeed(line): continue if self.is_note(line): if line[0] == '#': continue if line[0] == '@': self.handle_note() token = shlex.split(line) tmp_token = token[0] # 如果Token为OUT代表输出 if tmp_token == 'OUT': plus_num = token.count('+') if plus_num != len(token) - plus_num - 2: self.exception(line) i = 0 while plus_num != 0: if token[i + 2] != '+': self.exception(line) plus_num -= 1 i += 2 # 如果Token为IN代表输入 elif tmp_token == 'IN': if len(token) == 2: if token[1][0] != '$' or token[1][1:] not in self.var.keys(): self.exception(line) else: self.exception(line) ``` --- ### 5. 运行时Runner设计 运行时Runner流程图如图所示: ![](https://md.byr.moe/uploads/upload_5d56c7c65679f29bf***333c4e9f4a290.png) 在初始化运行时Runner时,包含语法树的parser实例被传入当作类成员,该实例还包含每个过程块的执行语句。解释器从Entry开始运行,从过程块中按序取指令进行执行,执行时调用对应指令的操作函数(如OUT指令调用run_out函数)。若遇到过程调用语句则直接跳转到对应过程块,开始按序执行,执行结束后即可返回。这里的栈结构和指令执行继承了python的栈结构和指令执行方式,无需另外开辟空间。 ```python= # Srunner.py run_proc函数 def run_proc(self, proc): """[运行脚本过程] :param proc: [传入过程的具体内容,包括指令行数和解析后的具体指令] :type proc: [list] """ for inst in proc.inst: if 'Switch' not in inst[1][0]: # 如果是Switch结构单独映射 if '$' not in inst[1][0]: func = self.function[inst[1][0]] func(inst) else: if len(inst[1]) == 5 or len(inst[1]) == 3: self.handle_expression(inst) else: self.exception(inst) else: # 直接根据映射内容调用函数 func = self.function[inst[1][0][0]] func(inst) ``` --- ### 6. 词法语法分析器优化设计 考虑到同一脚本若运行于多个用户,而仅存在变量上的不同,于是进行优化,同一脚本只进行一次词法分析,之后只传入语法树和运行所需要的信息。即可以通过pickle序列化parser实例并且保存起来,之后即可直接加-p 参数指定路径 进行读取并且传入Runner运行时类。 ```python= # 测试桩: 读取程序语法树,提供多用户方案 if config.parserfile != '': with open(config.parserfile,'rb') as fp: parser = pickle.load(fp) else: if config.scriptfile != '' and config.scriptfile[-7:] == '.script': # 解析脚本 file, parser = get_parser(config.scriptfile) parser.parse_file(file) # 测试桩: 保存程序语法树,提供多用户方案 with open('.\\parser\\' + config.scriptfile[:-7] + '.parser', 'wb') as fp: pickle.dump(parser,fp) fp.close() else: print("There is no script for parse and run") runner = get_runner(parser) ``` --- ### 7. 测试桩设计 #### (1) 输出重定向 通过参数指定输出文件路径,将标准输出重定向 ```-o 路径``` ```python= # 测试桩: 输出重定向到文件,可用于 if config.outfile != '': out = open(config.outfile, 'w',encoding='utf-8') sys.stdout = out ``` #### (2) 保存读取parser类实例(含语法树) 解析脚本后默认将parser实例保存于当前路径下的parser文件夹中 ```python= # 测试桩: 读取程序语法树,提供多用户方案 if config.scriptfile != '' and config.scriptfile[-7:] == '.script': # 解析脚本 file, parser = get_parser(config.scriptfile) parser.parse_file(file) # 测试桩: 保存程序语法树,提供多用户方案 with open('.\\parser\\' + config.scriptfile[:-7] + '.parser', 'wb') as fp: pickle.dump(parser,fp) fp.close() else: print("There is no script for parse and run") ``` 可以使用 下面参数读取parser实例 ```-p 路径``` ```python= # 测试桩: 读取程序语法树,提供多用户方案 if config.parserfile != '': with open(config.parserfile,'rb') as fp: parser = pickle.load(fp) ### 省略 runner = get_runner(parser) runner.start() ``` #### (3) 语音模拟文件读入 通过-s参数可以指定一个文件,该文件内容为输入内容,意在模拟语音输入驱动 ```-s 路径``` ```python= if self.config.speakfile != '': line = self.speak_file.readline().strip('\n').strip('\r\n') # 测试桩 语音输入接口 if self.config.no_color == False: print("\033[31m" + " IN : " + "\033[0m", end='') print(line) else: print(" IN : ", end='') print(line) return line ``` #### (4) 调试模式输出变量信息 可以通过-d参数输出调试信息(程序运行前初始化的变量信息和结束后的变量信息) ```-d``` ```python= # 测试桩: 若开启调试模式则输出初始化后的变量键值对 if config.debug == True: runner.print_var() ### 省略 # 测试桩: 若开启调试模式则输出结束时的变量键值对 if self.config.debug == True: self.print_var() ``` #### (5) 从json文件初始化变量 通过以下参数可以指定初始化变量内容,以满足部分变量没有在脚本中写明,需要在解释运行前查询初始化变量 ```-v 变量json文件路径``` ```python= # 测试桩: 用于接收从其他端口获得变量数据后给与脚本变量初始化 if config.varfile != '': runner.init_var(config.varfile) ``` #### (6) 结束时以json格式发送变量到远程 若使用```-r```参数可在程序结束前将变量以json格式发送到远程服务器 ```python= # 测试桩,记录客服服务内容并上传到服务器 if self.config.record == True: self.send_var() def send_var(self): """[测试桩: 向远程服务器发送GET请求请求内容为变量值] """ # 测试桩: 向远程服务器发送GET请求 请求内容为变量值 url = 'http://xxx.xxx.xxx.xxx/test.php?var=' + ... ...

近期下载者

相关文件


收藏者