这是我参与「第五届青训营」笔记创作活动的第2天。书接上文,在完成猜数字的小程序后,第二个项目是利用Go实现一个在线字典,主要记录一下实战入门项目的课程收获和完成课后作业的感想。
SimpleDict在线词典
-
课后作业:添加另外一种翻译引擎支持、并行请求两个翻译引擎来加提高响应。
-
项目分析:该项目主要核心流程是通过Go发起Http请求,分析请求返回的响应并将结果打印到终端上。
graph LR 模拟用户发起请求 --> 解析返回体获取期望结果 --> 打印至终端
1. 模拟用户发起请求
在这我将以sogou翻译引擎作为演示。Go语言内置的net/http提供了HTTP客户端、服务端的实现,这一部分类似于Python在爬虫时的request配置,需要拷贝浏览器的Header参数来将Go标准库net/http所生成的请求流伪装成常见浏览器的样式。
func SendHttp(DictSearch DistRequest) []byte {
client := &http.Client{}
buff, err := json.Marshal(DictSearch) //返回的是byte对象
if err != nil {
log.Fatal(err)
}
data := bytes.NewReader(buff)
req, err := http.NewRequest("POST", "https://fanyi.sogou.com/api/transpc/text/result", data) //data流 string.newreader转换
if err != nil {
log.Fatal(err)
}
req.Header.Set("Accept", "application/json, text/plain, */*")
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/json;charset=UTF-8")
req.Header.Set("Cookie", "ABTEST=7|1674143952|v17; SNUID=4B1835314045B2E043D00870412B0072; IPLOC=CN4452; SUID=0B5875718486A20A0000000063C968D0; wuid=1674143952012; FQV=5ac5db14843fad3d4383e6056e7f00ed; translate.sess=bfeee71e-72c3-4215-9c40-09f726ee3548; SUV=1674143955462; SGINPUT_UPSCREEN=1674143955484")
req.Header.Set("Origin", "https://fanyi.sogou.com")
req.Header.Set("Referer", "https://fanyi.sogou.com/text?keyword=hello%0A&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 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.52")
req.Header.Set("sec-ch-ua", `"Not_A Brand";v="99", "Microsoft Edge";v="109", "Chromium";v="109"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"macOS"`)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close() //结束后关闭respons流
bodyText, err := ioutil.ReadAll(resp.Body) //读取并存放进内存
if err != nil {
log.Fatal(err)
}
return bodyText
}
这里需要注意的是Client是http包提供的一种数据结构,和Transport结构共同构建成一个连接池var httpClient *http.Client供多个处理函数共同使用,http.Client{}可以在初始化时指定多个参数,相关释义如下。
client := &http.Client{
Transport: &http.Transport{ //Transport结构
Proxy: http.ProxyFromEnvironment, //
DialContext: (&net.Dialer{ //创建非加密TCP链接
Timeout: 30 * time.Second, //数据流等待时间
KeepAlive: 30 * time.Second, //链接复用的最长空闲时间
}).DialContext,
MaxIdleConns: MaxIdleConns, //控制所有host总的最大链接数
MaxIdleConnsPerHost: MaxIdleConnsPerHost, //每个host的最大链接数
IdleConnTimeout: time.Duration(IdleConnTimeout) * time.Second,
//空闲最长时间,连接在自行关闭之前将保持当前状态(keep-alive)。
}
2.解析返回体
程序虽然完成了模拟用户发送查询请求的步骤,但返回的通常是未经过处理的源数据,并不能像浏览器一样解析成可视化界面展示给我们。以下是发送hello单词查询后返回的Response.Body,Body是以[]byte类型进行存储,再通过json.Unmarshal()方法进行反序列化存储,从而来实现调用和匹配期望值。
{
"data": {
"translate": {
"qc_type": "1",
"zly": "zly",
"errorCode": "10",
"index": "content0",
"from": "zh-CHS",
"source": "sogou",
"text": "hello",
"to": "en",
"id": "id",
"dit": "x1yukzzrcxf353p33dvm",
"orig_text": "text",
"md5": ""
},
"detect": {
"zly": "zly",
"detect": "en",
"errorCode": "0",
"language": "英语",
"id": "93883570-98c5-11ed-8ea5-b97266b69982",
"text": "hello"
},
"sgtkn": "",
"wordCard": {
"title": false,
"show": false,
"usualDict": "",
"secondQuery": "",
"exchange": "",
"levelList": ""
}
...
在分析返回的Json格式后,找出期望结果的存储位置,而且反序列化后的结果是以结构体形式进行存储。以下是解析函数的代码块,bodyText反序列化后的内容存储在dictresponse结构体变量内(JsonToGo转译生成的结构体),dictresponse.Data.Gaokao.ExamFreqInfo中包含单词的多个释义,通过range可以进行遍历输出。
var dictresponse DictResponse
err := json.Unmarshal(bodyText, &dictresponse)
if err != nil {
log.Fatal(err)
}
fmt.Printf("搜索词汇: %v\n", dictresponse.Data.Gaokao.Word)
//len := len(dictresponse.Data.Gaokao.ExamFreqInfo)
for _, item := range dictresponse.Data.Gaokao.ExamFreqInfo {
fmt.Println(item.Pos + "." + item.Chinese)
}
3.并行请求多个翻译引擎
这里我一开始主要思路是直接开多个Go Routine,然后主线程等回复,但想想不太环保,而且拿到数据后也比较麻烦, 可能处理数据的过程中会出现不知道这个Response是哪个Request的情况,后来我想到了可以试下Channel缓冲区,类似队列的结构,先完成的Response入缓冲区,主线程再从缓冲区取先拿到的Response,流程实现如下:
func ParallelsQuery(DictSearch DistRequest) {
bodyTextsrc := make(chan []byte, 3)
go func() {
defer close(bodyTextsrc)
bodyTextsrc <- QuerySougo(DictSearch)
bodyTextsrc <- QueryCaiyun(DictSearch)
}()
bodyText := <- bodyTextsrc //取最先获取到的结果出来输出
return bodyText
}
由于不同站点的翻译结果返回的Response不一致,而不同站点的bodyText反序列化后存放的结构体参数要求是不同的,因此需要重新统一下返回体最终处理的结构体类型,我的想法是在Query函数内先对响应体进行UnMarshal反序列化操作,取出单词释义放置到统一的结构体中再进行Marshal序列化,从而保证输出结果的类型一致性。
type unified struct {
word string
wordInfo []struct{
pos string
chinese string
}
}
我这应该不是此问题的最佳解,不过姑且是可以实现简单的并发请求,并实现结果择先取用的效果。希望有大佬能交流下这个问题的更优解。
4.项目坑汇总
-
Json.Marshal后为空:
请确保结构体内设置了Json标签,`json:x` 检查结构体是否公有(首字母大写) 检查结构体内字段是否公有 -
$http.Client{}.Do(request)所请求的Response流在访问完成后要记得Close。
-
ioutil.Readall()返回的是[]byte类型,http.NewRequest(method string, url string, body io.Reader)中的body是属于io.Reader接口类型,可以接收流式数据。
-
var resp *http.Response中的结构体如下
type Response struct {
Status string
StatusCode int
Proto string
ProtoMajor int
ProtoMinor int
Header Header
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Uncompressed bool
Trailer Header
Request *Request
TLS *tls.ConnectionState
}
Body是属于io复合接口类型,包含了Read和Close的方法,其内容需要通过ioutil.Readall()来读取。ioutil.Readall()返回的是[]byte类型。
5.总结
经过几天的课程学习和项目实战,对Go常用的库函数有了进一步的认知,并了解遇到相关函数不同参数需要转换情况下的解决方法。在这个过程中结合课程引导,多查查Go手册还是蛮有收获的。接着努力吧,今儿年三十了,除夕快乐~
- 青训营笔记Day1
- 青训营笔记Day2