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
上一篇 2026年3月8日 15:11
下一篇 2026年3月9日 12:56

相關推薦

  • 編程基礎 0004_Web_beego開發

    beego 開始 2 文章的添加與刪除 創建 TopicController // controllers中添加topic.go package controllers import "github.com/astaxie/beego" type TopicController struct { beego.Controller } fu…

    後端開發 2026年3月6日
    5800
  • Go工程師體系課 011

    查詢的倒排索引 1. 什麼是倒排索引? 倒排索引(Inverted Index)是一種數據結構,用於快速查找包含特定詞彙的文檔。它是搜索引擎的核心技術之一。 1.1 基本概念 正排索引:文檔 ID → 文檔內容(詞列表) 倒排索引:詞 → 包含該詞的文檔 ID 列表 1.2 爲什麼叫"倒排"? 倒排索引將傳統的"文檔包含哪些詞"的關係倒轉爲"詞出現在哪些文檔…

    後端開發 2026年3月6日
    6800
  • 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…

    後端開發 2026年3月6日
    5300
  • Go工程師體系課 004

    需求分析 後臺管理系統 商品管理 商品列表 商品分類 品牌管理 品牌分類 訂單管理 訂單列表 用戶信息管理 用戶列表 用戶地址 用戶留言 輪播圖管理 電商系統 登錄頁面 首頁 商品搜索 商品分類導航 輪播圖展示 推薦商品展示 商品詳情頁 商品圖片展示 商品描述 商品規格選擇 加入購物車 購物車 商品列表 數量調整 刪除商品 結算功能 用戶中心 訂單中心 我的…

    2026年3月6日
    5900
  • 編程基礎 0005_錯誤處理進階

    Go 錯誤處理進階 目錄 Go 錯誤處理哲學 error 接口本質 自定義錯誤類型 fmt.Errorf 與 %w 包裝錯誤 errors.Is 和 errors.As 哨兵錯誤模式 錯誤處理最佳實踐 實際項目中的錯誤處理模式 1. Go 錯誤處理哲學 1.1 與 try-catch 的根本區別 在 Java、Python、C++ 等語言中,異常處理依賴 t…

    後端開發 2026年3月6日
    6400
簡體中文 繁體中文 English