Go 语言实践经验|青训营

31 阅读4分钟

Go 语言实践经验|青训营

Go语言工程实践的学习与踩坑笔记

实践题目:简单词典的实现

本次实践的目的是调用网页翻译API实现简单的命令行翻译功能

一、分析网页API

按F12打开浏览器控制台,使用彩云小译翻译,并点击网络观察翻译过程。

image-20230728212829069.png

发现在dict出现了翻译的结果并在负载处查看翻译请求的参数。

image-20230728213155687.png image-20230728213046337.png

二、自动构建Go语言爬虫

右键点击dict,选择按如下模式进行复制 image-20230728213327478.png

并在Convert curl to Go (curlconverter.com)将命令转化为Go语言爬虫,转化结果如下所示

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
)

func main() {
	client := &http.Client{}
	var data = strings.NewReader(`from=auto&to=zh-CHS&client=web&text=nice&uuid=be4d6ff4-e791-4b0b-bf51-8fb619ddc8f9&pid=sogou-dict-vr&addSugg=on`)
	req, err := http.NewRequest("POST", "https://fanyi.sogou.com/reventondc/suggV3", data)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
	req.Header.Set("Connection", "keep-alive")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("Cookie", "ABTEST=2|1690549097|v17; SNUID=BB6253B8282F29A28B4E7D9129DC051E; SUID=934A7B908586A20A0000000064C3BB69; wuid=1690549097022; FQV=b49fef34c820cf843d5821336e8bdfcb; translate.sess=4ad9d4d4-d72a-48cc-b192-40fd4b64b406; SUV=1690549097752; SGINPUT_UPSCREEN=1690549097771")
	req.Header.Set("Origin", "https://fanyi.sogou.com")
	req.Header.Set("Referer", "https://fanyi.sogou.com/text?keyword=no&transfrom=auto&transto=zh-CHS&model=general")
	req.Header.Set("Sec-Fetch-Dest", "empty")
	req.Header.Set("Sec-Fetch-Mode", "cors")
	req.Header.Set("Sec-Fetch-Site", "same-origin")
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.183")
	req.Header.Set("sec-ch-ua", `"Not/A)Brand";v="99", "Microsoft Edge";v="115", "Chromium";v="115"`)
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("sec-ch-ua-platform", `"Windows"`)
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", bodyText)
}

三、修改代码

由负载与预览处可知,请求参数与返回结果为JSON格式,因此建立以下struct

type DictRequest struct {
	TransType string `json:"trans_type"`
	Source    string `json:"source"`
	UserID    string `json:"user_id"`
}

可以直接复制JSON内容在Goland内自动生成struct

type DictResponse struct {
	Rc   int `json:"rc"`
	Wiki struct {
		KnownInLaguages int `json:"known_in_laguages"`
		Description     struct {
			Source string      `json:"source"`
			Target interface{} `json:"target"`
		} `json:"description"`
		ID   string `json:"id"`
		Item struct {
			Source string `json:"source"`
			Target string `json:"target"`
		} `json:"item"`
		ImageURL  string `json:"image_url"`
		IsSubject string `json:"is_subject"`
		Sitelink  string `json:"sitelink"`
	} `json:"wiki"`
	Dictionary struct {
		Prons struct {
			EnUs string `json:"en-us"`
			En   string `json:"en"`
		} `json:"prons"`
		Explanations []string      `json:"explanations"`
		Synonym      []string      `json:"synonym"`
		Antonym      []string      `json:"antonym"`
		WqxExample   [][]string    `json:"wqx_example"`
		Entry        string        `json:"entry"`
		Type         string        `json:"type"`
		Related      []interface{} `json:"related"`
		Source       string        `json:"source"`
	} `json:"dictionary"`
}

添加以下代码实现对返回内容的解析

	if resp.StatusCode != 200 {//判断是否返回成功
		log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
	}
	var dictResponse DictResponse//实例化一个DictResponse类型用于存储数据
	err = json.Unmarshal(bodyText, &dictResponse)//对返回的内容实现反序列化
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("彩云翻译")
	fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)//将翻译的内容打印出来
	for _, item := range dictResponse.Dictionary.Explanations {
		fmt.Println(item)
	}

这样,就完成了对彩云小译API的调用。

四、多API调用与并发

在实现彩云小译后,依照相同的方法实现了火山翻译与搜狗翻译的调用,并使用sync库实现了多线程的调用。完整的文件结构如下图所示。 image-20230728214835878.png 最终结果如下图 image-20230728215006031.png

五、补充知识与踩坑记录

补充知识:

序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。

因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则**(序列化),那么我们从IO流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。**

踩坑记录

为了实现模块化,我将三种API转换为三个包,在调用函数时传入参数 var wg sync.WaitGroup已实现多线程。如果函数在同一个包下,不存在问题。但是转换为三个包并进行调用时出现了线程的死锁,原因是,也就是wg.Done()没起作用,原来是wg 给拷贝传递到了 goroutine 中,导致只有 Add 操作,其实 Done操作是在 wg 的副本执行导致的。将wg 的传入类型改为 *sync.WaitGrou,这样就能引用到正确的WaitGroup了。

func SougouQuery(word string, wg sync.WaitGroup)     ×
func SougouQuery(word string, wg *sync.WaitGroup)

调用时

go SougoApi.SougouQuery(word, &wg)