玩转 Go HTTP 客户端系列(番外)—— Goroutine + Channel 爬取抖音合集

2023年 10月 5日 125.5k 0

GO 异步并发爬取抖音短视频合集

法律意识

仅作为经验交流,不可用于其他用途!

在进行网络爬虫前,了解和遵守相关法律法规至关重要。在互联网上,有一些指导文件被用来规范爬虫的行为,其中包括 robots.txt 文件。

robots.txt 是一个文本文件,用于向搜索引擎和其他网络爬虫提供关于网站访问权限的指示。它告诉爬虫哪些网页可以被访问,哪些不可以。

如果想要对抖音进行爬虫操作,建议首先查看他的 robots.txt 文件,以了解平台对爬虫的规定。网站:www.douyin.com/robots.txt

确定目标

爬取某个抖音合集,批量下载无水印的短视频保存至本地,这里选择使用了《和老王一起看华语乐坛排行》。

爬取过程

思路分析

先复制下目标 URL,可以观察到 endpoint 是 1、2、3... 对应着相应的集数,这样非常方便我们去遍历 URL 地址:

image.png

打开这个 URL,点击进入是这样的,我们拿到 Media 类型的报文,查看它的 Request URL,这其实就是真正的下载地址,但我们肯定不会手动一个个这样去进行查找并下载,并且这个地址是动态,具有反爬机制的。

image.png

我们尝试复制动态的路径到 HTML 源码中查找,在源码中也拿到了渲染后的 URL 下载路径。

image.png

一次失败的尝试

注意,这个真正的 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 动态生成的内容中,因此使用 GoHTML 解析器无法提取它。在这种情况下,我们可能需要考虑使用其他方法来获取动态生成的内容。一种方法是使用爬虫技术手段来分析页面,但这不是我们的重点。相反,我们可以选择使用更方便的方式,例如模拟浏览器行为并执行 JavaScript 代码,或使用 Headless 浏览器工具。

使用 Chromedp 模拟 Dom 操作

在这里,我们使用了 Gochromedp 库,以控制 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
        }),
    }
}

可以看到同步处理速度还是相对较慢的,但想要的内容我们都已经成功拿到了:

Oct-04-2023 03-54-16.gif

保存本地(同步版本)

紧接着,我们就先浅尝一下,请求保存到本地吧:

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
}

看来已经成功下载到本地了,但是同步的时间实在是太感人了。。。

image.png

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

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论