Godict
近期一直再使用golang语言开发一些工具,相关的后端技术链(golang+orm+postgresql+gin+jwt+logrus)和对应前端的技术链(vue+iview+axios+vue-router)基本已经打通了,项目地址。但是想到除了这一套前后端的东西外,命令行的一些操作也是不可避免的。因此就找到了cobra这个应用广泛的第三方命令行库,并借这个小项目练一下手。
功能
golang天然的带有网络操作的优势,所以直接借用现有的第三方api服务来做一个实用的小工具。首先想到的就是词典翻译,因为这个工具我之前在学习python时就做过一个。
既然需要做一个golang版本的有道词典(前期只考虑命令行),那么第一步就需要有翻译接口。
接口获取有两种途径:
-
模拟网页请求,然后解析html文本抓取其中的有效结果数据。这就是所谓的爬虫了。
-
使用官方指定的api调用接口直接获取数据。
第一种方法方法简单粗暴,没啥限制,但是由于时爬虫解析整个网页,如果网页结构变化了,就容易失效,而且效率也相对较低,毕竟要从一大堆数据中找出一点点有用的东西出来。
第二种方法是官方提供的接口,所以基本是长期有效的,相对很稳定,返回的数据就是json数据,比较简洁,没有多余的无用内容,方便解析。但是每天的访问次数有一定限制。不过个人使用也够用了。
综上所述,我们选择第二种方式来实现。
所以第一个需要使用的库就是golang官方的net/http库了。
有道智云API
网易提供了现有的api,这个api需要先注册,然后获取一个应用的key,同时会生成一个应用的密钥,此处我把这两个东西用appKey和appSecret来表示。至于怎么申请,官方流程会说的很详细
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fRMKkW1f-1580622385490)( https://raw.githubusercontent.com/qiuzhiqian/Godict/master/doc/img/image_1.png)]
API使用方式
api的使用方式需要参考有道智云的官方文档
从文档上我们获取到一下信息:
文本翻译接口地址: https://openapi.youdao.com/api
协议:
规则 | 描述 |
---|---|
传输方式 | HTTPS |
请求方式 | GET/POST |
字符编码 | 统一使用UTF-8 编码 |
请求格式 | 表单 |
响应格式 | JSON |
表单中的参数:
字段名 | 类型 | 含义 | 必填 | 备注 |
---|---|---|---|---|
q | text | 待翻译文本 | True | 必须是UTF-8编码 |
from | text | 源语言 | True | 参考下方 支持语言 (可设置为auto) |
to | text | 目标语言 | True | 参考下方 支持语言 (可设置为auto) |
appKey | text | 应用ID | True | 可在 应用管理 查看 |
salt | text | UUID | True | UUID |
sign | text | 签名 | True | sha256(应用ID+input+salt+curtime+应用密钥) |
signType | text | 签名类型 | True | v3 |
curtime | text | 当前UTC时间戳(秒) | true | TimeStamp |
ext | text | 翻译结果音频格式,支持mp3 | false | mp3 |
voice | text | 翻译结果发音选择 | false | 0为女声,1为男声。默认为女声 |
签名生成方法如下:
signType=v3;
sign=sha256(应用ID
+input
+salt
+curtime
+应用密钥
);
其中,input的计算方式为:input
=q前10个字符
+q长度
+q后10个字符
(当q长度大于20)或input
=q字符串
(当q长度小于等于20);
好了,到了这一步基本的一些操作信息就都有了
我们来逐个分析一下参数:
q
就是需要翻译的文本
from to
就是从什么语言翻译成什么语言,对应语言的格式官方文档有详细列表。
appKey
这个就是上面提到的在有道智云申请的弄个东西了。
salt
是一个uuid,所以我们需要一个用来生成uuid的库,golang有第三方uuid库。
sign
这个就是最关键的东西,前面,这个需要根据前面的这些信息来计算出来,计算公式上面提到了。
signType
这个固定位v3就行
准备数据
通过上面的分析,可以知道我们需要准备一下数据:
待翻译的单词(word),uuid,源语言(fromLan),目标语言(toLan),appKey,appSecret,sign,signType
我们先考虑一下整个app的工作流程:
鉴于appKey和appSecret是比较私密的东西,所以应该放到配置文件中来让用户配置自己对应的appKey和appSecret,而不应该把这两部分在程序中写死。所以我们需要加载一个配置文件,暂定为应用同级目录下的config.json。
带翻译的单词、源语言和目标语言这个应该是由用户来输入的,所以需要有一个命令行传参,我们借用cobra。
uuid需要在应用程序内实时生成
sign需要根据已知变量来计算,signType固定值
cobra
cobra是一个构建命令行工具的库,我们先大致描述一下我们需要的命令结构,首先word是必须的,还要附加两个标志(flag):from和to。
所以大概就是这个样子:
$ ./appname word --from en --to zh-CSH
或者简写成
$ ./appname word -f en -t zh-CSH
cobra中的命令组织方式是一个树状的方式,首先有一个根命令,根命令中添加若干个子命令,然后每个子命令又可以添加自己的子命令。
所处cobra中,最基本的单元就是命令(cobra.Command),命令之间可以添加父子关系,最后组织成一个命令树。
每个命令有基本的5个成员:
- Use 用来描述命令的使用方式
- Short 命令的简短帮助信息
- Long 命令完整的帮助信息
- Args 命令的参数约束
- Run 命令匹配成功后执行的函数体
很显然,我们这个命令工具暂时用不到子命令,所以我们直接使用一个根命令即可。
var rootCmd = &cobra.Command{
Use: "app {word}", Short: "translate words",
Long: `translate words to other language by cmdline`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
//do something
fmt.Println("Arg:", strings.Join(args, " "))
fmt.Println("translate:", "from", fromLan, "to", toLan)
}}
根命令按照上面的定义即可。
还有两个flag需要添加到rootCmd上面,这两个选项是以kv键值对形式存在的,可以省略,所以需要提供一个默认值。根据有道智云的文档可以看到,翻译语言可以自动识别,所以我们只需要默认设置成auto即可
rootCmd.Flags().StringVarP(&fromLan, "from", "f", "auto", "translate from this language")
rootCmd.Flags().StringVarP(&toLan, "to", "t", "auto", "translate to this language")
此处我们需要定义两个字符串变量来接收这两个flag的值
var fromLan string
var toLan string
然后只需要将这个命令运行起来即可
rootCmd.Execute()
完整代码:
package main
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
var fromLan string
var toLan string
func main() {
var rootCmd = &cobra.Command{
Use: "app {word}", Short: "translate words",
Long: `translate words to other language by cmdline`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Arg:", strings.Join(args, " "))
fmt.Println("translate:", "from", fromLan, "to", toLan)
}}
rootCmd.Flags().StringVarP(&fromLan, "from", "f", "auto", "translate from this language")
rootCmd.Flags().StringVarP(&toLan, "to", "t", "auto", "translate to this language")
rootCmd.Execute()
}
PS E:\code\code_go\Godict\test> go build
PS E:\code\code_go\Godict\test> ./test nice --from en --to zh-CSH
Arg: nice
translate: from en to zh-CSH
加载config.json配置
由于golang自带有json编解码库,所以我们使用json格式的配置文件。
如前面所述,配置文件需要加载appKey和appSecret两个参数,因此定义如下:
{
"appKey":"your app key",
"appSecret":"your app secret code"
}
json在golang中,使用tag来指定json与结构体的映射
type Config struct {
AppKey string `json:"appKey"`
AppSecret string `json:"appSecret"`
}
json是一个文本文件,所以我们首先需要把文件中的内容读取出来
fileobj, err := os.Open(str)
if err != nil {
return err
}
defer fileobj.Close()
var fileContext []byte
fileContext, err = ioutil.ReadAll(fileobj)
然后将读取出来的内容使用json.Unmarshal函数解析
json.Unmarshal(fileContext, cfg)
我们将此部分代码定义成一个函数方便调用:
func InitConfig(str string, cfg *Config) error {
fileobj, err := os.Open(str)
if err != nil {
return err
}
defer fileobj.Close()
var fileContext []byte
fileContext, err = ioutil.ReadAll(fileobj)
json.Unmarshal(fileContext, cfg)
return nil
}
此处函数需要传入config.json文件的路径和解析成功后保存数据的Config变量指针。我们此处规定加载应用同级目录下的config.json。所以我们需要能获取应用程序的绝对路径,此处使用绝对路径是为了保证config.json一定能获取到。
绝对路径可以使用一下方式获取:
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
整理成一个函数方便调用:
func GetCurrentDirectory() string {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
log.Fatal(err)
return ""
}
return strings.Replace(dir, "\\", "/", -1) //将\替换成/
}
上述完整代码和测试:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
)
type Config struct {
AppKey string `json:"appKey"`
AppSecret string `json:"appSecret"`
}
var config Config
var fromLan string
var toLan string
func main() {
var rootCmd = &cobra.Command{
Use: "app {word}", Short: "translate words",
Long: `translate words to other language by cmdline`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println