最近做的某个项目中需要用到中英文之间的翻译,故使用 Python 编写 MachineTranslation
包,调用有道智云、彩云小译、百度的自然语言通用翻译 API。
需求
在一个 Python 包(具有 __init__.py
的目录)中实现对三种 API 的调用,且调用方式统一。出于安全与便于编辑考虑,将所有 key/token 保存至一个 json 文件中。
目录结构
目录结构如下:
MachineTranslation
│
├── __init__.py
├── CaiYun.py
├── Baidu.py
├── YouDao.py
└── KeySecret.json
其中 CaiYun.py
、Baidu.py
、YouDao.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
中代码如下:
# __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-8
与 UTF-8
等别名的处理)。当然,得益于布尔运算中的短路运算,理论上调用时使用 or
左侧准确的名称时会有微小的速度优势。
将 import 语句写到条件分支中,只有调用该服务商时才 import 对应代码。若用户只调用一两个服务商的 Translator 类,可略微减小参与运行的代码量。
而在外部调用 MachineTranslation
包时的代码如下:
# 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
文件内容如下:
{
"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 中有很好的提示。
模块代码全文如下:
# 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 = "./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__":
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文档
代码:
# 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"
MAX_LENGTH = 1800 # 限制翻译输入的最大长度
def load_appid(appid_file: str = "./KeySecret.json") -> 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()
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__":
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文档
代码:
# CaiYun.py
import json
import requests
CAIYUN_URL = "http://api.interpreter.caiyunai.com/v1/translator"
MAX_LENGTH = 1500 # 限制翻译输入的最大长度
def load_token(token_file: str = "MachineTranslation/KeySecret.json") -> 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("./KeySecret.json")
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__":
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"))