使用go语言爬虫库colly爬取绘本音频 - 宝爸宝妈强力支援
代码地址: bettersun/hello_colly (github.com)
查看绘本附带音频的链接
现在的绘本种类相当丰富,有很多绘本自带音频链接,一般扫码就可以听。
手机扫描绘本上的音频二维码后,复制链接粘贴后可以看到。
但是每次都扫码,或者听完一个再听下一个,还要手动再选择,也挺麻烦。
所以身为程序猿且比较懒的我,感觉这种不需要权限的音频信息一般都能直接访问到,那就可以找到文件地址,然后下载下来。
在浏览器中打开上面的链接,选择一个音频之后,会跳转到对应音频的播放界面,然后就可以(F12)打开开发者工具查看音频元素,然后就可以下载了。
能看到音频的地址(即 audio 标签中 source 的 src 属性)是: /uploads/video/20201113183619499.mp3
这个路径是相对于站点的地址,站点是 http://www.whfwdh.com
。
所以完整的音频 URL 是 http://www.whfwdh.com/uploads/video/20201113183619499.mp3
。
把这个 URL 粘贴到迅雷的下载地址中就可以下载了。
爬取音频文件
但是一个链接一个链接的打开下载也比较麻烦。
音频内容数量少还好,如果音频数量多的话,那可太累了。
像上面这个绘本,每一节都是一个单独的音频文件,一本绘本就有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")
}
-
首先设置允许访问的域
// 允许访问的域 colly.AllowedDomains("www.whfwdh.com")
域是扫码后显示连接最前面的站点,去掉
http://
的部分。 -
以下代码用于递归爬取子链接
// 爬取到页面内的链接 a[href] 时回调 c.OnHTML("a[href]", func(e *colly.HTMLElement) { link := e.Attr("href") // 爬取子链接 // 只会访问允许域下的子链接 c.Visit(e.Request.AbsoluteURL(link)) })
-
以下代码用于爬取到音频文件时进行处理,这里是打印出音频文件的相对 URL (即 audio 标签中的 source 的 src 属性)。
// 爬取到音频文件时回调 c.OnHTML("audio source[src]", func(e *colly.HTMLElement) { src := e.Attr("src") fmt.Printf("File src: %s\n", src) })
-
回调都设置完成后,就可以开始爬取入口页面了。
// 开始爬取 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
,然后就可以粘贴到迅雷中批量下载了。
自动下载
获取音频文件的完整 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)
})
这里加了个判断,对于本文中的例子不太需要。
- src 以 http 开头时,直接使用。
- 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)
})
运行一下,所有的音频文件都自动下载了下来。
通用函数
可以把下载的处理定义为一个单独的函数,然后可以传入多个域和入口页面地址,就能实现同时下载多个页面中关联的音频文件。
当然不同的绘本音频对应的爬取规则是不一样的,可根据实际情况自定义回调函数。
主要代码:
/// 爬取
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 配合改为异步,效率更高。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。