用Go语言写一个视频站爬虫?聊聊pp365小视频m.tzcwn.cn那些事儿

最近有个老哥在群里问,说他想用Go语言写个小工具,专门采集pp365小视频m.tzcwn.cn上的视频信息,我一听就乐了,这不是我上个月...

最近有个老哥在群里问,说他想用Go语言写个小工具,专门采集pp365小视频m.tzcwn.cn上的视频信息,我一听就乐了,这不是我上个月折腾过的事儿嘛,当时我也是心血来潮,想研究一下这个站点的数据结构,顺便练练Go的并发抓取,折腾了几天,踩了不少坑,今天就把这些经验掰开了揉碎了讲给你听。

先别急着写代码,你得懂这个站的脾气

pp365小视频m.tzcwn.cn这个站点,说实话,它的页面结构有点“任性”,我第一次用net/http直接去GET首页的时候,返回的HTML让我一度怀疑是不是拿错了数据,它的视频信息不是老老实实写在静态页面里的,而是通过一堆JavaScript动态加载的,你要是直接抓静态HTML,只能拿到一个空壳子。

那怎么办呢?两个思路:

  1. 分析它的XHR请求,找到真正返回视频JSON数据的那个接口;
  2. 无头浏览器(比如chromedp),模拟真人操作,等JS跑完再抓取。

我当时图省事,选了第一条路,打开浏览器的开发者工具,切到Network面板,刷新页面,看着几十个请求哗哗地往下滚,我一个个点开看,终于在一个叫/api/video/list的接口里发现了视频数据的JSON,里面整整齐齐地排列着视频标题、封面图URL、播放地址、时长、点赞数这些字段。

这一步的坑在于:它的接口有签名校验,你在请求头里得带上一个动态生成的X-Sign字段,这个字段是用当前时间戳加一个固定盐值,再经过MD5加密得到的,搞错一次,它就直接返回403 Forbidden,干净利落,不带一点犹豫。

用Go搭建基础的抓取框架

确定了目标,接下来就可以动手写Go代码了,我习惯先把整个流程拆解成几个清晰的步骤,每步一个函数,这样后期维护起来不头疼。

第一步:获取接口数据

Go标准库的net/http就够用了,但为了效率,我一般会封装一个带超时控制的Client,毕竟对方服务器又不是只伺候你一个人,得讲点武德,设置个合理的超时时间,比如10秒。

package main
import (
    "crypto/md5"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "strconv"
    "strings"
    "time"
)
// buildSign 模拟前端的签名生成逻辑
func buildSign(path string, timestamp int64) string {
    // 这里用伪代码示意,真实的盐值你得自己从它的JS文件里扒拉出来
    rawString := fmt.Sprintf("%s:%d:%s", path, timestamp, "your_salt_here")
    hash := md5.Sum([]byte(rawString))
    return hex.EncodeToString(hash[:])
}
// fetchVideoList 抓取指定页码的视频列表
func fetchVideoList(page int) ([]VideoItem, error) {
    baseURL := "https://m.tzcwn.cn/api/video/list"
    fullURL := fmt.Sprintf("%s?page=%d&type=hot", baseURL, page)
    client := &http.Client{Timeout: 10 * time.Second}
    req, err := http.NewRequest("GET", fullURL, nil)
    if err != nil {
        return nil, err
    }
    // 模拟浏览器的请求头,防止被简单拦截
    req.Header.Set("User-Agent", "Mozilla/5.0 ... Chrome/120.0")
    req.Header.Set("Referer", "https://m.tzcwn.cn/index.html")
    timestamp := time.Now().Unix()
    req.Header.Set("X-Sign", buildSign("/api/video/list", timestamp))
    req.Header.Set("X-Timestamp", strconv.FormatInt(timestamp, 10))
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    if resp.StatusCode != 200 {
        return nil, fmt.Errorf("请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body))
    }
    var result ApiResponse
    if err := json.Unmarshal(body, &result); err != nil {
        return nil, fmt.Errorf("JSON解析失败: %v", err)
    }
    if result.Code != 0 {
        return nil, fmt.Errorf("业务错误,代码: %d,消息: %s", result.Code, result.Msg)
    }
    return result.Data.List, nil
}

注意了,这个ApiResponse结构体里,Data.List才是我们真正想要的视频元信息,你从JSON里能看到play_urlcover_url这些字段,但别忘了还有like_countcomment_count,分析用户喜好时用得上。

第二步:解决“懒加载”和分页

这个站的视频列表采用了无限滚动,你向下滚动,它自动加载下一页,对应的API就是?page=2?page=3,但你别傻乎乎地一次发十几个并发请求,它的服务端做了限流,一秒内超过5次,直接封你IP半小时,我当时就吃过这个亏,写完代码一跑,整个宿舍的IP段都被封了,连手机流量都跟着遭殃。

我的建议是:用time.Ticker控制节奏,每抓一页,睡个300毫秒,别用sync.WaitGroup一股脑全上,要优雅,懂吧?

数据存储到本地,用什么格式好?

抓下来的视频信息,保存成JSON文件自然是最简单的,但如果你打算长期维护这个数据,用SQLite比较合适,Go语言这边我用的是modernc.org/sqlite这个驱动,不需要CGO编译,特别省心。

我建的表结构大致是下面这个样子:

CREATE TABLE IF NOT EXISTS videos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    vid TEXT UNIQUE NOT NULL,TEXT,
    cover_url TEXT,
    play_url TEXT,
    duration INTEGER,
    like_count INTEGER DEFAULT 0,
    comment_count INTEGER DEFAULT 0,
    create_time INTEGER
);

vid设成唯一索引,防止重复数据。play_url存的是原始M3U8地址,如果你要下载视频,还得用ffmpeg转封装,这个不在今天的讨论范围,但可以提一嘴。

用Go语言写一个视频站爬虫?聊聊pp365小视频m.tzcwn.cn那些事儿

处理反爬虫策略的“奇技淫巧”

前面提到了签名和限流,其实它还有个隐藏的坑:Cookie验证,有时候你光带请求头不够,它还会校验一个名为__session_id的Cookie,这个Cookie是怎么来的?你得先访问一次它的首页,获取到Set-Cookie,然后把这个Cookie存下来,后续的API请求都带上。

用Go实现就是,得先写个函数“预热”一下:

// warmUp 获取Cookie
func warmUp() (*http.Cookie, error) {
    client := &http.Client{}
    req, _ := http.NewRequest("GET", "https://m.tzcwn.cn/index.html", nil)
    req.Header.Set("User-Agent", "Mozilla/5.0 ...")
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    for _, c := range resp.Cookies() {
        if c.Name == "__session_id" {
            return c, nil
        }
    }
    return nil, fmt.Errorf("未找到session cookie")
}

拿到这个Cookie后,再丢到后面的请求里,如果不带这个,就算你签名算得再准,它也会返回一个空列表,看起来像是数据没加载出来,实际上是在跟你打哑谜。

并发技巧:Go程别乱开

说到Go就绕不开goroutine,但在这个场景下,我反而建议你谨慎使用并发,原因有三点:

  • 服务器压力:人家是小站点,带宽有限,你10个goroutine同时打过去,等于直接把它服务器打崩,这不厚道。
  • IP封禁风险:即使没崩,高频请求也会触发防火墙的自动封禁机制,封了IP,你只能换代理,狼狈得很。
  • 数据一致性:如果你边抓边往数据库里写,并发情况下还得加锁,否则会重复插入,这笔账算下来,并发反而拖慢速度。

所以我的做法是:串行抓取,批量写入,每抓完5页,用一次事务插入到SQLite,这样既快又稳,写入时间也没增加多少。

下面是我用的一个简单的写入函数:

func batchInsert(videos []VideoItem) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback()
    stmt, _ := tx.Prepare("INSERT INTO videos(vid,title,cover_url,play_url,duration,like_count) VALUES(?,?,?,?,?,?)")
    defer stmt.Close()
    for _, v := range videos {
        if _, err := stmt.Exec(v.Id, v.Title, v.CoverUrl, v.PlayUrl, v.Duration, v.LikeCount); err != nil {
            continue // 忽略重复的id
        }
    }
    return tx.Commit()
}

注意看,我在Exec后面直接continue了,这是为了处理那些重复的vid,不让他们打断整个批次的操作。

怎么判断抓下来的数据是不是“好”数据?

抓完数据,你得给自己一个反馈,我一般会用几个维度来判断:

维度 检查项 合格标准
完整性 titleplay_urlcover_url是否为空 无空值,play_urlhttp开头
真实性 视频时长duration是否 > 0 80%以上的数据满足
时效性 create_time是否在一个合理范围 不晚于当前时间,不早于2020年
去重率 vid的唯一性 重复度低于0.1%

如果某页数据的play_url大量为空,说明接口可能又改版了,这时候你得回去重新分析它的前端JS,看看请求参数是不是变了,这活儿有点像考古,一层一层挖。

稍等,我忘了说代理IP的使用

顺嘴提一句,如果你真的要大规模采集,千万别用自己的家庭IP,去弄个代理池,哪怕是最便宜的动态住宅代理,在Go里设置代理很简单:

proxyUrl, _ := url.Parse("http://user:pass@proxy_ip:port")
transport := &http.Transport{
    Proxy: http.ProxyURL(proxyUrl),
}
client := &http.Client{
    Transport: transport,
    Timeout: 10 * time.Second,
}

测试代理是否可用,就发个请求给httpbin.org/ip,看看返回的IP是不是你设置的那个,能用就接着跑,不能用就换下一个,写个循环测试,这事儿不费劲,但能省你很多麻烦。

最后分享一个调试小技巧

你在写代码的时候,难免遇到返回数据不对劲的情况,别急着打fmt.Println看日志,我一般会这么干:把响应体的原始数据先存成一个.txt文件。

os.WriteFile("debug_response.txt", body, 0644)

然后直接用编辑器打开,看看JSON的层级结构,或者看看里面是不是混进去了什么HTML片段,有时候它错误提示不是JSON格式,而是个纯文本的“禁止访问”,如果不存下来看,你在终端里看着那一串乱码,问题不好定位。

这招对于这类“不太老实”的站点特别管用,写代码就是一个不断跟对方博弈的过程,别恼火,慢慢来,总能把数据掏出来。

好了,从分析接口到写Go代码,再到处理各种防爬细节,基本就是这么个流程,你手头那台电脑够跑这代码了,别浪费,试试看。

本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://hljbesthome.com/qiche/2273.html

(13)

文章推荐

发表回复

本站作者才能评论

评论列表(4条)

  • kyadmin
    kyadmin 2026-08-15

    我是be365的签约作者“kyadmin”!

  • kyadmin
    kyadmin 2026-08-15

    希望本篇文章《用Go语言写一个视频站爬虫?聊聊pp365小视频m.tzcwn.cn那些事儿》能对你有所帮助!

  • kyadmin
    kyadmin 2026-08-15

    本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名

  • kyadmin
    kyadmin 2026-08-15

    本文概览:最近有个老哥在群里问,说他想用Go语言写个小工具,专门采集pp365小视频m.tzcwn.cn上的视频信息,我一听就乐了,这不是我上个月...

    联系我们

    工作时间:周一至周五,9:30-18:30,节假日休息

    关注我们