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日
    5900
  • Go工程师体系课 011

    查询的倒排索引 1. 什么是倒排索引? 倒排索引(Inverted Index)是一种数据结构,用于快速查找包含特定词汇的文档。它是搜索引擎的核心技术之一。 1.1 基本概念 正排索引:文档 ID → 文档内容(词列表) 倒排索引:词 → 包含该词的文档 ID 列表 1.2 为什么叫"倒排"? 倒排索引将传统的"文档包含哪些词"的关系倒转为"词出现在哪些文档…

    后端开发 2026年3月6日
    6900
  • 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