距离上次更新本文已经过去了 674 天,文章部分内容可能已经过时,请注意甄别

翻译功能实现

打开 code/endpoints/translate.py,这便是本仓库中翻译代码的实现。

其中第一部分有道翻译的代码来自 Many-Translaters 项目,该项目上一次维护是在 4 年前,且里面的部分代码已经无法使用。该代码属于一个白嫖产物,稳定性未知。

为了不让我的 bot 在有道翻译接口失效后直接没有了翻译功能,这里我使用了彩云小译来作 “备胎”

你可以在彩云小译 的官网上找到 api 文档,内部包含了一个 Python 代码示例,开箱即用!免费用户申请的 api-key,每月有 100w 字符的免费额度,对于我们的 bot 算是够用了。

1. 关于 aiohttp 和 requests 的优劣

khl.py 一众大佬的建议下,我简单学习了 aiohttp 的代码,并将彩云小译的 requests 修改为了 aiohttp

python
1
2
3
4
5
6
7
#原有requests代码
response = requests.request("POST", url, data=json.dumps(payload), headers=headers)
return json.loads(response.text)["target"]
#替换为aiohttp代码
async with aiohttp.ClientSession() as session:
async with session.post(url, data=json.dumps(payload), headers=headers) as response:
return json.loads(await response.text())["target"]

这里简单说一下 aiohttprequests 的区别

  • requests 途中,程序会挂起,bot 将不会响应其他命令
  • aiohttp 作为异步框架,bot 在使用它的同时,可以同步处理其他命令

举个栗子:假设有用户每 1 分钟调用一次 bot 的翻译接口,彩云小译的服务器用了 3 秒钟(实际肯定没那么久)响应了我们的 requests。这 3 秒钟内,如果有其他用户调用了 bot 的另外一个指令,我们的 bot 就跟假死了一样,不会响应该用户的指令

这样看来,优势就很明显了:虽然在网络稳定的时候,requestsaiohttp 不会形成鲜明的效率差距,但在 KOOK 或者彩云小译服务器拥堵期间,我们的 bot 也能做到不会因为 requests 时间太长而影响用户的使用体验。

2. 利用抛异常机制更改翻译引擎

看到 main.py 中的 translate 部分,这里我 importtranslate.py 中的相关函数,随后使用 bot.command 来调用这两个函数。那么如何让 bot 在有道翻译接口寄了的时候,自动去找备胎彩云小译呢?

  • 简单了解抛异常

在 python 中,基本的抛异常机制如下:

python
1
2
3
4
try:
num('a', 'b')
except:
print('程序奔溃啦!')

上面这个代码就是一个简单的抛异常机制。编译器会先尝试运行 try 后面的代码,如果该部分报错,则会转而执行 except 后的代码。

转换倒我们这里的例子,我们只需要在 try 后面写入有道翻译的代码,在 except 后写入彩云小译的代码,编译器就能在有道的接口出错的时候,自动找备胎

python
1
2
3
4
try:
#有道代码实现
except:
#彩云小译代码实现

3. 关于带空格英文句子传参问题

当我们翻译一个句子的时候,中文内容往往没有空格,但英文句子极其依赖于空格进行单词的分割。

如果我们简单地使用 str 来接受传参,就会导致用户需要翻译的英文内容,只有第一个单词传了过来

python
1
async def translate(msg: Message,txt:str):

比如当用户打出:/TL I LOVE YOU 时,bot 实际接收到的只有首单词 I,它会翻译该单词,可后面的 LOVE YOU 直接被无情抛弃了!

这里我们就需要使用 python 中牛逼的不定传参 *arg 了!

python
1
async def translate(msg: Message,*arg):

*arg 是 python 中支持的不定参数传参,即函数先前不知道用户会传入多少个参数。我们可以在传参完毕后,再对这些参数进行操作。

利用 *arg 的特性,我们可以一次性把所有单词都接收过来,再在函数中将它们拼成一个完整的字符串,传入到 translate 函数中。

python
1
2
async def translate(msg: Message,*arg):
word = " ".join(arg)

这里的 " ".jion(arg) 代表用空格来分隔每一个参数,这样才能拼出一个完整的英文句子!

好了,基本的单句翻译已经写好了,但我们还可以整点花活,让 bot 可以实时翻译某一个文字频道内的所有消息!

4. 实时翻译(全局变量)

main.py 中的 ListTL,这里我为实时翻译创建了一个全局数组,用来存放需要实时翻译的文字频道 id。

注意:ListTL 作为全局变量,在函数中调用的时候,需要先用 global ListTL 进行全局变量声明。否侧程序会在该函数中创建临时变量!

  • 当用户需要实时翻译时,利用 /TLON 功能在他所在的文字频道开启该功能

bot 可以在 msg.ctx.channel.id 中获取到用户所在文字频道的 id,并将其写入 List 中

  • 使用 @bot.command(regex=r'(.+)') 正则表达式获取文字频道的所有内容,再通过判断该文字频道 id 是否存于 List 之中,来确认是否要进行实时翻译并返回结果

这种正则方法也是让 bot 监看文字频道的一个非常好的办法,比如发现关键词之后,自动发送对应消息提示。

  • 当用户使用 /TLOFF 关闭实时翻译后,将对应位置的 List 置零,空出栏位

具体的代码实现可以查看 main.pytranslate 相关部分的函数

5. 处理 met 和 rol 消息

为了避免冲突,机器人不应该翻译 @xx用户@xx角色 的消息,在 kook 的后台,机器人会接收到 (met)user_id(met)/(rol)role_id(rol) 格式的文字;

用下面的代码处理,可以将这两个串替换成空串,不进行翻译

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 单独处理met和rol消息,不翻译这部分内容
def deleteByStartAndEnd(s, start, end):
# 找出两个字符串在原始字符串中的位置
# 开始位置是:开始始字符串的最左边第一个位置;
# 结束位置是:结束字符串的最右边的第一个位置
while s.find(start) != -1:
x1 = s.find(start)
x2 = s.find(end, x1 + 5) + len(end) # s.index()函数算出来的是字符串的最左边的第一个位置,所以需要加上长度找到末尾
# 找出两个字符串之间的内容
x3 = s[x1:x2]
# 将内容替换为空字符串s
s = s.replace(x3, "")

print(f'Handel{start}: {s}')
return s

结语

使用实时翻译可以实现一些好玩的事情:比如把一句话来回翻译 N 遍进行 “提纯”

(非常无聊了属于是)