当前位置: 首页 > 工具软件 > go_spider > 使用案例 >

Go语言spider基础

章德惠
2023-12-01

一. Go语言爬取

Go语言的爬虫与python类似(可以参照官网http包和regexp​​​​​​​包),但是Go语言爬取的效率更高,一般的爬取主要使用到ioutil,http,regexp,sync等相关处理的包,ioutil包主要是读取和写入数据的工具包(包括将数据读取和写入文件),http是关于客户端与服务端请求操作的包,regexp是关于正则表达式匹配相关操作的包,sync是关于同步操作相关的包(包括加锁和解锁的操作),下面是一个简单爬取邮箱,链接,手机号,身份证号的例子,类似于python的爬取,首先需要发送url请求,服务端返回一个响应体,我们从响应体中读取数据将其转换为字符串(读取数据的时候返回的可能是byte切片),然后使用正则表达式匹配字符串的内容,爬取的核心主要是正则表达式的编写。下面的这个例子中通过GetPageStr()函数传递url,在这个函数中通过http包中的http.Get(url)发送请求,使用ioutil包中ReadAll()函数读取Get()函数返回的响应体的内容,如果有错误那么传递给Go语言内置的err输出错误的结果(可以使用一个方法处理错误),因为ReadAll()函数返回的是读取到的内容的byte切片,所以需要使用string()函数将其转为字符串,最后通过regexp包中的MustCompile()编译的正则表达式匹配内容(类似于compile函数),核心还是正则表达式,以身份证号码的匹配为例:"[123456789]\d{5}((19\d{2})|(20[01]\d))((0[1-9])|(1[012]))((0[1-9])|([12]\d)|(3[01]))\d{3}[\dXx]",[]表示一个集合,[123456789]表示从1~9中其中一个数字(只匹配一个数字),\d表示数字,{5}表示出现5次,|表示或者,(19\d{2})表示以19开头的年份,(20[01]\d)表示200或者201开头的年份,(0[1-9])|(1[012])表示0开头的月份或者1开头的月份,(0[1-9])|([12]\d)|(3[01])表示是一个月第几号,\d{3}表示出现三个数字,[\dXx]表示数字或者"x","X":

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"
)

var (
	// w代表大小写字母+数字+下划线
	reEmail = `\w+@\w+\.\w+`
	// s?有或者没有s, +代表出1次或多次, \s\S各种字符, +?代表贪婪模式
	reLink = `href="(https?://[\s\S]+?)"`
	// 手机号码的正则表达式
	rePhone = `1[3456789]\d{9}`
	// 身份证号的正则表达式
	reIdcard = `[123456789]\d{5}((19\d{2})|(20[01]\d))((0[1-9])|(1[012]))((0[1-9])|([12]\d)|(3[01]))\d{3}[\dXx]`
)

// 处理异常
func HandleError(err error, why string) {
	if err != nil {
		fmt.Println(why, err)
	}
}

// 爬取邮箱
func GetEmail(url string) {
	pageStr := GetPageStr(url)
	re := regexp.MustCompile(reEmail)
	results := re.FindAllStringSubmatch(pageStr, -1)
	for _, result := range results {
		fmt.Println(result)
	}
}

// 根据爬取url获取页面内容
func GetPageStr(url string) (pageStr string) {
	resp, err := http.Get(url)
	HandleError(err, "http.Get url")
	// 使用defer关键字在GetPageStr()函数返回的时候关闭resp.Body()
	defer resp.Body.Close()
	// 读取页面内容
	pageBytes, err := ioutil.ReadAll(resp.Body)
	HandleError(err, "ioutil.ReadAll")
	// 使用string()函数将字节转字符串
	pageStr = string(pageBytes)
	return pageStr
}

func main() {
	// 1. 爬取邮箱
	// GetEmail("https://tieba.baidu.com/p/6051076813?red_tag=1573533731")
	// 2. 爬取链接
	//GetLink("https://pkg.go.dev/regexp#section-documentation")
	// 3.爬取手机号
	//GetPhone("https://www.zhaohaowang.com/")
	// 4.爬取身份证号
	GetIdCard("http://sfzdq.uzuzuz.com/sfz/230182.html")
}

// 爬取身份证号码
func GetIdCard(url string) {
	pageStr := GetPageStr(url)
    // 编译正则表达式
	re := regexp.MustCompile(reIdcard)
	results := re.FindAllStringSubmatch(pageStr, -1)
	for _, result := range results {
		fmt.Println(result)
	}
}

// 爬取链接
func GetLink(url string) {
	pageStr := GetPageStr(url)
	re := regexp.MustCompile(reLink)
	results := re.FindAllStringSubmatch(pageStr, -1)
	for _, result := range results {
		fmt.Println(result[1])
	}
}

// 爬取手机号
func GetPhone(url string) {
	pageStr := GetPageStr(url)
	re := regexp.MustCompile(rePhone)
	results := re.FindAllStringSubmatch(pageStr, -1)
	for _, result := range results {
		fmt.Println(result)
	}
}

二. regexp包

regexp包的官方文档提供了16种类型的方法来匹配一个正则表达式或者识别的文本,他们的名字包含下面的单词:Find(All)?(String)?(Submatch)?(Index)?,这些方法会传递一个参数n,当n >= 0的时候这些函数返回至多n个匹配或者子匹配,当n小于0的时候会返回所有的匹配,,"All"会匹配整个表达式的连续非重叠匹配,"String"表示查找或者返回的是字符串,"Submatch"表示子匹配,返回值是包含所有连续子匹配表达式的切片,子匹配是括号表达式匹配,也即匹配括号中的内容,例如当前的正则表达式为"xxx()()()xxx",第一个子匹匹配满足正则表达式的所有内容,第二个子匹配匹配正则表达式第一个括号的内容,第三个子匹配匹配正则表达式第二个括号的内容,以此类推,例如上面的例子中匹配身份证号的时候,使用了小括号,那么最终会匹配每一个括号的内容:

package main

import (
	"fmt"
	"regexp"
)

func main() {
	s := `[123456789]\d{5}((19\d{2})|(20[01]\d))((0[1-9])|(1[012]))((0[1-9])|([12]\d)|(3[01]))\d{3}[\dXx]`
	re := regexp.MustCompile(s)
    // -1表示匹配所有
	results := re.FindAllStringSubmatch("230182195604045724", -1)
	fmt.Println(results)
	return
}

 输出结果:

[[230182195604045724 1956 1956  04 04  04 04  ]]

三. 爬取图片并下载到本地

爬取的思路:我们肯定需要先知道图片的具体的位置,也即对应的超链接,以爬取多页的图片为例,我们需要先获取到每一页中图片的超链接,然后将超链接的图片下载到本地,所以属于两个任务,那么我们就可以使用go关键字开启两个goroutine分别完成图片链接的爬取和下载图片到本地的任务,goroutine一般会结合通道来使用,这样两个goroutine就可以通过通道通信,爬取的具体实现:① 初始化数据通道,声明一个string类型的chanImageUrls的通道记录爬取每一个页面的图片超链接,声明一个全局的sync.WaitGroup类型变量waitGroup进行goroutine的同步操作,waitGroup能够一直等到所有的goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成;声明一个string类型的chanTask通道记录当前已经完成了多少个页面的爬取,当所有页面的任务爬取完成之后关闭chanImageUrls通道;

package main

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

// 异常处理
func HandleError(err error, why string) {
	if err != nil {
		fmt.Println(why, err)
	}
}

// 下载图片, 传入图片的url地址
func DownloadFile(url string, filename string) (ok bool) {
	resp, err := http.Get(url)
	HandleError(err, "http.get.url")
	// 关闭response.Body()
	defer resp.Body.Close()
	bytes, err := ioutil.ReadAll(resp.Body)
	HandleError(err, "resp.body")
	// 写入的文件路径
	filename = "C:/Users/张宇/Desktop/images/" + filename
	// 将图片写入到对应的路径中
	err = ioutil.WriteFile(filename, bytes, 0666)
	if err != nil {
		return false
	} else {
		return true
	}
}

// 并发爬思路:
// 1.初始化数据管道
// 2.爬虫写出:26个协程向管道中添加图片链接
// 3.任务统计协程:检查26个任务是否都完成,完成则关闭数据管道
// 4.下载协程:从管道里读取链接并下载

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

func main() {
	// 1.初始化管道
	chanImageUrls = make(chan string, 1000000)
	chanTask = make(chan string, 22)

	// 2.爬虫协程, 使用go关键字启动22个goroutine
	for i := 1; i < 23; i++ {
		waitGroup.Add(1)
		// 1. https://www.bizhizu.cn/shouji/tag-%E5%8F%AF%E7%88%B1/
		// 下面这个网址不知道为什么获取不了整个html页面的内容, 只获取到script标签的内容
		// 2. https://www.ivsky.com/bizhi/naruto_t563/index_01.html
		go getImgUrls("https://www.bizhizu.cn/shouji/tag-%E5%8F%AF%E7%88%B1/" + strconv.Itoa(i) + ".html")
	}

	// 3.任务统计协程,统计22个任务是否都完成,完成则关闭管道
	waitGroup.Add(1)
	go CheckOK()

	// 4.下载协程: 从通道中获取链接并下载
	for i := 0; i < 5; i++ {
		waitGroup.Add(1)
		go DownloadImg()
	}
	waitGroup.Wait()
}

// 下载图片
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()
}

// 截取url名字
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 CheckOK() {
	defer waitGroup.Done()
	var count int = 0
	for {
		url := <-chanTask
		fmt.Printf("%s 完成了爬取任务\n", url)
		count++
		if count == 22 {
			close(chanImageUrls)
			break
		}
	}
}

// 爬取图片链接到管道, url是传的整个页面的地址
func getImgUrls(url string) {
	urls := getImgs(url)
	// 遍历切片里所有链接,存入数据管道
	for _, url := range urls {
		chanImageUrls <- url
		fmt.Println(url, "=====>")
	}
	// 标识当前协程完成
	// 每完成一个任务,写一条数据
	// 用于监控协程已经完成了多少个任务
	chanTask <- url
	// 当前页面爬取完成之后那么注册的goroutine的数量应该减1, 也即调用下面的方法
	waitGroup.Done()
}

// 获取当前页图片的超链接
func getImgs(url string) (urls []string) {
	// GetPageStr()函数根据url获取html内容对应的字符串表示
	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()
	// 读取页面内容
	pageBytes, err := ioutil.ReadAll(resp.Body)
	HandleError(err, "ioutil.ReadAll")
	// 使用string()函数将字节切片转为字符串
	pageStr = string(pageBytes)
	return pageStr
}
 类似资料: