2024年最新Go分布式爬虫学习笔记(十三)_go语言分布式爬虫pdf,如何获得大厂面试机会

一、Python所有方向的学习路线

Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。

img
img

二、Python必备开发工具

工具都帮大家整理好了,安装就可直接上手!img

三、最新Python学习笔记

当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。

img

四、Python视频合集

观看全面零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。

img

五、实战案例

纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。img

六、面试宝典

在这里插入图片描述

在这里插入图片描述

简历模板在这里插入图片描述

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

文章目录

13_接口

接口

优势:

  • 隐藏细节
  • 解耦
  • 权限控制

R; Go at Google: Language Design in the Service of Software Engineering

Go接口设计:

  • 没有继承
  • 面向组合
  • 隐式实现 duck type

If it walks like duck, swims like a duck and quacks like a duck, it’s a duck.

如果它像鸭子一样走路,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就是鸭子。

接口最佳实践

示例: orm使用

在 xorm 中插入一行数据的语法是调用 Insert 方法。

user := User{Name: "jonson", Age: 18, Birthday: time.Now()}
db.Insert(&User)

而在 gorm 中,添加一行数据的语法是调用 Create 方法。

user := User{Name: "jonson", Age: 18, Birthday: time.Now()}
db.Create(&User)

初学者:

  • 创建一个操作数据库的实例 XormDB,并把它嵌入到实际业务的结构体中。
  • 假设现在需要将 xorm 更换到 gorm,我们就需要重新创建一个操作数据库的实例 GormDB。然后把项目中所有使用了 XormDB 的结构体替换为 GormDB,最后检查项目中所有 DB 的操作,把不兼容的 API 全部替换掉,或者使用一些新的特性。

缺点

  • 在大型项目中不仅改动非常大,耗时耗力,更重要的是,我们很难对模块进行真正的拆分。
  • 对数据库的修改可能破坏或影响项目中一些核心流程的代码(例如插入订单、修改金额等),难以保证结果的正确性。
  • 同时,我们不希望随意操作数据库 DB 对象。例如,我们不想暴露删除表的操作,而只希望暴露有限的方法。

改造:

  • 创建一个接口实例 DBer,该接口包含一个自定义的插入方法 Insert。
  • 再创建一个数据库实例 XormDB,实现了 Insert 方法。
type DBer interface{
  Insert(ctx context.Context,instance interface{})
    ...
}
type XormDB struct{
  db \*xorm.Session
}
func (xorm \*XormDB) Insert(ctx context.Context,instance ...interface{}){
  xorm.db.Context(ctx).Insert(instance)
}

现在我们要实现从 xorm 到 gorm 的切换将变得非常简单,只需要新增一个实现了 DBer 的 GormDB 实例,同时在初始化时调用 AddDB 设置新的数据库实例就好了,其他地方的代码完全不用变动。

type GormDB struct{
  db \*xorm.Session
}
func (gorm \*GormDB) Insert(ctx context.Context,instance ...interface{}){
  gorm.db.Context(ctx).Create(instance)
}


有了接口,代码变得更具通用性和可扩展性了。而且,我们也不用修改 InsertTrade 等核心业务的方法,这就减少了出错的可能性。更重要的是,我们实现了模块间的解耦,修改 DB 模块不会影响到其他模块,每个模块都可以独立地开发、更换和调试。

依赖注入

程序中的模块通常会依赖其他模块返回的结果,测试中遇到的困难:

  • 第三方模块的环境不太容易和线上完全一致,依赖的模块可能又依赖了其他的模块。
  • 除了依赖服务太多这个问题外,依赖配置也很繁琐。例如,要测试一个场景,需要往数据库中插入数据、删除数据,这增加了复杂性。
  • 场景很难完全覆盖。打个比方,如果当前服务在进行逻辑处理时,非常依赖外部服务返回的数据,那我想测试外部服务返回特定的数据时,当前服务会有什么不同的行为就非常困难。
  • 有一些第三方模块涉及到复杂逻辑,或者会 sleep 很长时间,这时进行完整测试需要花费很长的时间。

示例1 InsertTrade不需要启动数据库

它的内部有一个插入订单的操作,测试时不必真的启动一个数据库,也不必真的将订单插入到数据库中。

下面这段代码中,EmptyDB 实现了 DBer 接口,但是实际函数中并不执行任何操作。

type Trade struct {
  db DBer
}

func (t \*Trade) AddDB(db DBer) {
  t.db = db
}

func (t\*Trade) InsertTrade() error{
    ...
  t.db.Create(t)
}

// 测试代码
type EmptyDB struct {
}

func (e \*EmptyDB) Insert(ctx context.Context, instance ...interface{}) {
  return
}

func TestHandleTrade(t \*testing.T)  {
  t := Trade{}
  t.add(EmptyDB{})
  err := t.handleTrade()
  assert.NotNil(t,err)
}

示例2 redigo时间函数

redigo 库的一个重要功能是维持 Redis 的连接池。但是连接一段时间后,需要强制断开,这段时间被称为最大连接时间。假设我们设置的最大连接时间是 300 秒。redigo 在取出连接池的连接后,会先判断当前时间减去连接创建时间是否超过 300 秒。如果超过,则立即销毁连接(这段代码省略掉了不必要的细节,原始代码

var nowFunc = time.Now
func (p \*Pool) GetContext(ctx context.Context) (Conn, error) {
// 从连接池获取连接
  for p.idle.front != nil {
    pc := p.idle.front
    p.idle.popFront()
     // 当前时间减去连接创建时间未超过300秒,立即返回。
    if (nowFunc().Sub(pc.created) < p.MaxConnLifetime) {
      return &activeConn{p: p, pc: pc}, nil
    }
  }

}

这里比较有意思的是,获取当前时间的方式通过了一个 nowFunc 变量。

nowFunc 是一个函数变量,其本质上也是 time.Now 函数。

为什么不直接使用我们比较熟悉的 time.Now(),而是额外增加了一层呢?

  • 为了方便测试。你试想一下,如果我们想测试函数在 300s 之后能否断开,那么我们的单元测试必须要等 300s 这么久吗?显然是不可能的,这样做效率太低了。
  • Redigo 的做法是,通过修改 now 函数变量对应的值,我们可以任意修改当前时间,从而影响 GetContext 函数的行为。当时间未超过最大连接时间时,我们预期连接会被复用,达不到测试超时的效果,所以我们可以设置 now = now.Add(p.MaxConnLifetime + 1) ,巧妙地让当前时间超过最大连接时间,看连接是不是真的和预期一样被销毁。

// pool\_test.go
func TestPoolMaxLifetime(t \*testing.T) {
  d := poolDialer{t: t}
  p := &redis.Pool{
    MaxIdle:         2,
    MaxConnLifetime: 300 \* time.Second,
    Dial:            d.dial,
  }
  defer p.Close()
  // 设置now为当前时间
  now := time.Now()
  redis.SetNowFunc(func() time.Time { return now })
  defer redis.SetNowFunc(time.Now)

  c := p.Get()
  \_, err := c.Do("PING")
  require.NoError(t, err)
  c.Close()

  d.check("1", p, 1, 1, 0)

  // 设置now为最大连接时间+1
  now = now.Add(p.MaxConnLifetime + 1)

  c = p.Get()
  \_, err = c.Do("PING")
  require.NoError(t, err)
  c.Close()

  d.check("2", p, 2, 1, 0)
}

接口底层

接口的底层结构如下,它分为 tab 和 data 两个字段。

type iface struct {
  tab \*itab           // 存储了接口的类型、接口中的动态数据类型、动态数据类型的函数指针等
  data unsafe.Pointer // 存储了接口中动态类型的数据指针
}

image

接口能够容纳不同的类型的秘诀

  • 存储当前接口的类型
  • 存储动态数据类型
  • 存储动态数据类型对应的数据
  • 动态数据类型实现接口方法的指针。

这种为不同数据类型的实体提供统一接口的能力被称为多态。实际上,接口只是一个容器,当我们调用接口时,最终会找到接口中容纳的动态数据类型和它所对应方法的指针,并完成调用。

接口成本

由于动态数据类型对应的数据大小难以预料,接口中使用指针来存储数据。

同时,为了方便数据被寻址,平时分配在栈中的值一旦赋值给接口后,Go 运行时会在堆区为接口开辟内存,这种现象被称为内存逃逸,它是接口需要承担的成本之一。

内存逃逸意味着堆内存分配时的时间消耗

接口的另一个成本是调用时查找接口中容纳的动态数据类型和它对应的方法的指针带来的开销。

这种开销的成本有多大呢?

这里我们用一个简单的 Benchmark 测试来说明一下。在下面这个例子中,BenchmarkDirect 测试直接调用调用的开销。BenchmarkInterface 测试进行接口调用的开销,但其函数接收者是一个非指针。BenchmarkInterfacePointer 也是测试接口调用的开销,但其函数接收者是一个指针。

package escape

import "testing"

type Sumifier interface{ Add(a, b int32) int32 }

type Sumer struct{ id int32 }

func (math Sumer) Add(a, b int32) int32 { return a + b }

type SumerPointer struct{ id int32 }

func (math \*SumerPointer) Add(a, b int32) int32 { return a + b }

func BenchmarkDirect(b \*testing.B) {
  adder := Sumer{id: 6754}
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
    adder.Add(10, 12)
  }
}

func BenchmarkInterface(b \*testing.B) {
  adder := Sumer{id: 6754}
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
    Sumifier(adder).Add(10, 12)
  }
}

func BenchmarkInterfacePointer(b \*testing.B) {
  adder := &SumerPointer{id: 6754}
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
    Sumifier(adder).Add(10, 12)
  }
}

在 Benchmark 测试中,我们静止编译器的优化和内联汇编,避免这两种因素对耗时产生的影响。测试结果如下。可以看到直接函数调用的速度最快,为 1.95 ns/op, 方法接收者为指针的接口调用和函数调用的速度类似,为 2.37 ns/op, 方法接收者为非指针的接口调用却慢了数倍,为 14.6 ns/op。

N: Windows直接运行**go test -gcflags "-N -l" -bench=.**​好像不行

» go test -gcflags "-N -l"   -bench=.
BenchmarkDirect-12                      535487740                1.95 ns/op
BenchmarkInterface-12                   76026812                 14.6 ns/op
BenchmarkInterfacePointer-12            517756519                2.37 ns/op

go test -gcflags "-N -l"   -bench=.
goos: linux
goarch: amd64
pkg: github.com/funbinary/go_example/example/crawler/benchmark
cpu: 12th Gen Intel(R) Core(TM) i7-12700F
BenchmarkDirect-20                      1000000000               0.9737 ns/op
BenchmarkInterface-20                   906270834                1.305 ns/op
BenchmarkInterfacePointer-20            1000000000               1.080 ns/op
PASS
ok      github.com/funbinary/go_example/example/crawler/benchmark       3.587s


**方法接收者为非指针的接口调用速度之所以很慢是受到了内存拷贝的影响。**由于接口中存储了数据的指针,而函数调用的是非指针,因此数据会从对堆内存拷贝到栈内存,让调用速度变慢。

启发:

  • 在使用接口时,方法接收者使用指针的形式能够带来速度的提升
  • 接口调用带来的性能损失很小,在实际开发中,不必担心接口带来的效率损失

爬取技术

  • 模拟浏览器访问
  • 代理访问

爬取接口抽象

  • 创建collect用于采集引擎, 存放与爬取相关代码
  • 定义Fetcher接口,内部方法Get,参数为url N: 后续会修改,不用提前费劲设计
type Fetcher interface {
  Get(url string) ([]byte, error)
}

  • 定义一个结构体 BaseFetch,用最基本的爬取逻辑实现 Fetcher 接口:
func (BaseFetch) Get(url string) ([]byte, error) {
  resp, err := http.Get(url)

  if err != nil {
    fmt.Println(err)
    return nil, err
  }

  defer resp.Body.Close()

  if resp.StatusCode != http.StatusOK {
    fmt.Printf("Error status code:%d", resp.StatusCode)
  }
  bodyReader := bufio.NewReader(resp.Body)
  e := DeterminEncoding(bodyReader)
  utf8Reader := transform.NewReader(bodyReader, e.NewDecoder())
  return ioutil.ReadAll(utf8Reader)
}

  • 在 main.go 中定义一个类型为 BaseFetch 的结构体,用接口 Fetcher 接收并调用 Get 方法,这样就完成了使用接口来实现基本爬取的逻辑。
var f collect.Fetcher = collect.BaseFetch{}
body, err := f.Get(url)

模拟浏览器访问

Mozilla/5.0 (操作系统信息) 运行平台(运行平台细节) <扩展信息>

  • 我的浏览器
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36	

+ Mozilla/5.0 由于历史原因,是现在的主流浏览器都会发送的。
+ Windows NT 10.0; Win64; x64: 操作系统版本号。
+ AppleWebKit/537.36: 使用的 Web 渲染引擎标识符。
+ KHTML: 在 Safari 和 Chrome 上使用的引擎。
+ Chrome/111.0.0.0 Safari/537.36: 浏览器版本号
  • 不同浏览器,User-Agent会不同
Lynx: Lynx/2.8.8pre.4 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/2.12.23

Wget: Wget/1.15 (linux-gnu)

Curl: curl/7.35.0

Samsung Galaxy Note 4: Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-N910F Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/4.0 Chrome/44.0.2403.133 Mobile Safari/537.36

Apple iPhone: Mozilla/5.0 (iPhone; CPU iPhone OS 10\_3\_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1

Apple iPad: Mozilla/5.0 (iPad; CPU OS 8\_4\_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4

Microsoft Internet Explorer 11 / IE 11: Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko

  • 有时候,我们的爬虫服务需要动态生成 User-Agent 列表,方便在测试、或者在使用代理大量请求单一网站时,动态设置不同的 User-Agent。

实现BrowserFetch

  • 创建一个 HTTP 客户端 http.Client
  • 通过 http.NewRequest 创建一个请求。
  • 在请求中调用 req.Header.Set 设置 User-Agent 请求头。
  • 调用 client.Do 完成 HTTP 请求。
type BrowserFetch struct {
}

func (b \*BrowserFetch) Get(url string) ([]byte, error) {
	client := &http.Client{}

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36\t\n")

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("Error status code:%v", resp.StatusCode)
	}

	r := bufio.NewReader(resp.Body)
	e := DeterminEncoding(r)
	utf8r := transform.NewReader(r, e.NewDecoder())
	return io.ReadAll(utf8r)
}

远程访问浏览器

要借助浏览器的能力实现自动化爬取,目前依靠的技术有以下三种:

文末有福利领取哦~

👉一、Python所有方向的学习路线

Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。img

👉二、Python必备开发工具

img
👉三、Python视频合集

观看零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
img

👉 四、实战案例

光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。(文末领读者福利)
img

👉五、Python练习题

检查学习结果。
img

👉六、面试资料

我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
img

img

👉因篇幅有限,仅展示部分资料,这份完整版的Python全套学习资料已经上传

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 29
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值