使用go语言爬虫库colly爬取绘本音频 - 宝爸宝妈强力支援

699 阅读6分钟

使用go语言爬虫库colly爬取绘本音频 - 宝爸宝妈强力支援

代码地址: bettersun/hello_colly (github.com)

查看绘本附带音频的链接

现在的绘本种类相当丰富,有很多绘本自带音频链接,一般扫码就可以听。

IMG_2759.JPG

十万个为什么——动物世界 (whfwdh.com)

手机扫描绘本上的音频二维码后,复制链接粘贴后可以看到。

IMG_2756.PNGIMG_2758.PNGIMG_2757.PNG

但是每次都扫码,或者听完一个再听下一个,还要手动再选择,也挺麻烦。
所以身为程序猿且比较懒的我,感觉这种不需要权限的音频信息一般都能直接访问到,那就可以找到文件地址,然后下载下来。

在浏览器中打开上面的链接,选择一个音频之后,会跳转到对应音频的播放界面,然后就可以(F12)打开开发者工具查看音频元素,然后就可以下载了。

image.png

能看到音频的地址(即 audio 标签中 source 的 src 属性)是: /uploads/video/20201113183619499.mp3

这个路径是相对于站点的地址,站点是 http://www.whfwdh.com
所以完整的音频 URL 是 http://www.whfwdh.com/uploads/video/20201113183619499.mp3

把这个 URL 粘贴到迅雷的下载地址中就可以下载了。

image.png

爬取音频文件

但是一个链接一个链接的打开下载也比较麻烦。
音频内容数量少还好,如果音频数量多的话,那可太累了。
像上面这个绘本,每一节都是一个单独的音频文件,一本绘本就有27个文件。
而且这个系列有8本。😭

程序猿们肯定都想到了爬虫了。
前几天学习了一个 go 语言的爬虫库 colly ,正好可以拿来试试。

colly 基本概念

Collector

Colly 主要的实体是一个 Collector 对象。当一个 collector 任务运行时,Collector 为绑定回调的运行管理网络通信和响应。要使用 colly ,需要初始化一个 Collector

c := colly.NewCollector()

回调

可以为一个 Collector 绑定不同类型的回调函数,用来控制收集任务或者用来获取信息。在包文档中查看 关联部分

为 Collector 添加回调
c.OnRequest(func(r *colly.Request) {
    fmt.Println("Visiting", r.URL)
})

c.OnError(func(_ *colly.Response, err error) {
    log.Println("Something went wrong:", err)
})

c.OnResponseHeaders(func(r *colly.Response) {
    fmt.Println("Visited", r.Request.URL)
})

c.OnResponse(func(r *colly.Response) {
    fmt.Println("Visited", r.Request.URL)
})

c.OnHTML("a[href]", func(e *colly.HTMLElement) {
    e.Request.Visit(e.Attr("href"))
})

c.OnHTML("tr td:nth-of-type(1)", func(e *colly.HTMLElement) {
    fmt.Println("First column of a table row:", e.Text)
})

c.OnXML("//h1", func(e *colly.XMLElement) {
    fmt.Println(e.Text)
})

c.OnScraped(func(r *colly.Response) {
    fmt.Println("Finished", r.Request.URL)
})
回调的调用顺序

1. OnRequest 在请求前调用

2. OnError 请求期间发生错误时调用

3. OnResponseHeaders 接收到响应头部后调用

4. OnResponse 接收到响应后调用

5. OnHTML 如果接收到的内容是 HTML,则在 OnResponse 后立即调用

6. OnXML 如果接收到的内容是 HTML 或 XML,则在 OnHTML 后立即调用

7. OnScraped 在 OnXML 回调后调用

爬取

基于 basic | Colly (go-colly.org) 改写一个简单的爬取程序爬取上面的音频链接。

package main

import (
   "fmt"
   "github.com/gocolly/colly"
)

func main() {
   // 初始化默认的收集器
   c := colly.NewCollector(
      // 允许访问的域
      colly.AllowedDomains("www.whfwdh.com"),
   )

   // 爬取到页面内的链接 a[href] 时回调
   c.OnHTML("a[href]", func(e *colly.HTMLElement) {
      link := e.Attr("href")

      // 爬取子链接
      //  只会访问允许域下的子链接
      c.Visit(e.Request.AbsoluteURL(link))
   })

   // 爬取到音频文件时回调
   c.OnHTML("audio source[src]", func(e *colly.HTMLElement) {
      src := e.Attr("src")
      fmt.Printf("File src: %s\n", src)
   })

   // 请求链接时回调
   c.OnRequest(func(r *colly.Request) {
      //fmt.Println("Visiting", r.URL.String())
   })

   // 开始爬取
   c.Visit("http://www.whfwdh.com/book/index/index/videoid/65e904a9a-0872-d9c1-417d-759e97bbf8b.html")
}
  1. 首先设置允许访问的域

    // 允许访问的域
    colly.AllowedDomains("www.whfwdh.com")
    

    域是扫码后显示连接最前面的站点,去掉 http:// 的部分。

  2. 以下代码用于递归爬取子链接

    // 爬取到页面内的链接 a[href] 时回调
    c.OnHTML("a[href]", func(e *colly.HTMLElement) {
      link := e.Attr("href")
    
      // 爬取子链接
      //  只会访问允许域下的子链接
      c.Visit(e.Request.AbsoluteURL(link))
    })
    
  3. 以下代码用于爬取到音频文件时进行处理,这里是打印出音频文件的相对 URL (即 audio 标签中的 source 的 src 属性)。

    // 爬取到音频文件时回调
    c.OnHTML("audio source[src]", func(e *colly.HTMLElement) {
      src := e.Attr("src")
      fmt.Printf("File src: %s\n", src)
    })
    
  4. 回调都设置完成后,就可以开始爬取入口页面了。

    // 开始爬取
    c.Visit("http://www.whfwdh.com/book/index/index/videoid/65e904a9a-0872-d9c1-417d-759e97bbf8b.html")
    

    运行一下,可以得到下面的结果:

    File src: /uploads/video/20201116175820731.mp3
    File src: /uploads/video/20201116175814175.mp3
    File src: /uploads/video/20201116175807960.mp3
    File src: /uploads/video/20201116175757991.mp3
    File src: /uploads/video/20201116175747732.mp3
    File src: /uploads/video/20201116175740429.mp3
    File src: /uploads/video/20201116175731998.mp3
    File src: /uploads/video/20201116175719288.mp3
    File src: /uploads/video/20201116175707791.mp3
    File src: /uploads/video/20201116175654934.mp3
    File src: /uploads/video/20201116175635112.mp3
    File src: /uploads/video/20201116175623501.mp3
    File src: /uploads/video/20201116175615656.mp3
    File src: /uploads/video/20201116175519145.mp3
    File src: /uploads/video/20201116175511844.mp3
    File src: /uploads/video/20201116175456805.mp3
    File src: /uploads/video/20201116175447629.mp3
    File src: /uploads/video/20201116175335237.mp3
    File src: /uploads/video/20201116175344792.mp3
    File src: /uploads/video/20201116175353770.mp3
    File src: /uploads/video/20201116175402207.mp3
    File src: /uploads/video/20201116175413998.mp3
    File src: /uploads/video/20201116175421582.mp3
    File src: /uploads/video/20201113183643551.mp3
    File src: /uploads/video/20201113183633773.mp3
    File src: /uploads/video/20201113183626977.mp3
    File src: /uploads/video/20201113183619499.mp3
    

    现在得到的结果是相对于站点的 URL 。

    这个时候,我们也可以复制出来,在每个相对 URL 前面加上站点地址 http://www.whfwdh.com ,然后就可以粘贴到迅雷中批量下载了。

    image.png

自动下载

获取音频文件的完整 URL

首先改造一下爬取到音频时的处理,自动把站点拼接到音频文件的相对 URL 前面。

// 爬取到音频文件时回调
c.OnHTML("audio source[src]", func(e *colly.HTMLElement) {
   src := e.Attr("src")
   // 音频文件 URL
   var fileUrl string
   if strings.Index(src, "http") == 0 {
      fileUrl = src
   } else {
      fileUrl = e.Request.URL.Scheme + "://" + e.Request.URL.Host + src
   }

   fmt.Printf("File URL: %s\n", fileUrl)
})

这里加了个判断,对于本文中的例子不太需要。

  1. src 以 http 开头时,直接使用。
  2. src 不以 http 开头时,拼接上站点地址。

e.Request.URL.Scheme + "://" + e.Request.URL.Host 在这里的结果即 http://www.whfwdh.com
拼接上音频文件的相对 URL 后,结果即 http://www.whfwdh.com/uploads/video/xxxxxxxx xxxx.mp3

下载处理

然后可以写一个下载用的函数来自动保存音频文件。

// 下载文件
func download(fileUrl string) error {
   // 请求数据
   resp, err := http.Get(fileUrl)
   if err != nil {
      return err
   }
   defer resp.Body.Close()

   // 文件名
   fileName := getFileName(fileUrl)
   fmt.Printf("File Name: %s\n", fileName)

   // 创建文件用于保存
   out, err := os.Create(fileName)
   if err != nil {
      panic(err)
   }
   defer out.Close()

   // 将获取的响应流写入到文件流
   _, err = io.Copy(out, resp.Body)
   if err != nil {
      return err
   }

   return nil
}

// 截取文件名
func getFileName(url string) string {
   separator := "/"
   pathIndex := strings.LastIndex(url, separator)

   var name string
   if pathIndex == -1 {
      name = url
   } else {
      path := strings.Split(url, separator)
      name = path[len(path)-1]
   }

   return name
}

下载函数代码参考: Go 语言下载文件 http.Get() 和 io.Copy() - 简单教程,简单编程 (twle.cn)

其中截取文件名的函数用于从 src 中获取文件名(不包含路径)。

然后在上面的回调函数中调用下载函数就能实现自动下载了。

// 爬取到音频文件时回调
c.OnHTML("audio source[src]", func(e *colly.HTMLElement) {
   src := e.Attr("src")
   // 音频文件 URL
   var fileUrl string
   if strings.Index(src, "http") == 0 {
      fileUrl = src
   } else {
      fileUrl = e.Request.URL.Scheme + "://" + e.Request.URL.Host + src
   }

   fmt.Printf("File URL: %s\n", fileUrl)
   // 下载文件
   download(fileUrl)
})

运行一下,所有的音频文件都自动下载了下来。

image.png

通用函数

可以把下载的处理定义为一个单独的函数,然后可以传入多个域和入口页面地址,就能实现同时下载多个页面中关联的音频文件。

当然不同的绘本音频对应的爬取规则是不一样的,可根据实际情况自定义回调函数。

主要代码:

/// 爬取
func crawl(domain []string, url string) {
   // 初始化默认的收集器
   c := colly.NewCollector(
      // 允许访问的域
      colly.AllowedDomains(domain...),
   )

   // 爬取到页面内的链接 a[href] 时回调
   // 略

   // 爬取到音频文件时回调
   // 略

   // 请求链接时回调
   // 略

   // 开始爬取
   err := c.Visit(url)
   if err != nil {
      fmt.Println("error on c.Visit()")
   }
}

主函数调用爬取处理:

func main() {
   var domain []string
   var urls []string

   // 域
   domain = []string{
      "www.whfwdh.com",
   }

   // 爬取的根URL
   urls = []string{
      "http://www.whfwdh.com/book/index/index/videoid/65e904a9a-0872-d9c1-417d-759e97bbf8b.html",
   }

   for _, url := range urls {
      crawl(domain, url)
   }
}

下载处理现在还是同步的,可以使用协程和 channel 配合改为异步,效率更高。


我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿