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包的官方文档提供了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
}