目标实现
通过调用第三方网站的 API,在终端输入单词,可以查询到单词的音标、翻译等信息并打印出来,在该案例中主要学习如何发送 HTTP 请求以及解析 JSON,通过代码生成提高开发效率。
具体过程
抓包
以彩云科技提供的在线翻译为例,打开彩云翻译的网页( fanyi.caiyunapp.com/#/ ),单击 F12(笔记本则需提前按一次 Fn),调出开发者工具
确保开发者工具上方的选项栏选中的是 Network 选项,然后在网页翻译的左框中输入文字,点击翻译,于是在开发者工具中便可以监测到浏览器发送的一系列请求。在开发者工具的请求列表中找到名为 dict 的请求,选中后看其 Header 的 Request method 是否为 POST,若是则说明寻找的请求是正确的。
除此之外,在 Payload 中可以查到请求头是一个具有两个返回值的 JSON,source 代表所查询的单词,trans_type 代表语言的转换(不过在我自己的浏览器里没有 Payload 这个选项,可能是内核版本太低了吧),在 Preview 中就可看到 API 的返回结果,分别是 dictionary 和 wiki,我们需要用到的结果主要在 dictionary.explanations 里。
代码生成
接下来需要在 Golang 里发送这个请求,但是用代码构造太麻烦了,所以这里建议直接用开发者工具的内置功能———将请求以 cURL 的方式复制下来,我这里需要右击之前选中的 dict 文件,选择 Copy - Copy as cURL (bash),就得到了该请求的终端命令,返回的是一堆 JSON。
但是有了发送请求的 bash 命令还是不够的,我们的目的是要在 Golang 里发送这个请求,因此还需把它转化为 Go 代码。借助代码转换平台( curlconverter.com/go/ )将上述命令转换为 Go 代码,然后直接将转换完成后的 Go 代码 copy 下来即可。
代码生成解读
Golang 通过使用 HTTP 客户端库调用网页 API。这些库包括内置的 net/http 包以及其他第三方库,如 gorilla/http 和 go-resty。
调用网页 API 的原理如下:
-
创建 HTTP 请求:使用
http.NewRequest函数创建一个 HTTP 请求对象。该函数接受请求方法(GET、POST 等)、URL 以及可选的请求体数据作为参数,并返回一个http.Request对象。 -
设置请求头部:对于某些需要特定头部信息的 API,你可以使用
req.Header.Set方法设置请求头部字段,例如设置认证信息、内容类型、用户代理等。 -
发送请求:使用
http.Client的Do方法发送 HTTP 请求。Do方法接收一个http.Request对象,并返回一个http.Response对象,其中包含了响应的状态码、头部信息和响应体数据。 -
处理响应:你可以通过访问
http.Response对象的属性来获取响应的状态码、头部信息以及响应体数据。你可以使用ioutil.ReadAll函数读取响应体数据,并根据需要进行处理。 -
关闭响应:在完成对响应的处理后,记得关闭响应体数据流,以释放相关的系统资源。你可以调用
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 里的值复制过去即可。
本质上,这一步又是上一步的改进,将返回的 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 是这样的:
看起来一切正常,但 Request Body 里的数据却是:
无法解码就不能实现查词,虽然没有成功,但是对抓包流程还是增进了一定的了解,至少知道请求和响应是要分开来看的,不能想当然地从响应倒推回请求。
本来还想着退而求其次尝试不同语种翻译的抓包,再尝试并行的,结果发现和英译汉抓到的包完全不一样(不会举一反三说的就是我了呜呜呜),只好无奈放弃了,接着往下学吧,以后再回过来开坑。