使用go开发的并发爬虫

前言

好久没更新了,最近一直在使用go基础写案例,现在就来分享分享我的爬虫
以前使用过python写过爬虫,python丰富的第三方库为我提供了很大的便利。那么对于go语言,它的优点就在于协程的使用,如果把协程的思想用于爬虫,实现并发,是不是更方便呢。

基本思路

1.初始化一个数据管道
2.爬虫写出:创建多个协程用于添加图片,我这里添加50个协程向管道中添加图片链接
3.任务统计协程:检查50个任务是否都完成,完成则关闭数据管道
4.下载协程:从管道里读取链接并下载
先来看看我们用到的标准库
使用go开发的并发爬虫_第1张图片
这些库的作用就不多说了,不会的话可以百度
我爬的是一个壁纸网站,是跟着一个大佬学的
https://www.bizhizu.cn/shouji/tag-%E5%8F%AF%E7%88%B1/
在这之前,咱们先说说一般爬虫的思路,以前我用过python写过一个爬虫爬的是一个豆瓣网站,当时爬的时候,是对整个网页的源代码进行分析,整个网页有不同的html元素,每个单独提取进行操作,把最后爬的内容存到csv,excel,mysql 里,但这种对于图片的爬取,存进去的往往是图片的链接以及图片的二进制码。所以我们今天爬的网站有点特殊,它就是一个妥妥的壁纸网站,所以我们就可以只对图片进行爬取,把爬到的图片存到一个普通的文件夹里,记住我们要的是图片。

数据管道
咱们这里开两个通道,一个用来存放图片链接,一个用来监控协程

var (
	// 存放图片链接的数据管道
	chanImageUrls chan string
	waitGroup     sync.WaitGroup
	// 用于监控协程
	chanTask chan string
	reImg    = `https?://[^"]+?(\.((jpg)|(png)|(jpeg)|(gif)|(bmp)))`
)

在这里说一下sync.WaitGroup的作用就是完美结决主线程等待协程运行完但是不知道等待多少时间的问题,对于爬虫就是较好地选择

协程处理进行并发爬虫
初始化管道

chanImageUrls = make(chan string, 1000000)
	chanTask = make(chan string, 50)

爬虫协程


```go
for i := 1; i < 51; i++ {
		waitGroup.Add(1)
		go getImgUrls("https://www.bizhizu.cn/shouji/tag-%E5%8F%AF%E7%88%B1/" + strconv.Itoa(i) + ".html")
	}
``

任务统计50个协程是否全部完成

waitGroup.Add(1)
	go CheckOK()

下载协程,从管道中读取链接下载

for i := 0; i < 5; i++ {
		waitGroup.Add(1)
		go DownloadImg()
	}
	waitGroup.Wait()

方法汇总
下载图片DownloadImg()

for url := range chanImageUrls {
		filename := GetFilenameFromUrl(url)
		ok := DownloadFile(url, filename)
		if ok {
			fmt.Printf("%s 下载成功\n", filename)
		} else {
			fmt.Printf("%s 下载失败\n", filename)
		}
	}
	waitGroup.Done()

截取url GetFilenameFromUrl()

func GetFilenameFromUrl(url string) (filename string) {
	// 返回最后一个/的位置
	lastIndex := strings.LastIndex(url, "/")
	// 切出来
	filename = url[lastIndex+1:]
	// 时间戳解决重名
	timePrefix := strconv.Itoa(int(time.Now().UnixNano()))
	filename = timePrefix + "_" + filename
	return
}

任务统计CheckOK()

var count int
	for {
		url := <-chanTask
		fmt.Printf("%s 完成了爬取任务\n", url)
		count++
		if count == 50 {
			close(chanImageUrls)
			break
		}
	}
	waitGroup.Done()

爬取图片链接到管道,这里的URL是整页链接,是以页为单位的

func getImgUrls(url string) {
	urls := getImgs(url)
	// 遍历切片里所有链接,存入数据管道
	for _, url := range urls {
		chanImageUrls <- url
	}
	// 标识当前协程完成
	// 每完成一个任务,写一条数据
	// 用于监控协程知道已经完成了几个任务
	chanTask <- url
	waitGroup.Done()
}

爬取图片链接,获取当前页的所有图片链接

func getImgs(url string) (urls []string) {
	pageStr := GetPageStr(url)
	re := regexp.MustCompile(reImg)
	results := re.FindAllStringSubmatch(pageStr, -1)
	fmt.Printf("共找到%d条结果\n", len(results))
	for _, result := range results {
		url := result[0]
		urls = append(urls, url)
	}
	return
}

根据url获取内容

func GetPageStr(url string) (pageStr string) {
	resp, err := http.Get(url)
	HandleError(err, "http.Get url")
	defer resp.Body.Close()
	// 2.读取页面内容
	pageBytes, err := ioutil.ReadAll(resp.Body)
	HandleError(err, "ioutil.ReadAll")
	// 字节转字符串
	pageStr = string(pageBytes)
	return pageStr
}

下载图片到本地

func DownloadFile(url string, filename string) (ok bool) {
	resp, err := http.Get(url)
	HandleError(err, "http.get.url")
	defer resp.Body.Close()
	bytes, err := ioutil.ReadAll(resp.Body)
	HandleError(err, "resp.body")
	filename = "E:/image/" + filename
	// 写出数据
	err = ioutil.WriteFile(filename, bytes, 0666)
	if err != nil {
		return false
	} else {
		return true
	}
}

*总结
写这个爬虫我找了很多资源,主要是想用到go的特性高并发,通过这个案例我对于go的特性以及通道协程的理解与使用越来越熟练,另外这个爬取的路径选择,尽量在内存充足的盘里进行,图片很多,通过爬的速度你就能知道go有多好用了。
下面附上完整源码

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

var (
	// 存放图片链接的数据管道
	chanImageUrls chan string
	waitGroup     sync.WaitGroup
	// 用于监控协程
	chanTask chan string
	reImg    = `https?://[^"]+?(\.((jpg)|(png)|(jpeg)|(gif)|(bmp)))`
)

func DownloadImg() {
	for url := range chanImageUrls {
		filename := GetFilenameFromUrl(url)
		ok := DownloadFile(url, filename)
		if ok {
			fmt.Printf("%s 下载成功\n", filename)
		} else {
			fmt.Printf("%s 下载失败\n", filename)
		}
	}
	waitGroup.Done()

}

func GetFilenameFromUrl(url string) (filename string) {
	// 返回最后一个/的位置
	lastIndex := strings.LastIndex(url, "/")
	// 切出来
	filename = url[lastIndex+1:]
	// 时间戳解决重名
	timePrefix := strconv.Itoa(int(time.Now().UnixNano()))
	filename = timePrefix + "_" + filename
	return
}
func getImgs(url string) (urls []string) {
	pageStr := GetPageStr(url)
	re := regexp.MustCompile(reImg)
	results := re.FindAllStringSubmatch(pageStr, -1)
	fmt.Printf("共找到%d条结果\n", len(results))
	for _, result := range results {
		url := result[0]
		urls = append(urls, url)
	}
	return
}

func GetPageStr(url string) (pageStr string) {
	resp, err := http.Get(url)
	HandleError(err, "http.Get url")
	defer resp.Body.Close()
	// 2.读取页面内容
	pageBytes, err := ioutil.ReadAll(resp.Body)
	HandleError(err, "ioutil.ReadAll")
	// 字节转字符串
	pageStr = string(pageBytes)
	return pageStr
}

func HandleError(err error, s string) {
	return
}

func CheckOK() {
	var count int
	for {
		url := <-chanTask
		fmt.Printf("%s 完成了爬取任务\n", url)
		count++
		if count == 50 {
			close(chanImageUrls)
			break
		}
	}
	waitGroup.Done()

}

func getImgUrls(url string) {
	urls := getImgs(url)
	// 遍历切片里所有链接,存入数据管道
	for _, url := range urls {
		chanImageUrls <- url
	}
	// 标识当前协程完成
	// 每完成一个任务,写一条数据
	// 用于监控协程知道已经完成了几个任务
	chanTask <- url
	waitGroup.Done()
}

func DownloadFile(url string, filename string) (ok bool) {
	resp, err := http.Get(url)
	HandleError(err, "http.get.url")
	defer resp.Body.Close()
	bytes, err := ioutil.ReadAll(resp.Body)
	HandleError(err, "resp.body")
	filename = "E:/image/" + filename
	// 写出数据
	err = ioutil.WriteFile(filename, bytes, 0666)
	if err != nil {
		return false
	} else {
		return true
	}
}

func main() {
	chanImageUrls = make(chan string, 1000000)
	chanTask = make(chan string, 50)

	for i := 1; i < 51; i++ {
		waitGroup.Add(1)
		go getImgUrls("https://www.bizhizu.cn/shouji/tag-%E5%8F%AF%E7%88%B1/" + strconv.Itoa(i) + ".html")
	}
	waitGroup.Add(1)
	go CheckOK()
	for i := 0; i < 5; i++ {
		waitGroup.Add(1)
		go DownloadImg()
	}
	waitGroup.Wait()

}

你可能感兴趣的:(golang,后端)