Go语言实践案例之在线词典 | 青训营

863 阅读10分钟

目标实现

通过调用第三方网站的 API,在终端输入单词,可以查询到单词的音标、翻译等信息并打印出来,在该案例中主要学习如何发送 HTTP 请求以及解析 JSON,通过代码生成提高开发效率。

具体过程

抓包

以彩云科技提供的在线翻译为例,打开彩云翻译的网页( fanyi.caiyunapp.com/#/ ),单击 F12(笔记本则需提前按一次 Fn),调出开发者工具

image.png

确保开发者工具上方的选项栏选中的是 Network 选项,然后在网页翻译的左框中输入文字,点击翻译,于是在开发者工具中便可以监测到浏览器发送的一系列请求。在开发者工具的请求列表中找到名为 dict 的请求,选中后看其 Header 的 Request method 是否为 POST,若是则说明寻找的请求是正确的。

image.png

除此之外,在 Payload 中可以查到请求头是一个具有两个返回值的 JSON,source 代表所查询的单词,trans_type 代表语言的转换(不过在我自己的浏览器里没有 Payload 这个选项,可能是内核版本太低了吧),在 Preview 中就可看到 API 的返回结果,分别是 dictionary 和 wiki,我们需要用到的结果主要在 dictionary.explanations 里。

image.png

代码生成

接下来需要在 Golang 里发送这个请求,但是用代码构造太麻烦了,所以这里建议直接用开发者工具的内置功能———将请求以 cURL 的方式复制下来,我这里需要右击之前选中的 dict 文件,选择 Copy - Copy as cURL (bash),就得到了该请求的终端命令,返回的是一堆 JSON。

image.png

image.png

但是有了发送请求的 bash 命令还是不够的,我们的目的是要在 Golang 里发送这个请求,因此还需把它转化为 Go 代码。借助代码转换平台( curlconverter.com/go/ )将上述命令转换为 Go 代码,然后直接将转换完成后的 Go 代码 copy 下来即可。

image.png

代码生成解读

Golang 通过使用 HTTP 客户端库调用网页 API。这些库包括内置的 net/http 包以及其他第三方库,如 gorilla/httpgo-resty

调用网页 API 的原理如下:

  1. 创建 HTTP 请求:使用 http.NewRequest 函数创建一个 HTTP 请求对象。该函数接受请求方法(GET、POST 等)、URL 以及可选的请求体数据作为参数,并返回一个 http.Request 对象。

  2. 设置请求头部:对于某些需要特定头部信息的 API,你可以使用 req.Header.Set 方法设置请求头部字段,例如设置认证信息、内容类型、用户代理等。

  3. 发送请求:使用 http.ClientDo 方法发送 HTTP 请求。Do 方法接收一个 http.Request 对象,并返回一个 http.Response 对象,其中包含了响应的状态码、头部信息和响应体数据。

  4. 处理响应:你可以通过访问 http.Response 对象的属性来获取响应的状态码、头部信息以及响应体数据。你可以使用 ioutil.ReadAll 函数读取响应体数据,并根据需要进行处理。

  5. 关闭响应:在完成对响应的处理后,记得关闭响应体数据流,以释放相关的系统资源。你可以调用 resp.Body.Close() 来关闭响应体。

在基本了解 Golang 调用网页 API 的原理后,再回去看代码:

  • 首先创建一个名为 client 的指向 http.Client 结构体类型的指针,初始化可以指定很多参数,但这里用空的大括号初始化。http.Client 是 Go 标准库中用于发送 HTTP 请求的结构体类型。它包含了与 HTTP 请求相关的配置和方法,可以用于发送 HTTP 请求并接收响应。

  • 接着使用 http.NewRequest 创建一个 HTTP 请求,该函数接收 3 个参数:

    • method string:表示 HTTP 请求的方法,比如 "GET"、"POST" 等。

    • url string:表示请求的 URL 地址,在这里请求的是 API 的 URL。URL 是统一资源定位符的缩写,用于标识和定位互联网上的资源。URL地址是一个字符串,用来描述一个资源在互联网中的位置。它通常由以下几个部分组成:

      • 协议部分:表示访问资源所使用的协议,例如 HTTP、HTTPS、FTP 等;
      • 主机部分:表示存放资源的主机名或 IP 地址;
      • 端口部分:可选,表示访问主机时使用的端口号,默认值根据协议而定;
      • 路径部分:表示资源在服务器上的具体路径;
      • 查询部分:可选,用于传递参数给服务器端的查询字符串;
      • 锚点部分:可选,表示页面中的特定位置或锚点。

      一个完整的 URL 示例:https://www.example.com:8080/api/data?id=12345#section1 ,其中:

      • 协议部分:https://
      • 主机部分:www.example.com
      • 端口部分::8080
      • 路径部分:/api/data
      • 查询部分:?id=12345
      • 锚点部分:#section1

      URL 地址的作用是唯一标识互联网上的资源,可以通过浏览器或其他网络请求方式访问这些资源。根据不同的协议和资源类型,URL 可以指向网页、图片、视频、文件等各种形式的资源。

    • body io.Reader:表示请求的主体,通常是一个实现了 io.Reader 接口的数据流,这里的请求主体就是实现了 io.Reader 的 JSON 字符串。

  • 再接着使用 req.Header.Set() 设置请求头以满足 API 的要求并确保正确的交互,虽然不是每一个请求头都是有用的,但是请求头的设置确实是十分重要的,原因如下:

    • 身份验证:许多 API 需要身份验证来保护资源的安全性。通过设置请求头,您可以提供令牌、密钥或其他凭据,以证明您有权限访问该 API 的特定资源。
    • 授权:某些 API 可能需要根据用户的权限级别来执行不同的操作。请求头中的信息可以用于授权用户对特定资源进行读取、写入或其他操作的能力。
    • 内容类型:API 可能需要指定请求发送的数据类型。例如,使用 application/json 表示请求主体的格式为 JSON 数据,或者使用 multipart/form-data 表示请求中包含文件上传等。
    • 缓存控制:通过设置请求头中的缓存控制信息,可以指示 API 是否应该缓存响应,以及缓存的有效期等。
    • 跨域资源共享:在跨域请求时,设置请求头中的 CORS 相关信息可以决定是否允许跨域访问,以及允许哪些域进行访问等。
    • 用户代理:通过设置请求头中的用户代理信息,您可以向 API 提供有关请求的客户端应用程序、浏览器或设备的相关信息。

    req.Header.Set() 函数是用于构造请求头的键值对的,通过提供键和值,可以设置请求头的特定字段,用于在发送 HTTP 请求时传递相关信息。

  • 然后使用 client.Do() 发起 HTTP 请求,client.Do() 方法是 http.Client 结构体中的一个方法,用于执行一个 HTTP 请求。client.Do(req) 方法接受一个 http.Request 对象作为参数,表示要执行的 HTTP 请求。它会向指定的 URL 发送请求,并返回一个 http.Response 对象作为响应。

    log.Fatal()log 包中的一个函数,该函数接受一个变参参数 v ,表示要打印的信息。它会将这些信息格式化为字符串,并加上时间、日期、文件名等额外的信息。然后,它将结果写入标准错误输出,并立即调用 os.Exit(1) 终止程序的执行。

    另外,在程序结束后,需要调用 resp.Body.Close() 函数来关闭响应体数据流,以释放相关的系统资源。

  • 最后一步是读取响应,使用 io.ReadAll() 函数,该函数接受一个 io.Reader 对象作为参数,可以是文件、网络连接等等任何实现了 Read([]byte) (int, error) 方法的对象。它会持续从 io.Reader 中读取数据,直到遇到 EOF(文件末尾)或发生错误。然后,它将读取到的数据保存在一个字节切片([]byte)中,并返回给调用者。

生成 Request Body

这一步可以认为是上一阶段代码的改进,之前是直接读入 JSON 字符串,现在是把数据写在结构体里,并对其序列化,再以字节形式读入,结果和之前没差。

解析 Response Body

这一步的意思是说,将浏览器里返回的 API 结构解析成 Golang 中可用的结构体字段,但是直接定义太过复杂,所以可以使用网上的代码生成工具( oktools.net/json2go )进行解析。只需将开发者工具里的 Preview 里的值复制过去即可。

image.png

image.png

本质上,这一步又是上一步的改进,将返回的 JSON 字符串反序列化后以结构体的方式输出,某种意义上这次的改进已经接近最终所要实现的目标了。

打印特定字段

我们的目标只是要单词的音标以及翻译,因此我们只需要关注 JSON 里的 dictionary.prons 和 dictionary.explanation,在结构体中做对应的输出即可:

fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs) //音标
	for _, item := range dictResponse.Dictionary.Explanations { //翻译
		fmt.Println(item)
	}

值得注意的是,这次代码修改使用了 resp.StatusCode 功能,用于获取 HTTP 响应的状态码:

  • 200:表示成功处理了请求。
  • 404:表示请求的资源不存在。
  • 500:表示服务器内部错误。
  • 302:表示临时重定向。

完善代码

最后为了实现单词的输入而非屡次修改代码,于是把上述功能实现用一个函数 query(word string) 封装起来,在主函数里调用,主函数及注释如下:

func main() {
	//正常来说os.Args的长度是参数个数 + 1(程序本身),运行需要在终端传一个参。
	//所以长度不是 2 则有误,返回错误信息,并退出程序,否则调用 query 函数。
	if len(os.Args) != 2 {
		//fmt.Fprintf 用于将内容写入指定的 io.Writer 对象中。
		//os.Stderr 表示标准错误输出流,这里被后面的内容写入,然后输出。
		//``引起来的部分为原始字符串文字,其中的内容会被原样输出。
		fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
		`)
		//立即终止当前的程序并返回一个指定的退出状态码 1。
		//被 os.Exit() 终止的程序不会触发 defer 语句。
		os.Exit(1)
	}
	word := os.Args[1]
	query(word)
}

代码优化

输入方式

主函数部分可可以改为用 fmt.Scanf() 实现:

func main() {
	var word string
	_, err := fmt.Scanf("%v\r\n", &word)
	if err != nil {
		fmt.Println("usage: simpleDict WORD")
		fmt.Println("example: simpleDict hello")
	} else {
		query(word)
	}
}

这样一来就可以在程序运行时输入而非运行前输入了,可以直接使用 vscode 自带的运行功能,更方便调试。

增加另一翻译引擎支持

试了一个晚上,感觉还是不行。。。可能是我的抓包技术还8太行。。。感觉不是所有的翻译引擎的包都像案例里的那么好抓,有的就算抓到了包,用于创建请求的数据(即 Request Body)太长,写出来的结构体非常复杂;还有的会给要查询的单词加一层编码,以 CNKI 翻译为例,查询单词 good,抓到的包里面的 Preview 是这样的:

image.png

看起来一切正常,但 Request Body 里的数据却是:

image.png

无法解码就不能实现查词,虽然没有成功,但是对抓包流程还是增进了一定的了解,至少知道请求和响应是要分开来看的,不能想当然地从响应倒推回请求。

本来还想着退而求其次尝试不同语种翻译的抓包,再尝试并行的,结果发现和英译汉抓到的包完全不一样(不会举一反三说的就是我了呜呜呜),只好无奈放弃了,接着往下学吧,以后再回过来开坑。