最近做的某个项目中需要用到中英文之间的翻译,故使用 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.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
中代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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
包时的代码如下:
1 2 3 4 5 6 7 from MachineTranslation import machine_translator m_translator = machine_translator("YouDao" ) 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 中有很好的提示。
模块代码全文如下:
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 import hashlibimport timeimport uuidfrom json import loads as json_loadsimport requests YOUDAO_URL = "https://openapi.youdao.com/api" KEY_FILE = "MachineTranslation/KeySecret.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_secretclass 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["strict" ] = "true" 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" : 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 : error_code = json_loads(response.content)["errorCode" ] 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_resultif __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 import jsonfrom hashlib import md5from random import randintimport requests BAIDU_URL = "https://fanyi-api.baidu.com/api/trans/vip/translate" KEY_FILE = "MachineTranslation/KeySecret.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_keyclass 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) return trans_resultif __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 import jsonimport requests CAIYUN_URL = "http://api.interpreter.caiyunai.com/v1/translator" KEY_FILE = "MachineTranslation/KeySecret.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 tokenclass 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种含义