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

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

需求

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

目录结构

目录结构如下:

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 中代码如下:

# __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 包时的代码如下:

# 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 中有很好的提示。

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

模块代码全文如下:

# 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"))

扩展阅读

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

Python中下划线的5种含义

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值