中秋节快到了,确定不爬点月饼送岳母娘?
中秋节快到了,确定不爬点月饼送岳母娘?
最近在学Go时,发现Go语言写爬虫好像也不错,恰逢中秋节,于是想爬点月饼的图片玩玩,各位也可以爬点送岳母娘啊~
温馨提示:本文是Go爬虫的教学博文,不会讨论过多有关Go语言写爬虫的重难点,不要担心看不懂,我也会介绍本文中用到的所有知识....如果是大佬,就此止步吧~ 也可以给本菜鸟点个赞再走~
一、获取页面图片链接
我们这里先介绍如何获取一个页面里面的图片链接。原理很简单,就是先利用我们编写的函数获取页面源代码,然后利用正则表达式获取图片链接,然后将链接保存到字符串数组里面。
下面展示函数:
func GetHtml(url string) string {
resp, _ := http.Get(url)
defer resp.Body.Close()
bytes, _ := ioutil.ReadAll(resp.Body)
html := string(bytes)
return html
}
本函数需要注意的是,需要延时关闭。
下面展示函数:
func GetPageImgurls(url string) []string {
html := GetHtml(url)
re := regexp.MustCompile(ReImg)
rets := re.FindAllStringSubmatch(html, -1)
imgUrls := make([]string, 0)
for _, ret := range rets {
imgUrl := "https://www.yuebing.com/"+ret[1]
imgUrls = append(imgUrls, imgUrl)
}
return imgUrls
}
因为爬取到的路径是相对路径,所以需要将相对路径前面加上域名、协议等信息形成绝对路径存入字符串数组中,便于以后下载图片。
二、实现同步下载功能
接着我们来实现同步下载功能,我们是将图片以时间戳命名保存到硬盘中。
下面展示函数:
func DownloadImg(url string) {
resp, _ := http.Get(url)
defer resp.Body.Close()
filename := `E:\\code\\src\\day4\\爬取图片\\img\\`+strconv.Itoa(int(time.Now().UnixNano()))+".jpg"
imgBytes, _ := ioutil.ReadAll(resp.Body)
err := ioutil.WriteFile(filename, imgBytes, 0644)
if err == nil{
fmt.Println(filename+"下载成功!")
}else{
fmt.Println(filename+"下载失败!")
}
}
这个是图片字节流,下面的代表r w x分别是4 2 1,所以这个0644代表拥有文件的用户可读可写,同一组的用户可读,其他用户可读。
owner group other
0 - rwx - rwx - rwx
另外,在处理strconv.Itoa(int(time.Now().UnixNano()))时,需要将时间戳改为int类型因为itoa时将int类型转为字符串类型,而时间戳是int64类型的。
三、实现异步下载功能
有人说用Go实现异步下载很容易啊~一行代码就能实现,嘿嘿嘿。没错,我们先看一看怎么实现的。
func DownloadImgAsync(url string) {
go DownloadImg(url)
}
但是这样,多少张图片就需要开辟多少条协程。
我们应该怎么办呢?
chSem = make(chan int,5)
先建立一个管道,容量为5,这样就可以同时下载张图片,也就是并发量为5.
func DownloadImgAsync(url string) {
downloadWG.Add(1)
go func() {
chSem <- 1
DownloadImg(url)
<-chSem
downloadWG.Done()
}()
downloadWG.Wait()
}
然后每次下载前往管道里面写入一个数,下载完就从管道读出一个数,这样就保证每次最多同时只下载5张照片。
然后你想到了运行会出现什么问题吗?
对的,我们保存文件是以时间戳命名的,如果异步下载的话,可能多个文件时间戳一致,所以我们得生成随机文件名。
四、生成随机文件名
上面我们说到了要生成随机文件名,下面我们就来写吧~
首先先要生成随机数,我打算在时间戳后面添加一个随机数来避免文件名重复。
先来展示一下生成随机数的代码:
func GetRandomInt(start,end int) int {
randomMT.Lock()
<- time.After(1 * time.Nanosecond)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
ret := start + r.Intn(end - start)
randomMT.Unlock()
return ret
}
先建立一个互斥锁,然后阻塞一纳秒,然后计算范围内的随机数,然后解开互斥锁,最后返回这个字符串。
接下来的生成随机文件名的函数就比较简单了:
func GetRandomName() string {
timestamp := strconv.Itoa(int(time.Now().UnixNano()))
randomNum := strconv.Itoa(GetRandomInt(100, 10000))
return timestamp + "-" + randomNum
}
就是生成时间戳和随机数,然后拼接。
五、使用Title属性作为文件名
我们是利用正则表达式获取图片链接和图片名Title的,刚开始我想是一个正则表达式爬取链接,一个爬取名称,但是有没有可能有图片没有Title属性,所以我选择爬取所有的不管是否有Title属性的信息。就像这样:

我们先来看看有关的第一段代码:
func GetPageImginfos(url string) []map[string] string {
html := GetHtml(url)
re := regexp.MustCompile(ReImgName)
rets := re.FindAllStringSubmatch(html, -1)
imgInfos := make([]map[string] string,0)
for _,ret := range rets {
imgInfo := make(map[string] string)
imgUrl := "https://www.yuebing.com/"+ret[1]
imgInfo["url"] = imgUrl[0:78]
imgInfo["filename"]=GetImgNameTag(ret[1])
//fmt.Println(imgInfo["filename"])
imgInfos = append(imgInfos, imgInfo)
}
return imgInfos
}
这段代码是利用正则表达式
ReImgName = ``
爬取带有图片链接和Title属性的字符串,然后将url和filename保存到Map中,因为图片链接都是一样长的,所以比较省事这里利用截取字符串就行了,但是Title标签就没这么轻松,它的长度是不固定的。那么怎么办呢?
下面展示一下怎么获取Title标签内的值吧:
func GetImgNameTag(imgTag string) string {
re := regexp.MustCompile(ReTitle)
rets := re.FindAllStringSubmatch(imgTag, -1)
//fmt.Println(rets)
if len(rets) > 0{
return rets[0][1]
}else {
return GetRandomName()
}
}
我们是再次使用正则表达式来获取Title内的值的。
正则表达式内容如下:
ReTitle = `title="(.+)`
这个爬虫就初步完成了。赶快爬点送岳母娘吧~

然后我就发现了一个大问题。
就是我发现这个异步下载只能异步下载没一页,并不能并发下载多页的图片。于是要对程序进行修改.......
我们把异步下载函数加上参数
func DownloadImgAsync(url ,filename string,wg *sync.WaitGroup) {
wg.Add(1)
go func() {
chSem <- 1
DownloadImg(url,filename)
<-chSem
downloadWG.Done()
}()
}
然后不在这里wait,而在主函数里面wait。
这里展示一下主函数。
func main() {
for i:=1;i<=15;i++{
j := strconv.Itoa(i)
url := "https://www.yuebing.com/category-0-b0-min0-max0-attr0-" + j + "-sort_order-ASC.html"
imginfos := GetPageImginfos(url)
for _,imgInfoMap := range imginfos{
DownloadImgAsync(imgInfoMap["url"],imgInfoMap["filename"],&downloadWG)
time.Sleep(500 * time.Millisecond)
}
}
downloadWG.Wait()
}

这样就明显速度快多了。
下面介绍本文中用到的相关知识。
六、 Go并发之CSP并发模型、协程并发
这个爬虫采用了并发下载图片,Go并发采用的是CSP并发模型,而Go使用的是协程。所以我们来谈谈什么是CSP并发模型和什么是协程。
1. 什么是CSP并发模型
CSP 即通信顺序进程、交谈循序程序,又被译为交换消息的循序程序(communicating sequential processes),它是一种用来描述并发性系统之间进行交互的模型。
CSP模型的最大优点是灵活。但是容易出现死锁的情况,且未给予直接的并行支持,并行需要建立在并发的基础之上。
在CSP模型里面,进程间需要经过一种被称为管道来进行通信。
什么是管道,两个并发任务不需要共享内存,而是通过建立一条点对点的管道,数据用完之后,管道立即撤销。有了管道,不需要事先锁,而是需要用数据时建立管道。不需要数据时就撤销管道了。
管道与共享内存之间有很大的区别,内存共享是通过内存来共享内存,而管道是通过通信来共享内存。所以管道通信比内存共享效率要高很多。
2. 协程
coroutine就是协程,也称为go程。通过管道能够实现百万级的并发。如果说线程是抢占式的,那么协程是协作式的。在协程里面,也是通过管道来调度的。解放线程对CPU和内存的开销,线程是先占用CPU和内存后才调度,而协程是通过通信发送信号来调度,协程全是通过管道,由于协程的消耗比线程小很多,所以能够实现百万并发。
在协程中,IO操作时绝大部分时间与CPU无关,这是管道带来的优势,不需要长时间锁住内存,也不需要CPU来做调度。
8G内存的电脑,用JAVA,C来做并发,差不多也就千级并发,而用GO语言,通过管道可以让并发能力得到很大提升。
七、Golang的同步等待组
我们现在开十条子协程,然后当十条子协程全部结束后,主协程立马结束。动动你的小脑袋,想一想应该怎么做?如果是一条子协程的话就很容易实现,当这条子协程结束时让主协程结束就行了。但是我们现在是10条,让任何一条子协程发布让主协程结束的命令都不行,因为你无法确定哪一条子协程是最后结束的。所以我们现在用上了等待组。
等待组是什么原理呢?创造一个子协程就登记一下,然后子协程干完活就将其除名,名单除干净了就结束主协程。
我们来看看等待组的有关示例:
func main() {
fmt.Println(time.Now())
var wg sync.WaitGroup
//起一个协程就加一
wg.Add(1)
go func() {
for i:=0;i<5;i++{
fmt.Println(i)
//相当于阻塞一秒,读到时间
<- time.After(time.Second)
}
fmt.Println(time.Now())
//活干完之后减一
wg.Done()
}()
wg.Add(1)
go func() {
var i int
ticker := time.NewTicker(time.Second)
for{
<- ticker.C
i++
fmt.Println("秒表",i)
if i>9 {
break
}
}
fmt.Println(time.Now())
wg.Done()
}()
//等待组阻塞等待至记录清零为止
wg.Wait()
fmt.Println("END")
}
这段代码是建立一条协程就使用wg.Add(1)给等待组加一,然后活干完之后就减一。
WaitGroup 等待一组 goroutine 完成。主 goroutine 调用 Add 来添加要等待的 goroutine 的数量。 然后每个 goroutine 运行并在完成时调用 Done。 同时,Wait 可用于阻塞,直到所有 goroutine 完成。
Add()方法是用来设置等待组中的计数器的值,我们可以理解每个等待组中都有一个计数器,这个计数器可以用来表示这个等待组中要执行的协程数量。如果计数器为零,那么表示被阻塞的协程都被释放了。
Done()方法就是当同步等待组中的某个协程执行完毕后,使同步等待组中的计数器数量减一。
这里一条协程5秒结束,另一条协程10秒结束,那按理来说应该是10秒结束,我们来看看运行结果吧!
2021-08-25 19:10:28.3511953 +0800 CST m=+0.016989401
0
1
秒表 1
2
秒表 2
秒表 3
3
秒表 4
4
秒表 5
2021-08-25 19:10:33.4452142 +0800 CST m=+5.111008301
秒表 6
秒表 7
秒表 8
秒表 9
秒表 10
2021-08-25 19:10:38.4369656 +0800 CST m=+10.102759701
END
下面来谈谈几个需要注意的事项:
八、 Golang的互斥锁
我们都知道有并发就有并发安全的问题。对于有的变量不能是并发运行访问的。比如银行的存取款业务,假如可以并发进行的话,你想一想你往银行存这个月的工资200万,你老婆同一时间在银行取200万去做美容。假如不使用锁,你存完之后发现金额没有变化,你老婆取完钱后发现钱也没有变化。你是慌死了,那你老婆不高兴坏了.......
所以我们这里就需要用到锁,当一个人访问这个业务时,就给它加上锁,别人就不能访问了。
看一看这个存钱的例子:
var wg sync.WaitGroup
func main() {
var money = 2000
for i:=0;i<10;i++{
wg.Add(1)
go func() {
for j:=0;j<100;j++{
money += 1
}
wg.Done()
}()
}
wg.Wait()
fmt.Println("最终金额",money)
}
这个例子就是10个人每个人给你存100块钱。这一百块钱分一百次存。这样存完后我们就有三千块钱了。
我们看一看运行结果:
最终金额 3000
好像是没问题哦!那我们加大一下存款金额吧。让10个人每个人存1000,这一千块钱分一千次存,这样我们就会得到一万二千块钱,来看一看运行结果吧!
最终金额 10366
是不是和我们预想得不一样?
这就是出现了并发安全问题。
对于这种问题,我们应该不允许并发访问。
然后我们看看怎么使用互斥锁解决这类问题吧!
func main() {
var money = 2000
var mt sync.Mutex
wg.Add(1)
go func() {
fmt.Println("搏达试图抢断")
mt.Lock()
fmt.Println("搏达抢断成功")
money -= 300
<- time.After(10 * time.Second)
mt.Unlock()
fmt.Println("搏达扔了球")
wg.Done()
}()
wg.Add(1)
go func() {
fmt.Println("搏达试图跳舞")
mt.Lock()
fmt.Println("搏达跳舞成功")
money -= 500
<- time.After(10 * time.Second)
mt.Unlock()
fmt.Println("搏达放弃跳舞")
wg.Done()
}()
wg.Wait()
}
这段程序的意义是两个协程同时抢锁,跳舞协程先抢到锁的话,搏达就开始跳舞,然后跳完舞解锁,抢断协程开始抢到锁,然后搏达结束跳舞开始抢断。如果抢断协程先抢到锁的话,搏达就先开始抢断然后再跳舞。
运行结果是
搏达试图抢断
搏达抢断成功
搏达试图跳舞
搏达扔了球
搏达跳舞成功
搏达放弃跳舞
我们可以看到,搏达扔了球才能开始跳舞。这就是锁的功劳,让搏达不至于一边跳舞一边抢断而累趴。
九、最后
代码我已经上传到Github了,请自取。
https://github.com/ReganYue/Crawling_yuebing_pics
