Go資深工程師講解(慕課) 005

005 標準庫

http

  • 使用 http 客戶端發送請求
  • 使用 http.Client 控制請求頭
  • 使用 httputil 簡化工作
package main

import (
    "fmt"
    "net/http"
    "net/http/httputil"
)

func main() {
    resp, err := http.Get("https://www.baidu.com")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    s, err := httputil.DumpResponse(resp, true)
    if err!=nil{
        panic(err)
    }
    fmt.Printf("%s\n",s)
}

設置手機瀏覽 set user-agent

package main

import (
    "fmt"
    "net/http"
    "net/http/httputil"
)

func main() {
    request, err := http.NewRequest(http.MethodGet, "https://www.baidu.com", nil)
    request.Header.Add("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Mobile Safari/537.36")

    resp, err := http.DefaultClient.Do(request) //http.Get("https://www.baidu.com")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    s, err := httputil.DumpResponse(resp, true)
    if err != nil {
        panic(err)
    }
      fmt.Printf("%s\n", s)
}

go tool pprof http://localhost:8888/debug/pprof/profile
或是引一個包_ "net/http/pprof"啓動 web 服務 在地址欄里輸入http://localhost:8888/debug/pprof

其它標準庫

  • bufio
  • log
  • encoding/json
  • regexp
  • time (channel 用法)
  • strings/math/rand

看標準庫的文檔
godoc -http :8888 本地啓一個服務
https://studygolang.com/pkgdoc

迷宮算法

廣度優先算法(應用廣泛,綜合性強)
已經發現還沒探索,放在隊列中,探索至終,然後倒著走就最短路徑

guangdu

package main

import (
      "fmt"
      "os"
)

//返回二維數組
func readMaze(filename string) [][]int {
      file, err := os.Open(filename)
      if err != nil {
            panic(err)
      }
      defer file.Close()
      var row, col int
      fmt.Fscanf(file, "%d %d", &row, &col)
      maze := make([][]int, row) // row行
      for i := range maze {
            maze[i] = make([]int, col)
            for j := range maze[i] {
                  fmt.Fscanf(file, "%d", &maze[i][j])
            }
      }
      return maze
}

type point struct {
      i, j int
}

func (p point) add(r point) point {
      return point{p.i + r.i, p.j + r.j}
}

func (p point) at(grid [][]int) (int, bool) { //bool代表是否越界
      if p.i < 0 || p.i >= len(grid) {
            return 0, false
      }
      if p.j < 0 || p.j >= len(grid[p.i]) {
            return 0, false
      }
      return grid[p.i][p.j], true
}

//四個方向 上左下右
var dirs = [4]point{
      {-1, 0},
      {0, -1},
      {1, 0},
      {0, 1},
}

func walk(maze [][]int, start, end point) [][]int {
      steps := make([][]int, len(maze))
      for i := range steps {
            steps[i] = make([]int, len(maze[i]))
      }
      //   隊列
      Q := []point{start}
      //隊列不空才去探索
      for len(Q) > 0 {
            cur := Q[0]
            Q = Q[1:]
            if (cur == end) {
                  break
            }
            for _, dir := range dirs {
                  next := cur.add(dir)
                  //      maze at next is 0
                  // and steps at next is 0 曾經到過
                  //      and next !=start
                  val, ok := next.at(maze)
                  if !ok || val == 1 {
                        continue
                  }
                  val, ok = next.at(steps)
                  if !ok || val != 0 { //走過了
                        continue
                  }
                  if next == start {
                        continue
                  }
                  //      steps 填進去
                  curSteps, _ := cur.at(steps)
                  steps[next.i][next.j] = curSteps + 1
                  Q = append(Q, next)
            }

      }
      return steps

}

func main() {
      maze := readMaze("maze/maze.in")
      for _, row := range maze {
            for _, val := range row {
                  fmt.Printf("%3d ", val)
            }
            fmt.Println()
      }
      fmt.Println()
      steps := walk(maze, point{0, 0}, point{len(maze) - 1, len(maze[0]) - 1})
      for _, row := range steps {
            for _, val := range row {
                  fmt.Printf("%3d", val)
            }
            fmt.Println()
      }
}
// 途經的點可以倒序遍歷一下

簡單爬蟲

  • 通用爬蟲 如 baidu,google
  • 聚焦爬蟲 從互聯網獲取結構化數據
  • go 語言的爬蟲庫/框架
  • henrylee2cn/pholcus
  • gocrawl
  • colly
  • hu17889/go_spider

技術選型,爬蟲的主題(如新聞,博客,社區,我們爬人,QQ 人人,微博,facebook,相親網站,求職網站)

  • ElasticSearch 作為數據存儲
  • Go 語言標準模板庫實現 http 數據展示部分

單任務版爬蟲

獲取並打印所有城市的第一頁用戶的詳細信息
轉碼 go get -g -v golang.org/x/text
自動識別網頁的編碼 go get -g -v golang.org/x/net/html

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"

    "golang.org/x/net/html/charset"
    "golang.org/x/text/encoding"
    "golang.org/x/text/transform"
)

func main() {
    //所有城市第一頁用戶
    resp, err := http.Get("http://www.zhenai.com/zhenghun")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        fmt.Println("Error: ", resp.StatusCode)
        //panic("Status Code:")
        return
    }
    //自動轉碼
    e := determineEncoding(resp.Body)
    utf8Reader := transform.NewReader(resp.Body, e.NewDecoder())
    all, err := ioutil.ReadAll(utf8Reader)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s\n", all)

}

func determineEncoding(r io.Reader) encoding.Encoding {
    bytes, err := bufio.NewReader(r).Peek(1024)
    if err != nil {
        panic(err)
    }
    e, _, _ := charset.DetermineEncoding(bytes, "")
    return e
}

正則表達式

package main

import (
    "fmt"
    "regexp"
)

const text = "My email is ccmouse@gmail.com"

func main() {
    //re := regexp.MustCompile("ccmouse@gmail.com")
    //用反引號,正則的特殊符號不用再轉意了
    re := regexp.MustCompile(`[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+`)
    match := re.FindString(text)
    fmt.Println(match)
}

讀取 json 文件並 parse 成對應的對象

package main

import (
    "encoding/json"
    "fmt"
    "os"

)

type CityObj struct {
    LinkContent string `json:"linkContent"`
    LinkURL string
}

type CityGroup struct {
    CityList []CityObj
    Order string
}

type RtnData struct {
    CityData []CityGroup
}

func main() {
    path,_:=os.Getwd()
    fmt.Println(path)
    //var jObj interface{}
    var jObj RtnData
    file, err := os.Open("./cities.json")
    if err != nil {
        panic(err)
    }
    //file stat
    fi, _ := file.Stat()
    buffer := make([]byte, fi.Size())
    _, err = file.Read(buffer)
    if err != nil {
        panic(err)
    }
    err = json.Unmarshal(buffer, &jObj)
    fmt.Println(jObj.CityData[0].CityList[0])
}

通過正則來爬取數據

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "regexp"

    "golang.org/x/net/html/charset"
    "golang.org/x/text/encoding"
    "golang.org/x/text/encoding/unicode"
    "golang.org/x/text/transform"
)

func main() {
    //所有城市第一頁用戶
    url := "http://www.zhenai.com/zhenghun"
    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        fmt.Println("Error: ", resp.StatusCode)
        panic("Status Code:")

    }
    //自動轉碼
    e := determineEncoding(resp.Body)
    utf8Reader := transform.NewReader(resp.Body, e.NewDecoder())
    all, err := ioutil.ReadAll(utf8Reader)
    if err != nil {
        //panic(err)
        fmt.Printf("%v\n", err)
        return
    }

    //fmt.Printf("%s\n", all)
    printCityList(all)

}

func determineEncoding(r io.Reader) encoding.Encoding {
    bytes, err := bufio.NewReader(r).Peek(1024)
    if err != nil {
        //panic(err)
        return unicode.UTF8
    }
    e, _, _ := charset.DetermineEncoding(bytes, "")
    return e
}

func printCityList(contents []byte) {
    //re := regexp.MustCompile(`<a target="_blank" href="http://www.zhenai.com/zhenghun/[0-9a-z]+"[^>]*>[^<]+</a>`)
    re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/[0-9a-z]+"[^>]*>[^<]+</a>`) // 470個城市
    matches := re.FindAll(contents, -1)
    for _,m:=range matches{
        fmt.Printf("%s\n",m)
    }
    fmt.Printf("Matches found: %d\n",len(matches))
}

single_task

爬取數據時需要注意,請求頭的設置

  1. 聲明 http.Clien{}
  2. 聲明 request,err:=http.NewRequest("GET",url,nil)
  3. request.Header.Add
  4. resp, err := client.Do(request)請求替換http.Get(url)
  5. defer resp.Body.Close()

到此單任務版的 查看engin fetcher model zhenai 處個目錄的內容

併發版

Fetcher 的輸出就是 Parser 的輸入,可以抽成一個模塊

bingfa_worker.jpg

Scheduler 實現 1:所有 Worker 公用一個輸入

見分支simplechen

實現 2 有兩個隊列 request 隊列 和 worker 隊列

bingfa_worker_v2

把 request 放到 request 的隊列中,為每一個 request 創建一個 go routine,然後讓所有的 workder 搶一個 chanel。

其它頁面

重復率很高,抓取一個城市,比如上海

e.Run(engine.Request{
    Url:"http://www.zhenai.com/zhenghun/shanghai",
    ParserFunc: parser.ParseCity,
})

url 去重

  • 哈希表(真接存儲,佔空間--本課使用)
  • 計算 md5 等哈希,再存哈希表
  • 使用 bloom filter 多重哈希結構
  • 使用 redis 等 key-value 存儲系統實現分布式去重

profile 的保存

抽象出 Task 的概念 FetchTask,PersistTask 共用一個 Engine,Scheduler,需要創建 FetchWorker,PersistWorker;本項目中顯得過重。為每個 Item 創建 goroutine,提交給 ItemSaver;ItemSaver 的速度比 Fetcher 快,類似 SimpleScheduler 的方法即可,我們將採用這種方法。

saver

elastic 與 docker

elastic 全文搜索引擎
本教程使用 docker 集成 elastic install doc

docker pull docker.elastic.co/elasticsearch/elasticsearch:6.7.2

 docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 -v /home/soft/ES/config/es1.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /home/soft/ES/data1:/usr/share/elasticsearch/data --name ES01 elasticsearch:5.6.8

 docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9201:9201 -p 9301:9301 -v /home/soft/ES/config/es2.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /home/soft/ES/data2:/usr/share/elasticsearch/data --name ES02 elasticsearch:5.6.8

 docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9202:9202 -p 9302:9302 -v /home/soft/ES/config/es3.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /home/soft/ES/data3:/usr/share/elasticsearch/data --name ES03 elasticsearch:5.6.8

#   本例運行如下
docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 -v /Users/willzhao/Documents/Develop/docker/elasticsearch/6.8/data:/usr/share/elasticsearch/data --name ES_6.8 elasticsearch:6.8.0
# http://localhost:9200/ 可以訪問

比如我們創建 course 庫
添加記錄我們可以用 put 或 post course/1 course/2 添加 json 數據,可以用 get 獲取全部數據,或用 get 加參數course/_search?,
不加 id 要用 post

  • <server>:9200/index/type/id
  • index 相當於 database
  • type 相當於 table
  • 我們不需要預先創建 index 和 type (IK 插件 有時間瞭解一下)
  • 使用 REST 接口
  • PUT/POST 創建/修改數據,使用 post 可以省略 id
  • GET 獲取數據
  • GET <index>/<type>/_search?q='參數'

本課中的 save()使用elasticsearch client
go 社區版
https://gopkg.in/olivere/elastic.v6

elastic_client.jpg

# 先安裝v6(我認為是基礎
go get -v -u github.com/olivere/elastic
# 引這個的包 本例中用的是v6 在查詢全
# 這個是v6的client https://olivere.github.io/elastic/ 請仔細閱讀這個文檔
go get gopkg.in/olivere/elastic.v6

html/template

分布式系統

  • 多個結點
  • 容錯
  • 可擴展性(性能)
  • 固有分布性
  • 消息傳遞
  • 節點具有私有存儲
  • 易於開發
  • 可擴展性(功能)
  • 對比:並行計算
  • 消息傳遞的方法
    • RESt
    • RPC
    • 中間件(messageQ)
  • 使用場景
    • 對外:Rest
    • 模塊內部:RPC
    • 模塊之間:中間件,REST
  • 完成特定需求

分布式架構 vs 微服務架構

分布式:指導節點之間如何通信
微服務:鼓勵按業務劃分模塊

多層架構 vs 微服務架構

微服務架構具有更多的“服務”
微服務通常需要配合自動化測試,部署,服務發現

分部式爬蟲

  • 限流問題
  • 單節點能夠承受的流量有限
  • 將 worker 放到不同的節點
  • 去重問題
  • 分布式去重將去重放到 worker 上
  • 數據存儲問題

jsonRPC

package rpcdemo

import "errors"

// Service.Method

type DemoService struct {}

type Args struct {
    A,B int
}

func (DemoService) Dive(args Args,result *float64) error  {
    if args.B==0 {
        return errors.New("division by zero.")
    }
     *result = float64(args.A)/float64(args.B)
     return nil
}
// server
package main

import (
    rpcdemo "gobasic/rpc"
    "log"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
)

func main() {
    rpc.Register(rpcdemo.DemoService{})

    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        panic(err)
    }
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Printf("accept error: %v", err)
            continue
        }

        go jsonrpc.ServeConn(conn)
    }
}

可以通過 telnet localhost 1234 來測試一下,發 json 數據如下

{"method":"DemoService.Dive","params":[{"A":3,"B":4}],"id":1}
// 返回結果
{"id":1,"result":0.75,"error":null}

創建一個 client 來調用

// client/main.go
package main

import (
    "fmt"
    rpcdemo "gobasic/rpc"
    "net"
    "net/rpc/jsonrpc"
)

func main() {
    conn, err := net.Dial("tcp", ":1234")

    if err != nil {
        panic(err)
    }
    client := jsonrpc.NewClient(conn)
    var result float64
    err = client.Call("DemoService.Dive", rpcdemo.Args{10, 3}, &result)
    fmt.Println(result, err)

    err = client.Call("DemoService.Dive", rpcdemo.Args{10, 0}, &result)
    fmt.Println(result, err)
}

主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/6742

(0)
Walker的頭像Walker
上一篇 10小時前
下一篇 2025年11月25日 12:00

相關推薦

  • 編程基礎 0007_併發模式

    Go 併發模式 常見的 Go 併發設計模式,每個模式都有完整可運行示例和適用場景說明 1. Worker Pool 模式 固定數量的 worker goroutine 從共享的任務隊列中取任務執行,控制併發度。 package main import ( "fmt" "sync" "time" ) …

    後端開發 15小時前
    000
  • Go工程師體系課 001

    轉型 想在短時間系統轉到Go工程理由 提高CRUD,無自研框架經驗 拔高技術深度,做專、做精需求的同學 進階工程化,擁有良好開發規範和管理能力的 工程化的重要性 高級開的期望 良好的代碼規範 深入底層原理 熟悉架構 熟悉k8s的基礎架構 擴展知識廣度,知識的深度,規範的開發體系 四個大的階段 go語言基礎 微服務開發的(電商項目實戰) 自研微服務 自研然後重…

  • Go資深工程師講解(慕課) 002

    go(二) string 字符串 package main import ( "fmt" "unicode/utf8" ) func main() { s := "Yes我愛Go語言" fmt.Println(len(s)) for _, b := range []byte(s) { fmt.Pri…

    後端開發 20小時前
    000
  • Go工程師體系課 015

    Docker 容器化 —— Go 項目實戰指南 一、Docker 核心概念 1.1 甚麼是 Docker Docker 是一個開源的容器化平台,它可以將應用程序及其所有依賴項打包到一個標準化的單元(容器)中,從而實現"一次構建,到處運行"。對於 Go 開發者而言,Docker 解決了以下痛點: 開發環境與生產環境不一致 依賴管理複雜(數據庫、緩存、消息隊列等…

  • Go工程師體系課 003

    grpc grpc grpc-go grpc 無縫集成了 protobuf protobuf 習慣用 Json、XML 數據存儲格式的你們,相信大多都沒聽過 Protocol Buffer。 Protocol Buffer 其實是 Google 出品的一種輕量 & 高效的結構化數據存儲格式,性能比 Json、XML 真的強!太!多! protobuf…

    後端開發 10小時前
    000
簡體中文 繁體中文 English