Python调用有道、百度、彩云API实现自然语言翻译

本文最后更新于:2022年3月31日 下午

最近做的某个项目中需要用到中英文之间的翻译,故使用 Python 编写 MachineTranslation 包,调用有道智云、彩云小译、百度的自然语言通用翻译 API。

需求

在一个 Python 包(具有 __init__.py 的目录)中实现对三种 API 的调用,且调用方式统一。出于安全与便于编辑考虑,将所有 key/token 保存至一个 json 文件中。

目录结构

目录结构如下:

1
2
3
4
5
6
7
MachineTranslation

├── __init__.py
├── CaiYun.py
├── Baidu.py
├── YouDao.py
└── KeySecret.json

其中 CaiYun.pyBaidu.pyYouDao.py 中实现调用 API 的 Translator 类。KeySecret.json 中保存密钥。__init__.py 实现 import 与归一化。

通用调用(多态与归一化)

封装与统一接口

三种调用不同服务商的 Translator 类中有许多不同的属性、方法,然而这些不重要细节对本包的调用者来说应是完全透明的——只需要保证提供一个参数、返回值类型完全相同的 translate 方法即可。不同的具体实现,封装,提供统一的接口。

对于C++面向对象编程,可以使用抽象基类多态技术来完成这样的归一化:

  • 创建一个 Translator 抽象基类
  • 定义 Translator 中的抽象成员函数 translate,所有子类都必须实现该方法
  • 在三个子类中分别实现 translate 方法

Python 标准库中的确有 abc 这个抽象基类模块,可以实现类似上述C++多态的方式来解决问题。

鸭子类型

而以灵活著称的 Python,在实现多态时,更崇尚鸭子类型(duck typing)

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

鸭子类型是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。

只关心某个对象是否能实现 walk quack 这样的方法,实现即为鸭子,不能实现就不是,不关心该对象的类型。

某个类实现了 .translate(q, mode) 方法,即认为它是一个 Translator 类。

具体实现

最终 __init__.py 中代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# __init__.py
def machine_translator(provider: str):
"""
返回指定服务商的机器翻译类的实例

:param provider: API提供商
:return: 翻译类的实例
"""
if provider == "YouDao" or provider in ("youdao", "you_dao", "YOUDAO", "You_Dao"):
from MachineTranslation.YouDao import YouDaoTranslator

return YouDaoTranslator()

elif provider == "CaiYun" or provider in ("CaiYunXiaoYi", "caiyun", "CAIYUN"):
from MachineTranslation.CaiYun import CaiYunTranslator

return CaiYunTranslator()

elif provider == "Baidu" or provider in ("BaiDu", "baidu", "Bai_du", "BAIDU"):
from MachineTranslation.Baidu import BaiduTranslator

return BaiduTranslator()

machine_translator 函数会根据 provider 参数返回一个指定服务商类的实例化对象。

判断条件中 or 右侧的条件方便调用者使用服务商别名(类似 Python 内置类型中对 utf-8UTF-8 等别名的处理)。当然,得益于布尔运算中的短路运算,理论上调用时使用 or 左侧准确的名称时会有微小的速度优势。

将 import 语句写到条件分支中,只有调用该服务商时才 import 对应代码。若用户只调用一两个服务商的 Translator 类,可略微减小参与运行的代码量。

而在外部调用 MachineTranslation 包时的代码如下:

1
2
3
4
5
6
7
# main.py
from MachineTranslation import machine_translator

m_translator = machine_translator("YouDao") # 看似为实例化m_t类,其实是函数调用
# 无论真正实例化的是哪个Translator类,使用方式都完全一致
print(m_translator.translate("Hello World.", "en2zh"))
print(m_translator.translate("人生苦短,我用Python。", "zh2en"))

密钥 json 文件

KeySecret.json 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"YouDao": {
"APP_KEY": "Your APP_KEY here",
"APP_SECRET": "Your APP_SECRET here"
},
"CaiYun": {
"Token": "Your Token here"
},
"Baidu": {
"appid": "Your appid here",
"appkey": "Your appkey here"
}
}

用户使用前需要自行前往对应官网申请密钥,链接详见下文。

有道翻译

官方文档:有道智云-自然语言翻译服务-API文档

参考 文档化 Python 代码:完全指南(翻译),此次编写代码时加入了较丰富的 __doc__ 与 Type Hint,在 PyCharm 等 IDE 中有很好的提示。

PyCharm 可以识别文档与类型提示

模块代码全文如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# YouDao.py
import hashlib
import time
import uuid
from json import loads as json_loads

import requests

YOUDAO_URL = "https://openapi.youdao.com/api"
KEY_FILE = "MachineTranslation/KeySecret.json" # 存储key与secret的json文件路径
MAX_LENGTH = 1500 # 限制翻译输入的最大长度


def load_key_secret(key_file: str) -> tuple[str, str]:
"""
读取json文件中保存的API key

:param key_file:存储key与secret的json文件
:return:(key, secret)
"""
with open(key_file, "r", encoding="utf-8") as f:
data = json_loads(f.read())["YouDao"]
app_key = data["APP_KEY"]
app_secret = data["APP_SECRET"]
return app_key, app_secret


class YouDaoTranslator:
"""
调用有道翻译API实现机器翻译
"""

def __init__(self):
self.q = "" # 待翻译内容
self._request_data = {}
self._APP_KEY, self._APP_SECRET = load_key_secret(KEY_FILE)

def _gen_sign(self, current_time: str, salt: str) -> str:
"""
生成签名

:param current_time: 当前UTC时间戳(秒)
:param salt: UUID
:return: sign
"""
q = self.q
q_size = len(q)
if q_size <= 20:
sign_input = q
else:
sign_input = q[0:10] + str(q_size) + q[-10:]
sign_str = self._APP_KEY + sign_input + salt + current_time + self._APP_SECRET
hash_algorithm = hashlib.sha256()
hash_algorithm.update(sign_str.encode("utf-8"))
return hash_algorithm.hexdigest()

def _package_data(self, current_time: str, salt: str) -> None:
"""
设置接口调用参数

:param current_time: 当前UTC时间戳(秒)
:param salt: UUID
:return: None
"""
request_data = self._request_data

request_data["q"] = self.q # 待翻译内容
request_data["appKey"] = self._APP_KEY
request_data["salt"] = salt
request_data["sign"] = self._gen_sign(current_time, salt)
request_data["signType"] = "v3"
request_data["curtime"] = current_time
# _request_data["ext"] = "mp3" # 翻译结果音频格式
# _request_data["voice"] = "0" # 翻译结果发音选择,0为女声,1为男声
request_data["strict"] = "true" # 是否严格按照指定from和to进行翻译
# _request_data["vocabId"] = "out_Id" # 用户上传的词典,详见文档

def _set_trs_mode(self, mode: str) -> None:
"""
设置翻译语言模式

:param mode: 语言模式,en2zh或zh2en
:return: None
"""
if mode == "en2zh":
self._request_data["from"] = "en"
self._request_data["to"] = "zh-CHS"
elif mode == "zh2en":
self._request_data["from"] = "zh-CHS"
self._request_data["to"] = "en"
else:
# 处理中英互译之外的异常翻译模式
self._request_data["from"] = "auto"
self._request_data["to"] = "auto"

def _do_request(self) -> requests.Response:
"""
发送请求并获取Response

:return: Response
"""
current_time = str(int(time.time()))
salt = str(uuid.uuid1())
self._package_data(current_time, salt)
headers = {"Content-Type": "application/x-www-form-urlencoded"}
return requests.post(YOUDAO_URL, data=self._request_data, headers=headers)

def translate(self, q: str, mode: str) -> str:
"""
翻译

:param q: 待翻译文本
:param mode: 翻译语言模式,en2zh或zh2en
:return: 翻译结果
"""
if not q:
return "q is empty!"
if len(q) > MAX_LENGTH:
return "q is too long!"

self.q = q
self._set_trs_mode(mode)
response = self._do_request()
content_type = response.headers["Content-Type"]

if content_type == "audio/mp3":
# 返回mp3格式的音频结果
millis = int(round(time.time() * 1000))
file_path = "合成的音频存储路径" + str(millis) + ".mp3"
with open(file_path, "wb") as fo:
fo.write(response.content)
trans_result = file_path
else:
# 返回json格式的文本结果
error_code = json_loads(response.content)["errorCode"] # 有道API的错误码
if error_code == "0":
trans_result = json_loads(response.content)["translation"]
else:
trans_result = f"ErrorCode {error_code}, check YouDao's API doc plz."
return trans_result


if __name__ == "__main__":
KEY_FILE = "./KeySecret.json"
translator = YouDaoTranslator()
print(
translator.translate(
"So we beat on, boats against the current, borne back ceaselessly into the past.",
"en2zh",
)
)
print(translator.translate("一个人只拥有此生此世是不够的,他还应该拥有诗意的世界。", "zh2en"))

YouDaoTranslator 类中的部分属性与方法名是以单下划线开头,表明是不希望被外部修改和调用的。

百度翻译

官方文档:百度通用翻译API文档

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# Baidu.py
import json
from hashlib import md5
from random import randint

import requests

BAIDU_URL = "https://fanyi-api.baidu.com/api/trans/vip/translate"
KEY_FILE = "MachineTranslation/KeySecret.json" # 存储key与secret的json文件路径
MAX_LENGTH = 1800 # 限制翻译输入的最大长度


def load_appid(appid_file: str) -> tuple[str, str]:
"""
读取json文件中保存的appid与appkey

:param appid_file: 存储appid与appkey的json文件
:return: (appid, appkey)
"""
with open(appid_file, "r", encoding="utf-8") as f:
data = json.loads(f.read())["Baidu"]
app_id = data["appid"]
app_key = data["appkey"]
return app_id, app_key


class BaiduTranslator:
"""
调用百度通用翻译API实现机器翻译
"""

def __init__(self):
self.q = "" # 待翻译内容
self._payload = {}
self._appid, self._appkey = load_appid(KEY_FILE)

def _gen_salt_sign(self) -> tuple[int, str]:
"""
生成salt与签名

:return: (salt, sign)
"""
salt = randint(32768, 65536)
tmp_str = self._appid + self.q + str(salt) + self._appkey
sign = md5(tmp_str.encode("utf-8")).hexdigest()
return salt, sign

def _package_data(self) -> None:
"""
设置接口调用参数

:return: None
"""
salt, sign = self._gen_salt_sign()
payload = self._payload
payload["q"] = self.q
payload["appid"] = self._appid
payload["salt"] = salt
payload["sign"] = sign # 签名

def _set_trs_mode(self, mode: str):
"""
设置翻译语言模式

:param mode: 语言模式,en2zh或zh2en
:return: None
"""
if mode == "en2zh":
self._payload["from"] = "en"
self._payload["to"] = "zh"
elif mode == "zh2en":
self._payload["from"] = "zh"
self._payload["to"] = "en"
else:
# 处理中英互译之外的异常翻译模式
self._payload["from"] = "auto"
self._payload["to"] = "zh"

def _do_request(self) -> requests.Response:
"""
发送请求并获取Response

:return: Response
"""
self._package_data()
headers = {"Content-Type": "application/x-www-form-urlencoded"}
return requests.post(BAIDU_URL, params=self._payload, headers=headers)

def translate(self, q: str, mode: str) -> str:
"""
翻译

:param q: 待翻译文本
:param mode: 翻译语言模式,en2zh或zh2en
:return: 翻译结果
"""
if not q:
return "q is empty!"
if len(q) > MAX_LENGTH:
return "q is too long!"
self.q = q
self._set_trs_mode(mode)
response = self._do_request()
trans_result = json.loads(response.content)
# TODO 获取翻译结果
return trans_result


if __name__ == "__main__":
KEY_FILE = "./KeySecret.json"
translator = BaiduTranslator()
print(
translator.translate(
"So we beat on, boats against the current, borne back ceaselessly into the past.",
"en2zh",
)
)
print(translator.translate("一个人只拥有此生此世是不够的,他还应该拥有诗意的世界。", "zh2en"))

彩云小译

官方文档:彩云小译API文档

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# CaiYun.py
import json

import requests

CAIYUN_URL = "http://api.interpreter.caiyunai.com/v1/translator"
KEY_FILE = "MachineTranslation/KeySecret.json" # 存储key与secret的json文件路径
MAX_LENGTH = 1500 # 限制翻译输入的最大长度


def load_token(token_file: str) -> str:
"""
读取json文件中保存的API token

:param token_file: 存储key的json文件
:return: token
"""
with open(token_file, "r", encoding="utf-8") as f:
data = json.loads(f.read())["CaiYun"]
token = data["Token"]
return token


class CaiYunTranslator:
"""
调用彩云小译API实现机器翻译
"""

def __init__(self):
self.q = "" # 待翻译内容
self._payload = {}
self._token = load_token(KEY_FILE)

def _package_data(self) -> None:
"""
设置接口调用参数

:return: None
"""
self._payload["source"] = self.q
self._payload["request_id"] = "demo"
self._payload["detect"] = True

def _set_trs_mode(self, mode: str) -> None:
"""
设置翻译语言模式

:param mode: 语言模式,en2zh或zh2en
:return: None
"""
if mode in {"en2zh", "zh2en"}:
self._payload["trans_type"] = mode
else:
self._payload["trans_type"] = "auto2zh"

def translate(self, q: str, mode: str) -> str:
"""
翻译

:param q: 待翻译文本
:param mode: 翻译语言模式,en2zh或zh2en
:return: 翻译结果
"""
if not q:
return "q is empty!"
if len(q) > MAX_LENGTH:
return "q is too long!"

self.q = q
self._set_trs_mode(mode)
self._package_data()
headers = {
"content-type": "application/json",
"x-authorization": "token " + self._token,
}
response = requests.request(
"POST", CAIYUN_URL, data=json.dumps(self._payload), headers=headers
)
return json.loads(response.text)["target"]


if __name__ == "__main__":
KEY_FILE = "./KeySecret.json"
translator = CaiYunTranslator()
print(
translator.translate(
"So we beat on, boats against the current, borne back ceaselessly into the past.",
"en2zh",
)
)
print(translator.translate("人生苦短,我用Python。", "zh2en"))

扩展阅读

一段关于面向对象编程的探讨 - invalid s

Python中下划线的5种含义


Python调用有道、百度、彩云API实现自然语言翻译
https://muzing.top/posts/fb89ce78/
作者
Muzing
发布于
2022年3月22日
更新于
2022年3月31日
许可协议