Go语言爬虫笔记 2

文章目录[x]
  1. 1:错误处理
  2. 1.1:简单演示
  3. 1.2:自定义错误
  4. 1.3:记录日志
  5. 1.4:colly中的错误处理
  6. 2:并行处理(多线程)
  7. 2.1:实际代码
  8. 2.2:效果演示
  9. 3:colly配置
  10. 4:HTTP配置
  11. 5:多线程
  12. 5.1:最简单的多线程
  13. 5.2:管道
  14. 5.3:使用管道在进程间通信
  15. 5.4:数据竞争
  16. 5.5:WaitGroup
  17. 5.6:select语句
  18. 6:代理
  19. 6.1:实际例子
  20. 6.2:实际效果
  21. 7:参考文章

错误处理

简单演示

Go语言的错误处理和其他高级语言不一样,GO语言的错误处理是放在函数的返回值后面的。

n, err := Foo(0)  
 
if err != nil { 
    //  错误处理  
} else { 
    //  使用返回值 n

函数有两个返回值,一直是正常的数据,后面可以加一个错误返回值,如果这个值不为空说明有错误发生。然后自己处理这个错误就可以了。

自定义错误

我们可以定义自己的错误处理,然后调用。

package errors_test
import (
    "fmt"
    "time"
)
// MyError is an error implementation that includes a time and message.
type MyError struct {
    When time.Time
    What string
}
func (e MyError) Error() string {
    return fmt.Sprintf("%v: %v", e.When, e.What)
}
func oops() error {
    return MyError{
        time.Date(1989, 3, 15, 22, 30, 0, 0, time.UTC),
        "the file system has gone away",
    }

我使用下面这个测试函数来测试。

func Example() { 
err := oops();
if err != nil { 
    fmt.Println(err) 
} // Output: 1989-03-15 22:30:00 +0000 UTC: the file system has gone away 
}

记录日志

Golang's log模块主要提供了3类接口。分别是 “Print 、Panic 、Fatal ”,对于 log.Fatal 接口,会先将日志内容打印到标准输出,接着调用系统的 os.exit(1) 接口,退出程序并返回状态 1 。但是有一点需要注意,由于是直接调用系统接口退出,defer函数不会被调用

colly中的错误处理

其实很简单,只是调用一个函数,并不需要我们自己设置返回值。

// Set error handler
c.OnError(func(r *colly.Response, err error) {
fmt.Println("Request URL:", r.Request.URL, "failed with response:", r, "\nError:", err)
})

并行处理(多线程)

colly有一个很好的优点就是在于它的并发性。下面我们来继续爬小说网站,这次我们继续拿那个小说网站来进行实战,我们这次把整本小说爬下来,然后保存。解释麻烦,所以会在代码里面加一些注释来弥补。注意我这里下载小说是为了演示多线程下载,所以只能一个线程保存一个文件。实际还是建议不要开多线程下载小说。

实际代码

package main

import (
	"fmt"
	"github.com/gocolly/colly" //爬虫库
	"os"                       //读写文件的库
)

func main() {
	c := colly.NewCollector(
	colly.Async(true),//开启异步功能
	colly.MaxDepth(1000),//最大深度,因为我们爬的小说的章节较多,所以深度调高一点
		)
	/*下面这个是设置请求头*/
	c.UserAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36"
	c.OnRequest(func(r *colly.Request) {
		//设置头部信息
		r.Headers.Set("Referer","https://www.tianxiabachang.cn/5_5166/")
	})
	_ = c.Limit(&colly.LimitRule{
		DomainRegexp: "",
		DomainGlob:   "",
		Delay:        0,
		RandomDelay:  0,
		Parallelism:  50, //这里是限制50个线程同时执行(其实可以调高一点)
	})
	//下面是获取到信息的回调函数
	c.OnHTML("body", func(e *colly.HTMLElement) {
		//先获取一章所有的文本
		var content string
		e.ForEach("#content", func(i int, element *colly.HTMLElement) {
			content+=element.Text //依次拼接字符串
			content+="\r\n"
		})
		/*在原有的基础上继续过滤,获取标题*/
		title:=e.ChildText("h1")
		//保存为txt文件
		file,err:=os.Create(title+".txt")
		//创建成功直接写入文件
		if err==nil {
			//在函数返回的时候调用,一般用于释放资源
			defer file.Close()
			_,err = file.Write([]byte(content))
		}
		//获取下一章的链接
		nexturl:=e.ChildAttr(".bottem2>a:contains(下一章)","href")
		if(nexturl!=""){
			//拼接一下形成完整的url
			nexturl ="https://www.tianxiabachang.cn"+nexturl
			//进入下一线程
			err=e.Request.Visit(nexturl)
		}
		fmt.Println("正在下载"+title)
	})
	c.Visit("https://www.tianxiabachang.cn/5_5166/2013982.html")
	c.Wait()//这个是等待异步任务完成,如果不加的话会导致程序立即退出
}

效果演示

写了这么久的代码,来运行看看效果。。看到这个速度舒服了,多线程的感觉就是爽。。。

colly配置

其实做到上面,我们已经算是入门了,下面我们来进阶一下,先来看看colly的其他配置。collector 默认已经为我们选择了较优的配置,其实它们也可以通过环境变量改变。这样,我们就可以不用为了改变配置,每次都得重新编译了。环境变量配置是在 collector 初始化时生效,正式启动后,配置是可以被覆盖的。

下面是一些支持的配置项(配置可以参考上面一个代码)

ALLOWED_DOMAINS (字符串切片),允许的域名,比如 []string{"segmentfault.com", "zhihu.com"}
CACHE_DIR (string) 缓存目录
DETECT_CHARSET (y/n) 是否检测响应编码
DISABLE_COOKIES (y/n) 禁止 cookies
DISALLOWED_DOMAINS (字符串切片),禁止的域名,同 ALLOWED_DOMAINS 类型
IGNORE_ROBOTSTXT (y/n) 是否忽略 ROBOTS 协议
MAX_BODY_SIZE (int) 响应最大
MAX_DEPTH (int - 0 means infinite) 访问深度
PARSE_HTTP_ERROR_RESPONSE (y/n) 解析 HTTP 响应错误
USER_AGENT (string)

HTTP配置

下面我们来说一下http的配置

c := colly.NewCollector()
c.WithTransport(&http.Transport{
    Proxy: http.ProxyFromEnvironment,
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,          // 超时时间
        KeepAlive: 30 * time.Second,          // keepAlive 超时时间
        DualStack: true,
    }).DialContext,
    MaxIdleConns:          100,               // 最大空闲连接数
    IdleConnTimeout:       90 * time.Second,  // 空闲连接超时
    TLSHandshakeTimeout:   10 * time.Second,  // TLS 握手超时
    ExpectContinueTimeout: 1 * time.Second,  
}

多线程

在学我们设置代理前,我们先来看一下Go语言的多线程原理。

最简单的多线程

func main()  {
	go fmt.Println("lalala")
	fmt.Println("hahaha")
	time.Sleep(4)
}

在语句前面加一个go就可以了,当然我们这样还不够,我们需要sleep一下 当第二行执行完的时候 这时候sleep会释放出CPU资源 这时候第一行可以使用CPU资源进行执行输出结果。

管道

声明双向的无缓存管道

test := make(chan int)

声明双向的有缓存管道

test2 := make(chan int, 10)

创建只写管道

test3 := make(chan <- int)

创建只读管道

test4 := make(<- chan int)

也可以声明通道是指针类型

test5 := make(<- chan *int)

使用管道在进程间通信

当一个管道被声明之后 它是开着的 如果没有信息传进去 那么等待管道的线程一直等不到输出 会陷入死锁。所以我们在声明管道后要及时关闭管道。

ch := make(chan string)
close(ch)
fmt.Println(<- ch)

我们这里简单举一个例子,使用线程里面使用管道发送数据。

package main

import "fmt"

func main()  {
   ch := make(chan string)
   go func() {
      ch <- "lalalalla"//通过管道发送消息
      ch <- "bbbbb"//通过管道发送消息
      close(ch)//关闭管道,避免死锁
   }()
   fmt.Println(<- ch)//第一次获取到了管道的数据,所以可以正确打印出结果
   fmt.Println(<- ch) //第二次我们也获取到了数据
   fmt.Println(<- ch)//第三次因为管道已经关闭,所以没有输出
   a, b := <- ch //这里检测管道有没有输出,用到了GO的错误处理
   fmt.Println("a", a, "b", b)
}

数据竞争

因为涉及到多个进程之间的通信问题,所以我们需要保证某个变量只能在某一个时间内只能被一个进程访问。这里就引入了同步锁的概念。下面同样举一个例子。

package main

import (
   "fmt"
   "sync"
)

type AutomicInt struct {
   mu sync.Mutex
   val int
}
/*这个是GO语言的类似于面向对象的用法,给结构体加一个方法*/
func (a *AutomicInt) add() {
   a.mu.Lock() //这里是解锁互斥锁
   a.val++ //解锁后才可以变量自增
   a.mu.Unlock()
}

func (a AutomicInt) get() int {
   a.mu.Lock()
   r := a.val
   a.mu.Unlock()
   return r
}

func main()  {
   mu := sync.Mutex{} //新建一个互斥锁
   a := AutomicInt{mu, 0} //这新建一个同步锁的结构体变量,然后赋值
   wait := make(chan struct{}) //这里我们新建一个管道
   go func() { //创建新线程
      for i:=0;i<10;i++ {
         a.add() //调用方法
      }
      close(wait) //执行函数
   }()
   a.add()
   <- wait//这里是等待线程执行完成
   r := a.get() //获取最后的数据
   fmt.Println(r)
}

WaitGroup

当个线程还好说,如果线程一多就不好管理,所以我们这里需要用到这个东西来吧所有的线程都放到一个队列里面阻塞运行,直到运行完毕。

func race()  {
	wg := sync.WaitGroup{}
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func() {
			fmt.Println("i", i)
			wg.Done()
		}()
	}
	wg.Wait()
}

func main()  {
	race()
}

最后会输出5个5 这个结果很显然的了 就是for循环上的自增是先被执行的 后面的线程才被执行 那么所有线程输出的i都是5。当然我们可以用下面这个方法来进行验证

func race()  {
	wg := sync.WaitGroup{}
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(i int) {
			fmt.Println("i", i)
			wg.Done()
		}(i)
	}
	wg.Wait()
}

func main()  {
	race()
}

这里我们可以发现,其实输出的结果是乱的,因为线程执行的顺序是不确定的。

select语句

这个算是一个判断语句,用于判断哪个线程先完成,比如下面我们举一个例子。

package main

import "fmt"

func main()  {
	c1, c2 := make(chan int), make(chan int)
	//var i1, i2 int
	go func() {
		c1 <- 12
		close(c1)
	}()
	go func() {
		c2 <- 23
		close(c2)
	}()
	select {
	case i1 := <- c1: fmt.Println("r 1", i1)
	case i2 := <- c2: fmt.Println("r 2", i2)
	}
}

上面的代码可以输出第一个 也可以额输出第二个 这样取决于哪个线程最先完成 select是一定要等到其中一个case有io操作 就是有数据传输过来的 再次之前一直阻塞

但是一直没有io语句执行成功 而且select含有default语句的时候 会执行default

select的作用感觉非常大 因为可以想象一个场景 一个服务器上有一个服务 这个服务有个处理入口 每个请求过来我们轮训select上每个io能否进行操作 如果有那么处理这个请求 如果没有那么去到default那里返回系统繁忙请稍后操作的提示 我们可以使用select轻易实现这个需求

 

代理

代理也是爬虫的一个重要的反爬措施,所以我们下面代理的同时也进行一下实战。爬一下免费代理网站,然后打印出有用的代理。打字麻烦,这里直接给例子,然后加注释。

实际例子

package main

import (
   "fmt"
   "github.com/gocolly/colly"
   "log"
   "strconv"
   "sync"
)

func main() {
   c:=colly.NewCollector()
   c.UserAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36"
   c.OnHTML("*",func(e *colly.HTMLElement) {
      //我们把所有的代理都放到一个数组里面
      var list []string
      e.ForEach(".odd", func(i2 int, e2 *colly.HTMLElement) {
         var ip string
         var port string
         e2.ForEach("td", func(i int, element *colly.HTMLElement) {
            //第一个获取的是ip,第二个是端口
            if i==1{
               ip=element.Text
            }else if i==2{
               port=element.Text
            }
         })
         list=append(list,"http://"+ip+":"+port)
      })
      //这里使用多线程来测试代理ip(使用waitgroup)
      wg:=sync.WaitGroup{}
      //设置线程的数量
      wg.Add(len(list))
      //把获取到的所有的数据依次加到线程里面
      for i:=0;i< len(list);i++{
         go func(proxy string) {
            c2:=colly.NewCollector()
            //设置代理
            err:=c2.SetProxy(proxy)
            if(err!=nil){
               fmt.Println("代理设置失败!")
            }
            //这里获取状态码然后判断是否代理成功
            c2.OnResponse(func(r *colly.Response) {
               fmt.Println(proxy+"状态码:"+strconv.Itoa(r.StatusCode))
            })
            err=c2.Visit("https://www.baidu.com")
            //这里说明线程执行完毕
            wg.Done()
         }(list[i])
      }
      //等待线程执行完成
      wg.Wait()
   })
   err:=c.Visit("https://www.xicidaili.com/wt/")
   if(err!=nil){
      log.Fatal(err)
   }
}

实际效果

可以看到,我们成功利用多线程爬到了一些免费的代理。后面我们会继续拿这个代理网站作为实战,把我们爬到的数据保存起来。

参考文章

1.Go错误处理---errors包以及Go记录日志---log包

2.《Go语言四十二章经》第四十一章 网络爬虫

3.Go语言中文网

 

 

 

点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

Title - Artist
0:00