中秋节快到了,确定不爬点月饼送岳母娘?
中秋节快到了,确定不爬点月饼送岳母娘?
最近在学 Go 时,发现 Go 语言写爬虫好像也不错,恰逢中秋节,于是想爬点月饼的图片玩玩,各位也可以爬点送岳母娘啊~
温馨提示:本文是 Go 爬虫的教学博文,不会讨论过多有关 Go 语言写爬虫的重难点,不要担心看不懂,我也会介绍本文中用到的所有知识....如果是大佬,就此止步吧~ 也可以给本菜鸟点个赞再走~
一、获取页面图片链接
我们这里先介绍如何获取一个页面里面的图片链接。原理很简单,就是先利用我们编写的GetHtml
函数获取页面源代码,然后利用正则表达式获取图片链接,然后将链接保存到字符串数组里面。
下面展示GetHtml
函数:
本函数需要注意的是,需要延时关闭resp.Body
。
下面展示GetPageImgurls
函数:
因为爬取到的路径是相对路径,所以需要将相对路径前面加上域名、协议等信息形成绝对路径存入字符串数组中,便于以后下载图片。
二、实现同步下载功能
接着我们来实现同步下载功能,我们是将图片以时间戳命名保存到硬盘中。
下面展示DownloadImg
函数:
ioutil.WriteFile(filename, imgBytes, 0644)
这个imgBytes
是图片字节流,下面的代表 r w x 分别是 4 2 1,所以这个 0644 代表拥有文件的用户可读可写,同一组的用户可读,其他用户可读。
另外,在处理 strconv.Itoa(int(time.Now().UnixNano()))时,需要将时间戳改为 int 类型因为 itoa 时将 int 类型转为字符串类型,而时间戳是 int64 类型的。
三、实现异步下载功能
有人说用 Go 实现异步下载很容易啊~一行代码就能实现,嘿嘿嘿。没错,我们先看一看怎么实现的。
但是这样,多少张图片就需要开辟多少条协程。
我们应该怎么办呢?
先建立一个管道,容量为 5,这样就可以同时下载张图片,也就是并发量为 5.
然后每次下载前往管道里面写入一个数,下载完就从管道读出一个数,这样就保证每次最多同时只下载 5 张照片。
然后你想到了运行会出现什么问题吗?
对的,我们保存文件是以时间戳命名的,如果异步下载的话,可能多个文件时间戳一致,所以我们得生成随机文件名。
四、生成随机文件名
上面我们说到了要生成随机文件名,下面我们就来写吧~
首先先要生成随机数,我打算在时间戳后面添加一个随机数来避免文件名重复。
先来展示一下生成随机数的代码:
先建立一个互斥锁,然后阻塞一纳秒,然后计算范围内的随机数,然后解开互斥锁,最后返回这个字符串。
接下来的生成随机文件名的函数就比较简单了:
就是生成时间戳和随机数,然后拼接。
五、使用 Title 属性作为文件名
我们是利用正则表达式获取图片链接和图片名 Title 的,刚开始我想是一个正则表达式爬取链接,一个爬取名称,但是有没有可能有图片没有 Title 属性,所以我选择爬取所有的不管是否有 Title 属性的信息。就像这样:
我们先来看看有关的第一段代码:
这段代码是利用正则表达式
爬取带有图片链接和 Title 属性的字符串,然后将 url 和 filename 保存到 Map 中,因为图片链接都是一样长的,所以比较省事这里利用截取字符串就行了,但是 Title 标签就没这么轻松,它的长度是不固定的。那么怎么办呢?
下面展示一下怎么获取 Title 标签内的值吧:
我们是再次使用正则表达式来获取 Title 内的值的。
正则表达式内容如下:
这个爬虫就初步完成了。赶快爬点送岳母娘吧~
然后我就发现了一个大问题。
就是我发现这个异步下载只能异步下载没一页,并不能并发下载多页的图片。于是要对程序进行修改.......
我们把异步下载函数加上参数wg *sync.WaitGroup
然后不在这里 wait,而在主函数里面 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 条,让任何一条子协程发布让主协程结束的命令都不行,因为你无法确定哪一条子协程是最后结束的。所以我们现在用上了等待组。
等待组是什么原理呢?创造一个子协程就登记一下,然后子协程干完活就将其除名,名单除干净了就结束主协程。
我们来看看等待组的有关示例:
这段代码是建立一条协程就使用 wg.Add(1)给等待组加一,然后活干完之后就减一。
WaitGroup 等待一组 goroutine 完成。主 goroutine 调用 Add 来添加要等待的 goroutine 的数量。 然后每个 goroutine 运行并在完成时调用 Done。 同时,Wait 可用于阻塞,直到所有 goroutine 完成。
Add()方法是用来设置等待组中的计数器的值,我们可以理解每个等待组中都有一个计数器,这个计数器可以用来表示这个等待组中要执行的协程数量。如果计数器为零,那么表示被阻塞的协程都被释放了。
Done()方法就是当同步等待组中的某个协程执行完毕后,使同步等待组中的计数器数量减一。
这里一条协程 5 秒结束,另一条协程 10 秒结束,那按理来说应该是 10 秒结束,我们来看看运行结果吧!
下面来谈谈几个需要注意的事项:
我们使用等待组时不可以在 wg.Add()中填入负数,不然会导致报错。报错结果如下:
panic: sync: negative WaitGroup counter
这点需要注意。
WaitGroup 对象不是一个引用类型
在通过函数传值时需要使用地址,需要通过指针传值,不然程序会出现死锁!
八、 Golang 的互斥锁
我们都知道有并发就有并发安全的问题。对于有的变量不能是并发运行访问的。比如银行的存取款业务,假如可以并发进行的话,你想一想你往银行存这个月的工资 200 万,你老婆同一时间在银行取 200 万去做美容。假如不使用锁,你存完之后发现金额没有变化,你老婆取完钱后发现钱也没有变化。你是慌死了,那你老婆不高兴坏了.......
所以我们这里就需要用到锁,当一个人访问这个业务时,就给它加上锁,别人就不能访问了。
看一看这个存钱的例子:
这个例子就是 10 个人每个人给你存 100 块钱。这一百块钱分一百次存。这样存完后我们就有三千块钱了。
我们看一看运行结果:
好像是没问题哦!那我们加大一下存款金额吧。让 10 个人每个人存 1000,这一千块钱分一千次存,这样我们就会得到一万二千块钱,来看一看运行结果吧!
是不是和我们预想得不一样?
这就是出现了并发安全问题。
对于这种问题,我们应该不允许并发访问。
然后我们看看怎么使用互斥锁解决这类问题吧!
这段程序的意义是两个协程同时抢锁,跳舞协程先抢到锁的话,搏达就开始跳舞,然后跳完舞解锁,抢断协程开始抢到锁,然后搏达结束跳舞开始抢断。如果抢断协程先抢到锁的话,搏达就先开始抢断然后再跳舞。
运行结果是
我们可以看到,搏达扔了球才能开始跳舞。这就是锁的功劳,让搏达不至于一边跳舞一边抢断而累趴。
九、最后
代码我已经上传到 Github 了,请自取。
https://github.com/ReganYue/Crawling_yuebing_pics
版权声明: 本文为 InfoQ 作者【Regan Yue】的原创文章。
原文链接:【http://xie.infoq.cn/article/d225f127d1ae54241f41b3f74】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论