「伴学Go With Me」Go语言入门篇-实战入门-SimpleDict在线词典 | 青训营笔记

75 阅读6分钟

这是我参与「第五届青训营」笔记创作活动的第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.项目坑汇总

  1. Json.Marshal后为空:

     请确保结构体内设置了Json标签,`json:x`
     检查结构体是否公有(首字母大写)
     检查结构体内字段是否公有
    
  2. $http.Client{}.Do(request)所请求的Response流在访问完成后要记得Close。

  3. ioutil.Readall()返回的是[]byte类型,http.NewRequest(method string, url string, body io.Reader)中的body是属于io.Reader接口类型,可以接收流式数据。

  4. 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

6.资料参考

books.studygolang.com/