GO 异步并发爬取抖音短视频合集
法律意识
仅作为经验交流,不可用于其他用途!
在进行网络爬虫前,了解和遵守相关法律法规至关重要。在互联网上,有一些指导文件被用来规范爬虫的行为,其中包括 robots.txt
文件。
robots.txt
是一个文本文件,用于向搜索引擎和其他网络爬虫提供关于网站访问权限的指示。它告诉爬虫哪些网页可以被访问,哪些不可以。
如果想要对抖音进行爬虫操作,建议首先查看他的 robots.txt
文件,以了解平台对爬虫的规定。网站:www.douyin.com/robots.txt
确定目标
爬取某个抖音合集,批量下载无水印的短视频保存至本地,这里选择使用了《和老王一起看华语乐坛排行》。
爬取过程
思路分析
先复制下目标 URL
,可以观察到 endpoint
是 1、2、3... 对应着相应的集数,这样非常方便我们去遍历 URL
地址:
打开这个 URL
,点击进入是这样的,我们拿到 Media
类型的报文,查看它的 Request URL
,这其实就是真正的下载地址,但我们肯定不会手动一个个这样去进行查找并下载,并且这个地址是动态,具有反爬机制的。
我们尝试复制动态的路径到 HTML
源码中查找,在源码中也拿到了渲染后的 URL
下载路径。
一次失败的尝试
注意,这个真正的 URL
虽然在开发者工具中拿到了,但我们可以尝试用代码获取一下:
package main
import (
"fmt"
"net/http"
"golang.org/x/net/html"
)
// 获取动态生成的媒体资源
func main() {
// 请求地址
url := "https://www.douyin.com/collection/7230030857229043749/1"
// 创建 HTTP 客户端
client := http.DefaultClient
// 发送 GET 请求获取抖音内容
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
fmt.Println("创建请求失败:", err)
return
}
// User-Agent请求头
req.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36")
// 发送请求
resp, err := client.Do(req)
if err != nil {
fmt.Println("发送请求失败:", err)
return
}
// 解析网页内容
doc, err := html.Parse(resp.Body)
if err != nil {
fmt.Println("解析网页内容失败:", err)
return
}
// 在解析树中查找第一个标签
var findSourceURL func(*html.Node) string
findSourceURL = func(n *html.Node) string {
if n.Type == html.ElementNode && n.Data == "source" {
for _, attr := range n.Attr {
if attr.Key == "src" {
return attr.Val
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if url := findSourceURL(c); url != "" {
return url
}
}
return ""
}
// 查看结果
sourceURL := findSourceURL(doc)
if sourceURL != "" {
fmt.Println("目标 URL:", sourceURL)
} else {
fmt.Println("未找到标签")
}
}
可想而知,大概率是拿不到预期结果的。这个目标 URL
位于 JavaScript
动态生成的内容中,因此使用 Go
的 HTML
解析器无法提取它。在这种情况下,我们可能需要考虑使用其他方法来获取动态生成的内容。一种方法是使用爬虫技术手段来分析页面,但这不是我们的重点。相反,我们可以选择使用更方便的方式,例如模拟浏览器行为并执行 JavaScript
代码,或使用 Headless
浏览器工具。
使用 Chromedp 模拟 Dom 操作
在这里,我们使用了 Go
的 chromedp
库,以控制 Chromium
浏览器来执行 DOM
操作,从而获取我们所需的内容。
go get -u github.com/chromedp/chromedp
先来个同步版本:
package main
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
)
type Item struct {
id int
url string
title string
}
func GenURLs() []string {
baseURL := "https://www.douyin.com/collection/7230030857229043749/"
urls := make([]string, 0, 39)
for idx := 1; idx 0) {
sources[0].src;
} else {
'未找到标签';
}
`, &urlResult), // 执行 JavaScript 代码并将结果赋值给变量
chromedp.Evaluate(`document.title`, &titleResult), // 执行 JavaScript 代码并将结果赋值给变量
chromedp.ActionFunc(func(ctx context.Context) error {
res.id = func(url string) int {
splits := strings.Split(url, "/")
last := splits[len(splits)-1]
num, _ := strconv.Atoi(last)
return num
}(host)
res.url = urlResult
res.title = titleResult
return nil
}),
}
}
可以看到同步处理速度还是相对较慢的,但想要的内容我们都已经成功拿到了:
保存本地(同步版本)
紧接着,我们就先浅尝一下,请求保存到本地吧:
func DownloadDouyin(idx int, url, title string) {
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
fmt.Printf("Error creating request for %s: %vn", url, err)
return
}
ua := "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
req.Header.Set("User-Agent", ua)
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Error downloading video from %s: %vn", url, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Error downloading video from %s. Status code: %dn", url, resp.StatusCode)
return
}
fileName := fmt.Sprintf("/User/mystic/Documents/video/%02d-%s.mp4", idx, title)
data, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response body for %s: %vn", url, err)
return
}
err = os.WriteFile(fileName, data, 0644)
if err != nil {
fmt.Printf("Error writing video to file for %s: %vn", url, err)
return
}
fmt.Printf("Video %02d downloaded successfullyn", idx)
}
func main() {
parseStartAt := time.Now()
results := ChromeExec()
parseDuration := time.Since(parseStartAt)
fmt.Printf("解析总耗时:%sn", parseDuration) // 解析总耗时:2m6.314165333s
downloadStartAt := time.Now()
for _, item := range results {
DownloadDouyin(item.id, item.url, item.title)
}
downloadDuration := time.Since(downloadStartAt)
fmt.Printf("下载总耗时:%sn", downloadDuration) // 下载总耗时:7m20.820283917s
}
看来已经成功下载到本地了,但是同步的时间实在是太感人了。。。
Goroutine 并发版本
接下来,我们把上面的方式改造成异步协程(Goroutine + Channel)。
每完成一个对 url 的解析,就立马进行文件下载。
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
)
type Item struct {
id int
url string
title string
}
const (
baseURL = "https://www.douyin.com/collection/7230030857229043749/"
maxNumber = 5
)
var (
genURLs = func() []string {
urls := make([]string, 0, maxNumber)
for idx := 1; idx