一、问题背景
短视频运营中存在一个普遍盲区:创作者只能通过抖音后台查看当前时刻的累计指标,无法获取指标的时序演化过程。然而,抖音推荐系统采用多级流量池递进机制------视频发布后先进入初始流量池(约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_DATA 或 RENDER_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}"
工程实践要点:
- 凭据隔离 :代理账密走环境变量,
.env纳入.gitignore,禁止源码硬编码。 - 高匿代理 :必须选 High Anonymity 类型,不添加
X-Forwarded-For头,透明/匿名代理会暴露代理链路,等同未使用。 - 并发与抖动 :实测 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. 合规边界 :本方案采集的是视频页公开聚合指标,不涉及用户隐私。但需严守两条红线:控制频率与规模------追踪自有账号有限视频属合理使用,大规模爬取全站可能违反平台条款甚至《数据安全法》;采集数据不得商业化转售。技术能力应与合规意识对等。