转载地址:Go语言中文网

说到爬虫,不得不提到我自己写的《Python网络爬虫requests、bs4爬取空姐网图片》,可以在我的csdn看到这篇文章。这个爬虫很简洁,使用requests库发送http请求,使用bs4来解析html元素,获取所有图片地址。但是这个爬虫是单线程爬虫,速度太慢,一分钟只能爬下来300多张图片。所以,编写了Go语言的爬虫,亲测一分钟能爬下来800多张图片,速度提升了好几倍。先看一下效果图:

一、提取相册链接和下一页链接

1.1 提取相册链接

首先,我们查看一下空姐网的网页结构,找到每个人的相册页面。在kongjie.com里面随意翻翻,就能找到热门相册页面,如图:

分析一下该页面结构,提取出每个人的相册页链接。

可以看到,ul下面包含了很多个li元素,每个li元素就是每个人的相册,li元素图片上的链接就是每个人的相册链接。所以我们写出提取ul元素的正则表达式为:

// 用户相册块的正则表达式,用于从相册列表页提取出用户相册块,用户相册块中包含很多个用户的相册链接var peopleUlPattern = regexp.MustCompile(`<div\s+?class="ptw">(?s:.*?)<ul\s+?class="ml\s+?mlp\s+?cl">(?s:(.*?))</ul>`)
然后从ul元素中提取所有相册链接,正则表达式为:

​​​​​​​

// 用户相册的正则表达式,用于从用户相册块提取出用户相册链接,然后就可以进入相册爬取图片了var peopleItemPattern = regexp.MustCompile(`<li\s+?class="d">(?s:.*?)<div\s+?class="c">(?s:.*?)<a\s+?href="(.*?)">`)
有必要说一下,正常情况下,点号"."能匹配除了换行符外的任意字符,但是在html匹配中有很多换行符,我们想让点号能匹配到换行符,我们需要使用"(?s:.)"的形式,(?s:.*?)就表示这后面的点号可以匹配换行符了。其中的.*后面接问号?就表示这是正则表达式的勉强型匹配模式。想要详细了解勉强型匹配模式的可以看这篇文章《Go语言进阶之路(八):正则表达式》。

1.2 提取下一页链接

处理完一页之后需要翻到下一页,所以我们需要提取“下一页”的链接。我们看一下“下一页”所在的元素位置:

“下一页”这个链接在<div class="pgs cl mtm">元素里面的<div class="pg">的元素里的最后一个链接,而且“下一页”这个链接的class="nxt"。所以我们编写出正则表达式为:

​​​​​​​

// 下一个相册列表页链接的正则表达式,用于从相册列表页提取出下一页链接,翻页爬取var nextAlbumPageUrlPattern = regexp.MustCompile(`<div\s+?class="pgs\s+?cl\s+?mtm">(?s:.*?)</label>(?s:.*?)<a\s+?href="(.*?)"\s+?class="nxt">下一页</a>`)

二、进入相册提取图片链接和下一张页面的链接

2.1 提取图片链接

相册能提取了之后,我们进入相册,提取图片链接和下一张图片页面的链接。先来看一下图片浏览页的结构。

可以看到,图片在<div class="photo_pic"那个div元素里面的超链接中,所以我们写出正则表达式为:

​​​​​​​

// 图片链接的正则表达式,用于从图片浏览页面的html内容中提取出图片链接,然后保存图片var imageUrlPattern = regexp.MustCompile(`<div\s+?id="photo_pic"\s+?class="c">(?s:.*?)<a\s+?href=".*?<img\s+?src="(.*?)"\s+?id="pic"`)

同时,我们看到图片浏览页的链接地址中包含了uid和picid,那么,我们就可以在保存图片到本地时,使用uid+picid的方式保存文件名,这样爬取下来的图片就不会重名了。因此,我们提取uid和picid的正则表达式为:

// 用户id和图片id的正则表达式,用于从url中提取用户id和图片id,保存图片时这些id会拼接成图片名var uidPicIdPattern = regexp.MustCompile(`.*?uid=(\d+).*?picid=(\d+).*?`)

2.2 提取下一张图片浏览页的链接

我们在图片浏览页面提取了图片的url,那么浏览图片的时候翻到下一张,我们需要提取“下一张”的链接。看一下“下一张”的网页结构:

下一张这个链接在<div class="pns mlnv vm mtm cl">元素下的最后一个超链接,超链接的几个属性为class="btn" title="下一张",这样就很好提取了,我们写出提取的正则表达式为:

​​​​​​​

// 下一张图片所在的图片浏览页面的链接正则表达式,用于从图片浏览页面提取出下一页链接,翻页爬取var nextImagePageUrlPattern = regexp.MustCompile(`<div\s+?class="pns\s+?mlnv\s+?vm\s+?mtm\s+?cl">(?s:.*?)<a\s+?href="(.*?)"\s+?class="btn".*?title="下一张"(?s:.*?)<img\s+?src".*?"\s+?alt="下一张"`)

我们现在可以提取相册链接和图片链接了,所有正则表达式提取完毕,接下来就是开始爬取网页了。

三、爬取所有相册链接和翻页

先爬取所有相册并翻页。首先就是发起http请求,拿到相册列表页的html内容,提取所有相册链接。先来看一下http请求。

3.1 发起http请求并解析response

我们使用Go语言原生的http库来发起http请求。为了让我们的http请求更像是浏览器发出的,我们为Request添加header属性,设置一下UserAgent和Referer。该部分源代码如下:

定义header:

var headers = map[string][]string{  "Accept":                    []string{"text/html,application/xhtml+xml,application/xml", "q=0.9,image/webp,*/*;q=0.8"},  "Accept-Encoding":           []string{"gzip, deflate, sdch"},  "Accept-Language":           []string{"zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4"},  "Accept-Charset":            []string{"utf-8"},  "Connection":                []string{"keep-alive"},  "DNT":                       []string{"1"},  "Host":                      []string{"www.kongjie.com"},  "Referer":                   []string{"http://www.kongjie.com/home.php?mod=space&do=album&view=all&order=hot&page=1"},  "Upgrade-Insecure-Requests": []string{"1"},  "User-Agent":                []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"},}

设置header和发起http请求,我们封装成了getResponseWithGlobalHeaders函数:

func getReponseWithGlobalHeaders(url string) *http.Response {  req, _ := http.NewRequest("GET", url, nil)  if headers != nil && len(headers) != 0 {    for k, v := range headers {      for _, val := range v {        req.Header.Add(k, val)      }    }  }
  res, err := http.DefaultClient.Do(req)  if err != nil {    panic(err)  }  return res
}

拿到response之后,我们需要对response进行解压缩,并做编码转换。网页返回是gzip压缩内容,Go语言http库拿到的response是没有帮我们做任何解析和转换的,因此,我们需要使用gzip库解压缩。网页返回的编码是gbk,我们需要转换成UTF-8编码,否则会出现乱码,匹配不到我们想要的内容。

这里,我们使用golang.org/x/net/html/charset和golang.org/x/text/transform进行编码转换。这两个包需要下载,可以使用

​​​​​​​

go get -t golang.org/x/net/html/charsetgo get -t golang.org/x/text/transform

下载这两个包。我们解压缩和转码的源代码如下,封装成getHtmlFromUrl函数:

​​​​​​​

func getHtmlFromUrl(url string) []byte {  response := getReponseWithGlobalHeaders(url)
  reader := response.Body  // 返回的内容被压缩成gzip格式了,需要解压一下  if response.Header.Get("Content-Encoding") == "gzip" {    reader, _ = gzip.NewReader(response.Body)  }  // 此时htmlContent还是gbk编码,需要转换成utf8编码  htmlContent, _ := ioutil.ReadAll(reader)
  oldReader := bufio.NewReader(bytes.NewReader(htmlContent))  peekBytes, _ := oldReader.Peek(1024)  e, _, _ := charset.DetermineEncoding(peekBytes, "")  utf8reader := transform.NewReader(oldReader, e.NewDecoder())  // 此时htmlContent就已经是utf8编码了  htmlContent, _ = ioutil.ReadAll(utf8reader)
  if err := response.Body.Close(); err != nil {    fmt.Println("error happened when closing response body!", err)  }  return htmlContent}

3.2 提取相册链接和翻页

拿到正常的http response之后,我们就开始提取相册链接和翻页处理了。

我们使用FindSubmatch匹配相册链接,提取里面匹配组所匹配到的内容。从《Go语言进阶之路(八):正则表达式》文章中我们知道,FindSubmatch会提取正则表达式匹配到的第一个内容和匹配组的内容。

上文我们提到,peopleUlPattern是为了提取相册列表所在的ul元素的内容,这个ul元素里面包含了很多个相册链接。因此我们先提取ul元素:

​​​​​​​

// FindSubmatch查找正则表达式的匹配和所有的子匹配组,这里是查找当前页每个人的相册链接peopleListElement := peopleUlPattern.FindSubmatch(albumHtmlContent)

这里可以看到,如果当前页ul元素里面没有内容,那么我们就要翻到下一页继续提取。如果都没有“下一页”的链接,那么说明爬虫全部爬完了,可以结束了。

if len(peopleListElement) <= 0 {  // 当前页没有相册  fmt.Println("no peopleListElement!, url=", nextUrl)  // 当前页所有用户相册链接解析完毕,翻到下一页  nextAlbumUrl := nextAlbumPageUrlPattern.FindSubmatch(albumHtmlContent)  if len(nextAlbumUrl) <= 0 {    fmt.Println("all albums crawled!")    break  }  nextUrl = string(nextAlbumUrl[1])  continue}

提取了ul元素之后,我们就可以提取ul里面所有li元素中的相册链接了。从《Go语言进阶之路(八):正则表达式》文章中我们知道,FindAllSubmatch会提取正则表达式匹配到的所有内容和所有匹配组的内容。这样我们就能够拿到ul里面所有的相册链接了。拿到相册链接后,我们把链接发送到imagePageUrlChan通道中,用于后文中使用goroutine并发爬取。

// 子匹配组是第二个元素。里面包含了很多用户的相册连接peopleUlContent := peopleListElement[1]peopleItems := peopleItemPattern.FindAllSubmatch(peopleUlContent, -1)if len(peopleItems) > 0 {  for _, peopleItem := range peopleItems {    if len(peopleItem) <= 0 {      continue    }    // 找到了一个用户的相册链接,放入imagePageUrlChan中等待爬取    peopleAlbumUrl := strings.ReplaceAll(string(peopleItem[1]), `&amp;`, "&")    imagePageUrlChan <- peopleAlbumUrl  }
}

当前页ul解析完毕之后,我们就翻页爬取下一页所有的相册链接。

// 当前页所有用户相册链接解析完毕,翻到下一页nextAlbumUrl := nextAlbumPageUrlPattern.FindSubmatch(albumHtmlContent)if len(nextAlbumUrl) <= 0 {  fmt.Println("all albums crawled!")  break}nextUrl = strings.ReplaceAll(string(nextAlbumUrl[1]), `&amp;`, "&")
fmt.Println(nextUrl)

这样,我们解析相册的源码就大功告成了:

// 解析出相册url,然后进入相册爬取图片func parseAlbumUrl(nextUrl string) {  for {    albumHtmlContent := getHtmlFromUrl(nextUrl)
    // FindSubmatch查找正则表达式的匹配和所有的子匹配组,这里是查找当前页每个人的相册链接    peopleListElement := peopleUlPattern.FindSubmatch(albumHtmlContent)    if len(peopleListElement) <= 0 {      // 当前页没有相册      fmt.Println("no peopleListElement!, url=", nextUrl)      // 当前页所有用户相册链接解析完毕,翻到下一页      nextAlbumUrl := nextAlbumPageUrlPattern.FindSubmatch(albumHtmlContent)      if len(nextAlbumUrl) <= 0 {        fmt.Println("all albums crawled!")        break      }      nextUrl = string(nextAlbumUrl[1])      continue    }
    // 子匹配组是第二个元素。里面包含了很多用户的相册连接    peopleUlContent := peopleListElement[1]    peopleItems := peopleItemPattern.FindAllSubmatch(peopleUlContent, -1)    if len(peopleItems) > 0 {      for _, peopleItem := range peopleItems {        if len(peopleItem) <= 0 {          continue        }        // 找到了一个用户的相册链接,放入imagePageUrlChan中等待爬取        peopleAlbumUrl := strings.ReplaceAll(string(peopleItem[1]), `&amp;`, "&")        imagePageUrlChan <- peopleAlbumUrl      }    }    // 当前页所有用户相册链接解析完毕,翻到下一页    nextAlbumUrl := nextAlbumPageUrlPattern.FindSubmatch(albumHtmlContent)    if len(nextAlbumUrl) <= 0 {      fmt.Println("all albums crawled!")      break    }    nextUrl = strings.ReplaceAll(string(nextAlbumUrl[1]), `&amp;`, "&")    fmt.Println(nextUrl)  }  close(imagePageUrlChan)}

四、进入爬取所有图片和翻页,保存图片

4.1 从图片浏览页链接解析出uid和picid

上文提到过,我们要保存图片到本地,同时保证图片名不重复,我们可以从图片浏览页链接中解析uid和picid作为文件名。我们在上文3.2中拿到imagePageUrlChan中的图片浏览页链接,从这个链接中解析即可。

​​​​​​​

// 从当前图片页面url中获取当前图片所属的用户id和图片iduidPicIdMatch := uidPicIdPattern.FindStringSubmatch(imagePageUrl)if len(uidPicIdMatch) <= 0 {  fmt.Println("can not find any uidPicId! imagePageUrl=", imagePageUrl)  continue}uid := uidPicIdMatch[1]   // 用户idpicId := uidPicIdMatch[2] // 图片id

4.2 进入相册爬取图片和翻到下一张

进入相册到达图片浏览页,可以提取出图片链接。我们先获取图片浏览页的html内容,从html里使用FindSubmatch提取图片src属性。

imagePageHtmlContent := getHtmlFromUrl(imagePageUrl)
// redis中不存在,说明这张图片没被爬取过exists := hexists("kongjie", uid+":"+picId)if !exists {  // 获取图片src,即图片具体链接  imageSrcList := imageUrlPattern.FindSubmatch(imagePageHtmlContent)  if len(imageSrcList) > 0 {    imageSrc := string(imageSrcList[1])    imageSrc = strings.ReplaceAll(string(imageSrc), `&amp;`, "&")    saveImage(imageSrc, uid, picId)    hset("kongjie", uid+":"+picId, "1")  }}// 解析下一张图片页面的url,继续爬取nextImagePageUrlSubmatch := nextImagePageUrlPattern.FindSubmatch(imagePageHtmlContent)if len(nextImagePageUrlSubmatch) <= 0 {  continue}nextImagePageUrl := string(nextImagePageUrlSubmatch[1])imagePageUrlChan <- nextImagePageUrl

可以看到,我们这里使用redis去重。如果redis中不存在这张图片的属性,则图片没有被爬取过,接下来就会调用saveImage函数来保存图片。如果redis中存在这个属性,那么这张图片就被爬取过,直接翻到下一页。

hexists源码如下:

// redis链接信息var redisOption = redis.DialPassword("flyvar")                      // redis密码var redisConn, _ = redis.Dial("tcp", "127.0.0.1:6379", redisOption) // 连接本地redis
// 串行访问redis,否则goroutine并发访问redis时会报错var redisLock sync.Mutex
func hexists(key, field string) bool {  redisLock.Lock()  defer redisLock.Unlock()  exists, err := redisConn.Do("HEXISTS", key, field)  if err != nil {    fmt.Println("redis hexists error!", err)  }  if exists == nil {    return false  }  return exists.(int64) == 1}

这里我们使用了开源库redigo来访问redis。redigo可以使用

go get github.com/gomodule/redigo/redis

来下载。使用案例见https://github.com/pete911/examples-redigo。

4.3 保存图片

拿到图片src之后,就可以保存图片了。我们saveImage函数源码如下:

// 保存图片到全局变量saveFolder文件夹下,图片名字为“uid_picId.ext”。// 其中,uid是用户id,picId是空姐网图片id,ext是图片的扩展名。func saveImage(imageUrl string, uid string, picId string) {  res := getReponseWithGlobalHeaders(imageUrl)  defer func() {    if err := res.Body.Close(); err != nil {      fmt.Println(err)    }  }()  // 获取图片扩展名  fileNameExt := path.Ext(imageUrl)  // 图片保存的全路径  savePath := path.Join(SaveFolder, uid+"_"+picId+fileNameExt)  imageWriter, _ := os.OpenFile(savePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)  length, _ := io.Copy(imageWriter, res.Body)  fmt.Println(uid + "_" + picId + fileNameExt + " image saved! " + strconv.Itoa(int(length)) + " bytes." + imageUrl)}

五、创建goroutine并发爬取

5.1 并发爬取

我们使用单线程爬取所有相册链接,然后并发爬取每个相册里面的所有图片并保存。我们使用sync.WaitGroup等待所有goroutine爬取完成,源码如下:

var wg sync.WaitGroup
func main() {  // 创建保存的文件夹  _, err := os.Open(SaveFolder)  if err != nil {    if os.IsNotExist(err) {      _ = os.MkdirAll(SaveFolder, 0666)    }  }
  // 开启CONCURRENT_NUM个goroutine来爬取用户相册中所有图片的动作  wg.Add(ConcurrentNum)  for i := 0; i < ConcurrentNum; i++ {    go getImagesInAlbum()  }
  // 开启单个goroutine爬取所有用户的相册链接  parseAlbumUrl(startUrl)
  // 等待爬取完成  wg.Wait()}

5.2 运行并查看结果

运行一下查看结果,跟文章开头的结果一致:

并发爬取运行起来比Python快多了!

六、遇到的问题

6.1 http返回乱码

一开始直接使用原生http返回的response拿到body内容后,打印出来一直是乱码。发现空姐网返回的内容中Content-Type内容为text/html; charset=gbk,是GBK编码,需要转换到UTF-8才能进行正常处理。

参考了网上使用mahonia库和golang.org/x/text/encoding/simplifiedchinese库进行转换,一直没有解决。后来通过网上《golang http的动态ip代理、返回乱码解决》发现,空姐网返回的html header里面Content-Encoding为gzip内容,即返回内容是压缩过的,需要使用gzip库进行解压缩才能得到html内容。然后才能进行GBK转UTF-8的操作。

解压缩和GBK转换UTF-8的源码如下:

response := getReponseWithGlobalHeaders(url)
reader := response.Body// 返回的内容被压缩成gzip格式了,需要解压一下if response.Header.Get("Content-Encoding") == "gzip" {  reader, _ = gzip.NewReader(response.Body)}// 此时htmlContent还是gbk编码,需要转换成utf8编码htmlContent, _ := ioutil.ReadAll(reader)
oldReader := bufio.NewReader(bytes.NewReader(htmlContent))peekBytes, _ := oldReader.Peek(1024)e, _, _ := charset.DetermineEncoding(peekBytes, "")utf8reader := transform.NewReader(oldReader, e.NewDecoder())// 此时htmlContent就已经是utf8编码了htmlContent, _ = ioutil.ReadAll(utf8reader)

项目源码在Github上,欢迎关注!https://github.com/ychenracing/GoApps/blob/master/src/KongjieSpider/main/kongjie.go

Go语言并发爬虫,爬取空姐网所有相册图片相关推荐

  1. Go语言进阶之路:并发爬虫,爬取空姐网所有相册图片

    上次聊到了<Go语言正则表达式>和<Go语言手撸一个LRU缓存>,这次利用正则表达式来编写一个并发爬虫. 说到爬虫,不得不提到前面写的<Python网络爬虫request ...

  2. python爬虫爬取慕课网中的图片

    我们简单地爬取慕课网中免费课程下的第一页的图片,如想爬取多页图片,可以添加for循环自行实现 python版本:3.6.5 爬取网址:http://www.imooc.com/course/list ...

  3. 在当当买了python怎么下载源代码-python爬虫爬取当当网

    [实例简介]python爬虫爬取当当网 [实例截图] [核心代码] ''' Function: 当当网图书爬虫 Author: Charles 微信公众号: Charles的皮卡丘 ''' impor ...

  4. python爬虫爬取当当网的商品信息

    python爬虫爬取当当网的商品信息 一.环境搭建 二.简介 三.当当网网页分析 1.分析网页的url规律 2.解析网页html页面 书籍商品html页面解析 其他商品html页面解析 四.代码实现 ...

  5. python爬虫爬取知网

    python爬虫爬取知网 话不多说,直接上代码! import requests import re import time import xlrd from xlrd import open_wor ...

  6. [python爬虫]爬取天气网全国所有县市的天气数据

    [python爬虫]爬取天气网全国所有县市的天气数据 访问URL 解析数据 保存数据 所要用到的库 import requests from lxml import etree import xlwt ...

  7. python爬虫网页中的图片_Python爬虫爬取一个网页上的图片地址实例代码

    本文实例主要是实现爬取一个网页上的图片地址,具体如下. 读取一个网页的源代码: import urllib.request def getHtml(url): html=urllib.request. ...

  8. python3爬虫爬取百度贴吧下载图片

    python3爬虫爬取百度贴吧下载图片 学习爬虫时没事做的小练习. 百度对爬虫还是很友好的,在爬取内容方面还是较为容易. 可以方便各位读者去百度贴吧一键下载每个楼主的图片,至于是什么类型的图片,就看你 ...

  9. python爬虫爬取东方财富网股票走势+一些信息

    一.目标 我们的目标是爬取东方财富网(https://www.eastmoney.com/)的股票信息 我的目标是爬取100张股票信息图片 经过实际测试我的爬取范围为000001-000110,000 ...

最新文章

  1. SLF4j+LOG4j
  2. 新一代软件工程的标配:持续集成
  3. Spring+Hibernate+SpringMVC+MySql实现配置多个数据源!
  4. HelloJava,我的第一个Java程序
  5. 这就是数据分析之数据集成
  6. 第十四节(接口(行为))
  7. jdbc连接mysql登录注册_jdbc+mysql+servlet+jsp实现用户注册与登录功能
  8. 中国“新基建”7大产业链全景图!(附500家企业超全名单!)
  9. 修复/lib/ld-linux.so.2: bad ELF interpreter: No such file or directory问题
  10. 吴恩达《机器学习》课程总结(8)_神经网络参数的反向传播算法
  11. python基础(十):异常和断言
  12. manjaro中文输入法已安装但切换不了解决方法
  13. 艾伟:C#类和接口、虚方法和抽象方法及值类型和引用类型的区别
  14. 三种 绘制奈奎斯特曲线 的方法
  15. 阿帕奇服务器配置文件,阿帕奇服务器基本参数配置
  16. iOS 封装Healthkit
  17. vue报错“NavigationDuplicat: Avoided redundant navigation to current location”解决方法
  18. Linux学习(4)-文件颜色,绿色,蓝色,白色,红色等代表的意义
  19. PAKDD2020:阿里巴巴算法大赛中的得与失
  20. HTML中的 DOM 是什么?有什么作用?

热门文章

  1. JAVA设计模式 - 单例模式
  2. leetcodeT14-最长公共前缀(两种解法+图解)
  3. nginx安装到指定目录
  4. JSON 格式化 显示到页面中
  5. 云场景实践研究第52期:畅游
  6. Neural Networks for Machine Learning by Geoffrey Hinton (1~2)
  7. 越狱解决iphone4s外放无声音
  8. EditText 双击才能获取点击事件
  9. Android基础——数据存储
  10. NSMutableString可变字符串