写点什么

中秋节快到了,确定不爬点月饼送岳母娘?

用户头像
Regan Yue
关注
发布于: 5 小时前
中秋节快到了,确定不爬点月饼送岳母娘?

中秋节快到了,确定不爬点月饼送岳母娘?

最近在学 Go 时,发现 Go 语言写爬虫好像也不错,恰逢中秋节,于是想爬点月饼的图片玩玩,各位也可以爬点送岳母娘啊~


温馨提示:本文是 Go 爬虫的教学博文,不会讨论过多有关 Go 语言写爬虫的重难点,不要担心看不懂,我也会介绍本文中用到的所有知识....如果是大佬,就此止步吧~ 也可以给本菜鸟点个赞再走~

一、获取页面图片链接

我们这里先介绍如何获取一个页面里面的图片链接。原理很简单,就是先利用我们编写的GetHtml函数获取页面源代码,然后利用正则表达式获取图片链接,然后将链接保存到字符串数组里面。


下面展示GetHtml函数:


func GetHtml(url string) string {  resp, _ := http.Get(url)  defer resp.Body.Close()
bytes, _ := ioutil.ReadAll(resp.Body) html := string(bytes) return html}
复制代码


本函数需要注意的是,需要延时关闭resp.Body


下面展示GetPageImgurls函数:


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}
复制代码


因为爬取到的路径是相对路径,所以需要将相对路径前面加上域名、协议等信息形成绝对路径存入字符串数组中,便于以后下载图片。

二、实现同步下载功能

接着我们来实现同步下载功能,我们是将图片以时间戳命名保存到硬盘中。


下面展示DownloadImg函数:


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+"下载失败!")  }}
复制代码


ioutil.WriteFile(filename, imgBytes, 0644)这个imgBytes是图片字节流,下面的代表 r w x 分别是 4 2 1,所以这个 0644 代表拥有文件的用户可读可写,同一组的用户可读,其他用户可读。


  owner   group   other0 - 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 = `<a.+?path="(.+?)">`
复制代码


爬取带有图片链接和 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="(.+)`
复制代码


这个爬虫就初步完成了。赶快爬点送岳母娘吧~



然后我就发现了一个大问题。


就是我发现这个异步下载只能异步下载没一页,并不能并发下载多页的图片。于是要对程序进行修改.......


我们把异步下载函数加上参数wg *sync.WaitGroup


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.01698940101秒表 12秒表 2秒表 33秒表 44秒表 52021-08-25 19:10:33.4452142 +0800 CST m=+5.111008301秒表 6秒表 7秒表 8秒表 9秒表 102021-08-25 19:10:38.4369656 +0800 CST m=+10.102759701END
复制代码


下面来谈谈几个需要注意的事项:


  1. 我们使用等待组时不可以在 wg.Add()中填入负数,不然会导致报错。报错结果如下:

  2. panic: sync: negative WaitGroup counter

  3. 这点需要注意。

  4. WaitGroup 对象不是一个引用类型

  5. 在通过函数传值时需要使用地址,需要通过指针传值,不然程序会出现死锁

八、 Golang 的互斥锁

我们都知道有并发就有并发安全的问题。对于有的变量不能是并发运行访问的。比如银行的存取款业务,假如可以并发进行的话,你想一想你往银行存这个月的工资 200 万,你老婆同一时间在银行取 200 万去做美容。假如不使用,你存完之后发现金额没有变化,你老婆取完钱后发现钱也没有变化。你是慌死了,那你老婆不高兴坏了.......


所以我们这里就需要用到,当一个人访问这个业务时,就给它加上,别人就不能访问了。


看一看这个存钱的例子:


var wg sync.WaitGroupfunc 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



发布于: 5 小时前阅读数: 7
用户头像

Regan Yue

关注

还未添加个人签名 2020.08.12 加入

对Go、Python、网络安全、区块链感兴趣. · 华为云云享专家

评论

发布
暂无评论
中秋节快到了,确定不爬点月饼送岳母娘?