基于Python的抖音网页版视频点赞数增长趋势追踪系统设计与实现

一、问题背景

短视频运营中存在一个普遍盲区:创作者只能通过抖音后台查看当前时刻的累计指标,无法获取指标的时序演化过程。然而,抖音推荐系统采用多级流量池递进机制------视频发布后先进入初始流量池(约200-500次曝光),系统根据该阶段的完播率、互动率决定是否推入下一级流量池。这个决策点反映在指标曲线上,就是一个显著的增速拐点。 如果能完整记录视频发布后24-48小时内的点赞增速变化,就能逆向定位流量池跃迁节点,分析哪些内容特征触发了二次分发。

本文目标:设计并实现一个基于Python的自动化采集与分析系统,定时追踪抖音网页版视频的点赞数、评论数、分享数,通过时间序列差分计算增速与加速度,定位流量池跃迁拐点并输出可视化报告。

二、系统架构与环境

系统采用四层分离架构:

  • 采集层 :httpx 请求抖音视频页,解析内嵌 SSR 数据提取 diggCount/commentCount/shareCount。集成亿牛云隧道代理实现请求级 IP 轮换。
  • 存储层 :SQLite + SQLAlchemy ORM,append-only 快照模式,每条记录 (video_id, timestamp, digg_count, comment_count, share_count)
  • 调度层:schedule 库周期触发,默认5分钟间隔,内置异常隔离。
  • 分析层:pandas 一阶/二阶差分 + z-score 异常检测,plotly 输出交互式趋势图。
bash 复制代码
uv init douyin-trend-tracker && cd douyin-trend-tracker
uv add httpx[http2] sqlalchemy schedule plotly pandas

httpx 而非 requests,因 HTTP/2 多路复用在代理隧道场景下连接复用率更高;SQLite 而非 MySQL,因追踪场景为单线程低频写入(5分钟1次 × 288次/天),嵌入式数据库零运维成本。

三、核心实现

3.1 数据模型

python 复制代码
# models.py
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, Index, create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

Base = declarative_base()

class VideoSnapshot(Base):
    """快照表 ------ append-only,保证时序数据完整性。"""
    __tablename__ = "video_snapshots"
    __table_args__ = (Index("ix_video_time", "video_id", "fetched_at"),)

    id = Column(Integer, primary_key=True, autoincrement=True)
    video_id = Column(String(32), nullable=False)
    digg_count = Column(Integer, nullable=False, default=0)
    comment_count = Column(Integer, nullable=False, default=0)
    share_count = Column(Integer, nullable=False, default=0)
    fetched_at = Column(DateTime, nullable=False, default=datetime.utcnow)

def create_session_factory(db_path="trend.db"):
    engine = create_engine(f"sqlite:///{db_path}", connect_args={"check_same_thread": False})
    Base.metadata.create_all(engine)
    return sessionmaker(bind=engine)

3.2 采集层

抖音视频页 URL 为 https://www.douyin.com/video/{aweme_id},HTML 中嵌入 SSR 数据(window._ROUTER_DATARENDER_DATA 标签),包含完整视频元信息 JSON。

python 复制代码
# fetcher.py
import re, json, logging, httpx
from urllib.parse import unquote
from typing import Any

logger = logging.getLogger(__name__)
_P_ROUTER = re.compile(r'window\._ROUTER_DATA\s*=\s*(\{.*?\})\s*</script>', re.DOTALL)
_P_RENDER = re.compile(r'id="RENDER_DATA"[^>]*>(.*?)</script>', re.DOTALL)

def _extract_render_data(html: str) -> dict[str, Any] | None:
    """提取页面内嵌 SSR 数据 JSON,支持两种注入方式。"""
    for pattern, url_decode in [(_P_ROUTER, False), (_P_RENDER, True)]:
        if m := pattern.search(html):
            try:
                raw = unquote(m.group(1)) if url_decode else m.group(1)
                return json.loads(raw)
            except json.JSONDecodeError:
                continue
    return None

def _deep_find(node: Any, key: str) -> Any:
    """递归检索嵌套 JSON 字段。抖音前端改版会导致嵌套层级变化,
    硬编码 JSONPath 在改版后立即失效,递归搜索以 O(n) 开销换取鲁棒性。"""
    if isinstance(node, dict):
        if key in node:
            return node[key]
        for v in node.values():
            if (r := _deep_find(v, key)) is not None:
                return r
    elif isinstance(node, list):
        for item in node:
            if (r := _deep_find(item, key)) is not None:
                return r
    return None

def fetch_video_metrics(video_id: str, proxy_url: str | None = None) -> dict[str, int] | None:
    """采集单个视频的点赞/评论/分享指标。"""
    url = f"https://www.douyin.com/video/{video_id}"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
        "Referer": "https://www.douyin.com/",
        "Accept-Language": "zh-CN,zh;q=0.9",
    }
    proxies = {"http://": proxy_url, "https://": proxy_url} if proxy_url else None
    with httpx.Client(http2=True, proxies=proxies, timeout=15, follow_redirects=True) as client:
        resp = client.get(url, headers=headers)
        if resp.status_code != 200:
            logger.error("请求失败 status=%d vid=%s", resp.status_code, video_id)
            return None
        data = _extract_render_data(resp.text)
        if data is None:
            logger.error("渲染数据提取失败 vid=%s", video_id)
            return None
    return {
        "digg_count": _deep_find(data, "diggCount") or 0,
        "comment_count": _deep_find(data, "commentCount") or 0,
        "share_count": _deep_find(data, "shareCount") or 0,
    }

3.3 代理层:亿牛云隧道代理集成

抖音对网页版部署了多层反爬,IP 维度频控是第一道防线:同一出口 IP 超阈值请求后,依次返回验证码页、403、直至 IP 封禁。追踪多条视频时,纯本地 IP 会迅速触发风控。

亿牛云隧道代理(Tunnel Proxy)是此类场景的高效方案。 其核心机制:客户端连接固定入口地址(host:port),代理服务端每次请求自动从底层 IP 池选取出口 IP,对客户端完全透明------消除了自建 IP 池的可用性检测、失效剔除、负载均衡三项运维成本。

python 复制代码
# proxy_config.py
import os, logging
logger = logging.getLogger(__name__)

def get_proxy_url() -> str:
    """构造亿牛云隧道代理 URL,凭据通过环境变量注入避免硬编码。"""
    user, pwd = os.getenv("YNY_PROXY_USER", ""), os.getenv("YNY_PROXY_PASS", "")
    host = os.getenv("YNY_PROXY_HOST", "tunnel.yiniuyun.com")
    port = os.getenv("YNY_PROXY_PORT", "8000")
    if not user or not pwd:
        logger.warning("未配置代理凭据,降级为直连")
        return ""
    return f"http://{user}:{pwd}@{host}:{port}"

工程实践要点:

  1. 凭据隔离 :代理账密走环境变量,.env 纳入 .gitignore,禁止源码硬编码。
  2. 高匿代理 :必须选 High Anonymity 类型,不添加 X-Forwarded-For 头,透明/匿名代理会暴露代理链路,等同未使用。
  3. 并发与抖动 :实测 3-5 RPS 是兼顾效率与安全的区间。采集间隔加入随机抖动(time.sleep(random.uniform(3, 8))),使请求时序呈泊松特征,降低行为检测识别概率。

3.4 调度层

python 复制代码
# scheduler.py
import time, random, logging, schedule
from datetime import datetime
from typing import Final
from models import create_session_factory, VideoSnapshot
from fetcher import fetch_video_metrics
from proxy_config import get_proxy_url

logger = logging.getLogger(__name__)
WATCH_VIDEOS: Final[list[str]] = ["7345xxxxx", "7332xxxxx"]
_session_factory = create_session_factory()
_proxy_url = get_proxy_url()

def _snapshot_single(video_id: str) -> bool:
    try:
        metrics = fetch_video_metrics(video_id, _proxy_url)
    except Exception as exc:
        logger.exception("采集异常 vid=%s", video_id)
        return False
    if metrics is None:
        return False
    session = _session_factory()
    try:
        session.add(VideoSnapshot(video_id=video_id, fetched_at=datetime.utcnow(), **metrics))
        session.commit()
        return True
    except Exception:
        session.rollback()
        return False
    finally:
        session.close()

def snapshot_all() -> None:
    for vid in WATCH_VIDEOS:
        _snapshot_single(vid)
        time.sleep(random.uniform(3.0, 6.0))

def run(interval_minutes: int = 5) -> None:
    snapshot_all()
    schedule.every(interval_minutes).minutes.do(snapshot_all)
    while True:
        schedule.run_pending()
        time.sleep(1)

3.5 分析层:差分计算与拐点检测

采用三级计算体系:一阶差分(增量)、二阶差分(加速度)、z-score 异常检测(拐点判定)。

python 复制代码
# analyzer.py
import pandas as pd
from sqlalchemy import select
from models import create_session_factory, VideoSnapshot

def load_trend(video_id: str, db_path="trend.db") -> pd.DataFrame:
    """加载快照,计算增量/增长率/加速度,用 z-score 检测拐点。"""
    session = create_session_factory(db_path)()
    try:
        rows = session.execute(
            select(VideoSnapshot).where(VideoSnapshot.video_id == video_id)
            .order_by(VideoSnapshot.fetched_at)
        ).scalars().all()
    finally:
        session.close()

    df = pd.DataFrame([{"time": r.fetched_at, "digg": r.digg_count,
                        "comment": r.comment_count, "share": r.share_count} for r in rows])
    if df.empty:
        return df

    df["digg_delta"] = df["digg"].diff()                          # 一阶差分:增量
    df["growth_rate"] = df["digg_delta"] / df["digg"].shift(1)   # 相对增长率
    df["acceleration"] = df["digg_delta"].diff()                 # 二阶差分:加速度

    # z-score 检测:加速度超过 μ+2σ 判定为统计显著拐点
    threshold = df["acceleration"].mean() + 2 * df["acceleration"].std()
    df["is_surge"] = df["acceleration"] > threshold
    return df

def plot_trend(df: pd.DataFrame, video_id: str) -> str:
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots
    fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
                        subplot_titles=("点赞总量", "采集间隔增量"))
    fig.add_trace(go.Scatter(x=df["time"], y=df["digg"], name="点赞数"), row=1, col=1)
    fig.add_trace(go.Bar(x=df["time"], y=df["digg_delta"], name="增量"), row=2, col=1)
    surges = df[df["is_surge"]]
    if not surges.empty:
        fig.add_trace(go.Scatter(x=surges["time"], y=surges["digg"], mode="markers",
                    marker=dict(size=10, color="red", symbol="star"), name="拐点"), row=1, col=1)
    fig.update_layout(title=f"视频 {video_id} 点赞增长趋势", height=650)
    path = f"trend_{video_id}.html"
    fig.write_html(path)
    return path

四、运行与结果解读

bash 复制代码
export YNY_PROXY_USER="your_username"
export YNY_PROXY_PASS="your_password"
uv run python scheduler.py   # 启动采集
uv run python analyzer.py    # 生成趋势图

趋势图上半区为点赞总量曲线(通常呈先快后缓的对数增长),下半区为增量柱状图。红色星形标记为 z-score 检测的增速拐点------若拐点出现在发布后2-4小时且增量为前序均值5倍以上,高度对应流量池跃迁事件。将拐点时间戳与视频内容结构(前3秒钩子、叙事节奏)对照分析,可逆向推断触发二次分发的有效内容特征。

五、关键风险与合规

1. 渲染数据结构变更 :抖音前端迭代会导致 SSR 数据嵌套层级与字段路径变化。本文用递归检索(_deep_find)替代硬编码 JSONPath,以 O(n) 开销换取容错能力。若扩展至高频场景,可引入路径缓存:首次递归命中后记录路径,后续沿缓存直接取值。

2. 代理层局限性:亿牛云解决 IP 维度频控,但无法应对行为检测------抖音还部署了 TLS 指纹识别(JA3)、Cookie 会话一致性检测、请求时序分析。若 TLS 指纹和请求时序均为机器人特征,IP 轮换无效。应对:Headers 逼近真实浏览器指纹、间隔引入泊松抖动、必要时用 curl_cffi 模拟 TLS 指纹。

3. 合规边界 :本方案采集的是视频页公开聚合指标,不涉及用户隐私。但需严守两条红线:控制频率与规模------追踪自有账号有限视频属合理使用,大规模爬取全站可能违反平台条款甚至《数据安全法》;采集数据不得商业化转售。技术能力应与合规意识对等。

相关推荐
木由里予1 小时前
TEE(Trusted Execution Environment)技术详解
java·linux·开发语言·网络·嵌入式硬件·android-studio
JckOLF04Z1 小时前
C#客户端的异步操作
开发语言·c#
曹牧1 小时前
C#:注释嵌套
开发语言·c#
DogDaoDao1 小时前
FFmpeg 9.0 “Lei“ 正式发布:十年之后,以你之名
ffmpeg·音视频·实时音视频·视频编解码·ffprobe·ffmpeg 9.0
中国搜索直付通1 小时前
游戏车机端支付通道,会是下一个被低估的合规战场吗?
java·大数据·开发语言·人工智能·游戏
LccKyI1 小时前
C# 零基础学习 Day01|基础概念梳理 + 思维导图总结
开发语言·笔记·学习·c#
开开心心_Every1 小时前
电脑文件搜索软件支持内容和拼音搜索
linux·服务器·人工智能·r语言·pdf·音视频·symfony
AKA__Zas1 小时前
芝士算法(位运算)
java·开发语言·算法·leetcode·学习方法
circuitsosk1 小时前
平台整体能力与功能特性设计:分群、联运、ABTest与运营位系统
python·机器学习·搜索引擎·ab测试·vllm·rag检索