yugasun
Published on

如何基于腾讯云快速开发免费的翻译工具

Authors
  • avatar
    Name
    Yuga Sun
    Twitter

by yugasun from https://yugasun.com/post/serverless-practice-dict.html 本文可全文转载,但需要保留原作者和出处。

背景

作为一名程序员,日常工作和学习中,我们会接触到各种英文文档和代码,因此英文基础是不可或缺的。但是我们脑海中的英文词汇是有限的,总会碰到一些不认识的单词,因此一个好的翻译软件就显得尤为重要。由于每次点开翻译软件,然后再输入陌生单词,获得答案的操作,总觉得太繁琐,而且大多数时候我们只需要一个简单的翻译就行,并不需要翻译软件列出的一大堆翻译解释。因此,开发一款简单的翻译工具的念头应运而生。

思考

要开发一款翻译工具,第一反应就是想到使用现成的翻译接口,只需要我本地写几行脚本调用,可处理下数据就行了,同时我又想把它做成一个简单的服务,可以通过简单的 query api 的方式,就可以获得翻译,ok,现在需求很明确了:

0. 一个免费的翻译接口
1. 我需要一个简单的翻译服务,它的使用频率很低
2. 同时这个服务最好在我只有翻译需求时才运行
3. 我是个穷B,我不想花钱

写下第三条时,我默默地擦拭掉了眼角的泪水,可能年纪大了,容易进沙子......

准备

不知道为什么,我的脑海中第一个闪现的就是 云函数,因为他满足了上面提到的所有需求,关键是她免费,免费,免费....... 重要的是事情说三遍。当然免费是有限度的,对于这种小工具来说,已经够了。作为一名合格的撸羊毛党,对于免费的服务发现和洞察能力,是一门基本修养。

于是三下五除二就注册了一个腾讯云账号,顺势就开通了云函数服务。

接下来就是翻译接口了,在腾讯云平台搜了下,正好有腾讯云机器翻译的文本翻译接口正好可以满足需求,重点是它 每月有5百万字符 的免费额度,简直是我等屌丝的福音......

还有一个很重要的步骤就是 创建API密钥,因为之后无论是 请求云API请求 还是 scf 命令行工具部署 都需要 API密钥,只需要到腾讯云控制台 创建 API 密钥 就行。

鉴权开发

接下来就是正式开发了。腾讯云机器翻译的接口鉴权有两种签名算法:一种是简单的 HMAC-SHA1/SHA256算法,另一种则是相对复杂的 TC3-HMAC-SHA256 算法。官方给出的解释是:

TC3-HMAC-SHA256 签名方法相比以前的 HmacSHA1 和 HmacSHA256 签名方法,功能上覆盖了以前的签名方法,而且更安全,支持更大的请求,支持 json 格式,性能有一定提升,建议使用该签名方法计算签名。

考虑到以后的扩展性(作为一名喜欢装 x 的程序员),毅然选择了第二种鉴权算法。可是官方文档并未给出 Javascript 的实现版本,于是自己花时间用 Typescript 手写了这个签名算法,整体还是没有什么难度的,只需要按照 官方文档,一步一步实现就好。

核心代码如下:

// 1. create Canonical request string
const HTTPRequestMethod = (options.method || 'POST').toUpperCase()
const CanonicalURI = '/'
const CanonicalQueryString = ''
const CanonicalHeaders = `content-type:application/json\nhost:${Host}\n`
const SignedHeaders = 'content-type;host'
const HashedRequestPayload = crypto
  .createHash('sha256')
  .update(JSON.stringify(payload))
  .digest('hex')
const CanonicalRequest = `${HTTPRequestMethod}\n${CanonicalURI}\n${CanonicalQueryString}\n${CanonicalHeaders}\n${SignedHeaders}\n${HashedRequestPayload}`

// 2. create string to sign
const CredentialScope = `${date}/${options.ServiceType}/tc3_request`
const HashedCanonicalRequest = crypto.createHash('sha256').update(CanonicalRequest).digest('hex')
const StringToSign = `${Algorithm}\n${Timestamp}\n${CredentialScope}\n${HashedCanonicalRequest}`

// 3. calculate signature
const SecretDate = sign(date, Buffer.from(`TC3${options.SecretKey}`, 'utf8'))
const SecretService = sign(options.ServiceType, SecretDate)
const SecretSigning = sign('tc3_request', SecretService)
const Signature = crypto
  .createHmac('sha256', SecretSigning)
  .update(Buffer.from(StringToSign, 'utf8'))
  .digest('hex')

// 4. create authorization
const Authorization = `${Algorithm} Credential=${options.SecretId}/${CredentialScope}, SignedHeaders=${SignedHeaders}, Signature=${Signature}`

这里奉上 源代码

搞定了最复杂的签名算法,接下来就是 云函数 的开发了。

云函数创建

创建一个 云函数,参考官方文档 使用控制台创建函数,模板选择 Nodejs8.9 运行环境,创建成功后,在线编辑函数代码就行。当然你也可以本地创建,参考这个代码模板 https://github.com/yugasun/tencent-serverless-demo/tree/master/dict, 然后根据个人需求修改 template.yaml 文件就行。

如果你是本地开发的函数,需要部署到线上,就需要用到 SCF 命令行工具 了,更具文档安装下就行。(当然云函数的部署,还有其他很多种,待大家自己去探索了)

业务开发

接下来就是编写业务逻辑了,本来代码中应该包含了鉴权和标准云 API 请求的代码,但是考虑到以后可能还会再次使用,于是将腾讯云相关的 API 请求代码封装成了 tss-capi 模块。然后重构后的函数代码就变得简洁很多:

const Dotenv = require('dotenv')
const { Capi } = require('tss-capi')
const path = require('path')

function scfReturn(err, data) {
  return {
    isBase64Encoded: false,
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: { error: err, data: data },
  }
}

exports.main_handler = async (event, context, callback) => {
  const query = event.queryString || {}
  const sourceText = query.q
  if (!sourceText) {
    return scfReturn(new Error('Please set word you want to translate.'), null)
  }
  try {
    const envPath = path.join(__dirname, '.env')
    const { parsed } = Dotenv.config({
      path: envPath,
    })

    const client = new Capi({
      Region: 'ap-guangzhou',
      SecretId: parsed.TENCENT_SECRET_ID,
      SecretKey: parsed.TENCENT_SECRET_KEY,
      ServiceType: 'tmt',
      host: 'tmt.tencentcloudapi.com',
    })

    const res = await client.request(
      {
        Action: 'TextTranslate',
        Version: '2018-03-21',
        SourceText: sourceText,
        Source: 'auto',
        Target: 'zh',
        ProjectId: 0,
      },
      {
        host: 'tmt.tencentcloudapi.com',
      }
    )

    const translateText = res.Response && res.Response.TargetText
    return scfReturn(null, translateText)
  } catch (e) {
    return scfReturn(e, null)
  }
}

注意:函数使用了 dotenv 来配置上文提到的 API 密钥,开发中你需要将含有 TENCENT_SECRET_IDTENCENT_SECRET_KEY.env 文件放到项目根目录。

细心的读者可能还会发现,这里的函数返回都是通过 scfReturn 规范化的,这是为什么呢?

这里踩了一个坑,正常情况下,云函数执行结果是可以返回任何结果的,但是由于这里本人在创建 API 网关触发器 时,点击启用了 集成响应,但是当时并没有注意这个功能,就没有理会她,导致接口请求一直报错,也很莫名其妙。通过搜索,发现官方对于 集成响应 的说明:

集成响应,是指 API 网关会将云函数的返回内容进行解析,并根据解析内容构造 HTTP 响应。通过使用集成响应,可以通过带阿米自主控制响应的状态码、headers、body 内容,可以实现非 JSON 格式的内容响应,例如响应 XML、HTML、甚至 JS 内容。在使用集成响应时,需要按照 API 网关触发器的集成响应返回数据结构,才可以被 API 网关成功解析,否则会出现 {"errno":403,"error":"Invalid scf response format. please check your scf response format."} 错误信息。

找到了接口报错的原因了,于是便写了 scfReturn 函数,来规范所有接口返回。

函数部署

借助 SCF 命令行工具,云函数的部署变得相当简单。你只需要在项目根目录下执行 scf deploy 命令,接下来所有的一切事情,命令行就会自动帮你搞定。当然如果需要自动创建 API 网关触发器,还需要在 template.yaml 文件中进行配置,如下:

Resources:
  default:
    Type: TencentCloud::Serverless::Namespace
    dict:
      Type: TencentCloud::Serverless::Function
      Properties:
        CodeUri: ./
        Description: Tencent Machine Translator
        Environment:
          Variables: {}
        Handler: index.main_handler
        Role: QCS_SCFExcuteRole
        MemorySize: 128
        Runtime: Nodejs8.9
        Timeout: 3
        VpcConfig:
          SubnetId: ''
          VpcId: ''
        Events:
          dict:
            Type: APIGW
            Properties:
              StageName: release
              ServiceId: service-7kqwzu92
              HttpMethod: ANY

注意: 配置中的 service-7kqwzu92 是我在 API 网关 创建的服务,需要修改成私人配置。QCS_SCFExcuteRole 是配置的函数运行角色,如果用不到可以直接删除 Role: QCS_SCFExcuteRole 这一行配置。

第一次部署成功后,如果再次运行 scf deploy 会提示 default dict: The function already exists. 函数已经存在的错误,这里就需要进行强制覆盖,将部署命令修改为 scf deploy -f 就好。

最后

终于一个简单免费的云词典开发好了,浏览器访问:http://service-7kqwzu92-1251556596.gz.apigw.tencentcs.com/test/dictt?q=hello ,成功输出翻译结果:

{
  "isBase64Encoded": false,
  "statusCode": 200,
  "headers": {
    "Content-Type": "application/json"
  },
  "body": {
    "error": null,
    "data": "你好"
  }
}

当然,这还并不能满足我作为一名 懒惰 程序员的需求,因为我现在翻译,还需打开浏览器,然后输我要翻译的字符串才行,于是我又在本地写了一个简单的脚本,通过执行终端命令就可以了,最后的运行效果是:

$ dict billionaire
亿万富翁

不知道为啥,看到 亿万富翁 的翻译输出到命令行时,一粒沙子又莫名的飞入了我的眼中:

金钱最大不是只有 100块 吗?尽然还有 亿万 这个单位......

源码

朋友请留步,源码在这里: https://github.com/yugasun/tencent-serverless-demo/tree/master/dict