☰
Python爬取抖音播放量数据:从抓取到趋势分析实战
2026/10/20 1:32:53 网站建设 项目流程

简介:这是一份围绕Python分析抖音视频播放量的实战项目源码,面向具备Python基础、对网络请求与数据分析感兴趣的开发者与学习者,可用于理解视频热度数据的抓取、清洗、统计与可视化流程,并尝试对播放增长进行模拟预测。压缩包共21个文件,约32KB,以8个py脚本为核心,辅以4个xml与3个json配置、2个txt数据文件及pyc缓存等,结构紧凑,便于直接运行与二次修改。项目涉及requests、BeautifulSoup、re、pandas以及matplotlib或seaborn等常用库,覆盖数据获取、结构化处理、分布统计、趋势绘图与逻辑模拟等环节,能帮助读者掌握从接口请求到图表输出的完整链路。目前已有3879人学习下载,适合作为爬虫与数据分析入门练手,也可为相关业务模拟提供参考思路。

1. 从播放量黑匣子到可复现脚本:这套 Python 方案到底在算什么

刷到一条短视频,播放量卡在 500 不动,你怀疑是内容问题还是账号权重问题。想验证,就得把播放量、点赞、评论、发布时间这些字段拉下来做趋势对比。手动截图记录?三天就坚持不下去了。这套「Python-抖音播放量」资源包解决的正是这个场景:用 Python 把公开可见的播放量相关数据抓下来,落成 CSV,再跑一遍趋势分析。它适合做短视频运营的数据党、想练手爬虫与数据分析的 Python 新手,以及需要批量监控多个账号内容表现的小团队。核心不是「破解」什么,而是把页面上肉眼能看到的数字,变成可排序、可画图、可对比的结构化数据。下面按「能跑起来 → 跑得稳 → 不翻车」的顺序拆一遍。

2. 环境与依赖:把跑通的底线先划出来

2.1 为什么选 requests + pandas 这条轻量路线

这套资源没有上 Scrapy 或 Playwright 这类重武器,主干是 requests 发请求、pandas 做清洗和落盘。选型理由很直接:播放量这类字段在页面返回的 JSON 结构里就能拿到,不需要渲染整个 DOM,上浏览器自动化属于杀鸡用牛刀,还会把内存和启动时间拉高。requests 负责拿数据,pandas 负责把嵌套 JSON 拍平成表格,两个库加起来安装体积小,在普通笔记本上跑几百条记录毫无压力。

常见做法是再配一个轻量解析层,用 json 标准库直接取键,避免引入额外依赖。如果你的目标页面结构复杂、字段藏在多层嵌套里,可以加一个 jmespath 做路径提取,但这不是必需项。我一般会先把原始响应存一份到本地,再离线调试解析逻辑,这样不用反复请求,调试速度能快好几倍。

提示:环境用 Python 3.8 以上即可,3.10 和 3.11 实测都稳。虚拟环境强烈建议开,避免和系统里其他包的版本打架。

2.2 依赖安装与目录结构

先把依赖装好。资源包里通常带一个 requirements.txt,没有的话按下面这份装也够用。

# 创建虚拟环境,隔离依赖 python -m venv venv # 激活:Windows 用 venv\Scripts\activate,macOS/Linux 用下面这行 source venv/bin/activate # 安装核心依赖 pip install requests pandas openpyxl

requests负责网络请求,pandas负责数据处理,openpyxl是 pandas 导出 xlsx 时的引擎,不装的话 to_excel 会直接报错。这三个是底线,缺一个都跑不完整。

目录建议这样组织,后面排查问题时能快速定位:

douyin_playcount/ ├── data/ # 原始响应和最终 CSV 都放这 ├── config.py # 请求头、字段映射、输出路径 ├── fetch.py # 抓取主逻辑 └── analyze.py # 趋势分析

把配置单独抽出来,是为了改请求头或字段名时不用翻主逻辑。很多新手把 URL、headers、字段全写在一个文件里,改一处牵动全身,这是后期维护翻车的常见起点。

2.3 请求头与参数配置:决定你能不能拿到数据

请求头是这类抓取的第一道门槛。缺了关键字段,返回的可能是空结构或者验证页,而不是你要的数据。

# config.py HEADERS = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ), "Referer": "https://www.example.com/", # 换成目标页面所在域 "Accept": "application/json, text/plain, */*", } # 需要抓取的字段,按实际返回结构增删 FIELDS = ["aweme_id", "desc", "create_time", "play_count", "digg_count", "comment_count"] # 输出路径 RAW_DIR = "data/raw" OUT_CSV = "data/playcount.csv"

User-Agent伪装成正常浏览器,Referer告诉服务端请求来源,Accept声明期望的响应类型。这三个字段是最低配置。FIELDS列表决定了最后 CSV 里有哪些列,建议先抓一次原始响应,看清楚键名再定这个列表,不要凭记忆写。

参数部分通常包括翻页游标、每页条数、时间范围。每页条数不要贪大,设成 20 到 50 之间比较稳,太大容易触发频率限制。翻页游标一般从返回结果里取,不要自己硬编码页码。

3. 抓取与解析:从响应到 CSV 的完整链路

3.1 单次请求与响应结构确认

第一步不是写循环,而是先发一次请求,把返回结构看清楚。这一步偷懒,后面解析全是猜。

# fetch.py import requests import json import os from config import HEADERS, RAW_DIR def fetch_once(url, params=None): """发一次请求,原始响应落盘,返回解析后的 dict""" resp = requests.get(url, headers=HEADERS, params=params, timeout=10) resp.raise_for_status() # 非 200 直接抛错,别让脏数据流下去 os.makedirs(RAW_DIR, exist_ok=True) # 原始响应存一份,方便离线调试解析逻辑 with open(os.path.join(RAW_DIR, "sample.json"), "w", encoding="utf-8") as f: f.write(resp.text) return resp.json()

timeout=10是必须的,不设的话网络卡住会一直挂着。raise_for_status()让非 200 状态直接报错,避免把错误页当成正常数据解析。把原始响应写进data/raw/sample.json,是为了后面调字段路径时不用反复请求,直接读本地文件就行。

拿到 JSON 后,用编辑器打开 sample.json,找到播放量字段所在的层级。常见结构是data -> list -> 每一项 -> statistics -> play_count这种嵌套。把路径记下来,下一步要用。

3.2 字段提取与翻页控制

确认结构后,写提取函数和翻页循环。翻页的关键是拿到下一页的游标,而不是自己拼页码。

def parse_item(item): """从单条记录里提取需要的字段,缺失的填 None""" stats = item.get("statistics", {}) return { "aweme_id": item.get("aweme_id"), "desc": item.get("desc", ""), "create_time": item.get("create_time"), "play_count": stats.get("play_count"), "digg_count": stats.get("digg_count"), "comment_count": stats.get("comment_count"), } def fetch_all(base_url, max_pages=10): """翻页抓取,max_pages 是安全上限,防止死循环""" results = [] cursor = 0 for page in range(max_pages): params = {"cursor": cursor, "count": 20} data = fetch_once(base_url, params) items = data.get("data", {}).get("list", []) if not items: break # 没有更多数据,正常结束 for it in items: results.append(parse_item(it)) # 取下一页游标,取不到就停 cursor = data.get("data", {}).get("cursor") if not cursor: break return results

parse_item用.get()而不是直接下标,是为了字段缺失时不抛 KeyError,填 None 让后续清洗统一处理。max_pages是安全阀,防止游标逻辑出问题时无限翻页。cursor从响应里取,取不到就 break,这是翻页正常结束的判断点。

count设 20 是保守值,想快可以调到 50,但别超过 100,太大容易被限。翻页之间建议加个短暂停顿,下一节讲。

3.3 落盘与数据清洗

抓完的数据先落 CSV,再做清洗。清洗和抓取分开,出问题时能判断是抓错了还是洗错了。

import pandas as pd import time def save_and_clean(records, out_path): df = pd.DataFrame(records) # 去重:同一 aweme_id 只留一条 df = df.drop_duplicates(subset=["aweme_id"]) # 时间戳转可读时间 df["create_time"] = pd.to_datetime(df["create_time"], unit="s", errors="coerce") # 数值列转 int,缺失填 0 for col in ["play_count", "digg_count", "comment_count"]: df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0).astype(int) # 按播放量降序 df = df.sort_values("play_count", ascending=False) df.to_csv(out_path, index=False, encoding="utf-8-sig") return df

drop_duplicates按aweme_id去重,翻页边界处经常出现重复记录,不去重会虚高。unit="s"是因为时间戳通常是秒级,如果是毫秒级要改成unit="ms",这个搞错时间会差到 1970 年。errors="coerce"把无法转换的值变成 NaT 或 NaN,再用fillna(0)兜底,避免整列转换失败。encoding="utf-8-sig"是为了 Excel 打开中文不乱码,这个细节很多人栽过。

注意:play_count字段在某些返回里可能是字符串,pd.to_numeric能自动处理,但如果带单位(如 "1.2万"),需要先写替换逻辑,不能直接转。

4. 避坑与排查:那些让脚本跑一半就挂的细节

4.1 返回空数据或验证页

现象:请求返回 200,但 JSON 里data.list是空的,或者返回的是一段 HTML。

原因:请求头缺关键字段,或者请求频率过高被临时限制。最常见的是User-Agent用了默认的 python-requests,一眼就被识别。

解决:补齐User-Agent、Referer、Accept三个字段,用真实浏览器的值。如果还是空,把请求间隔拉长到 2 到 3 秒,观察是否恢复。不要用固定间隔,加一点随机抖动更稳。

4.2 翻页死循环或提前结束

现象:要么翻了几百页还在翻,要么第一页之后就停了。

原因:游标字段路径取错,或者把游标当成了页码。有些接口的游标是时间戳,有些是加密串,取错位置就会一直返回同一页或直接为空。

解决:在循环里打印每次的cursor值,看是否在变化。如果一直不变,说明路径取错了,回 sample.json 里重新确认层级。如果第一次就为空,检查是不是把has_more之类的布尔字段误当成了游标。

4.3 时间戳转换后日期离谱

现象:转换出来的时间是 1970 年或者 50000 年以后。

原因:单位搞错。秒级时间戳用了unit="ms",或者毫秒级用了unit="s"。

解决:先打印一个原始时间戳,看位数。10 位是秒级,13 位是毫秒级。按位数选单位,别凭感觉。这个坑我见过太多次,转换前先print(df["create_time"].head())看一眼原始值,能省半小时排查。

4.4 CSV 中文乱码

现象:Excel 打开 CSV,中文全是问号或方块。

原因:pandas 默认用 utf-8 写,Excel 在部分系统上按 GBK 读。

解决:导出时用encoding="utf-8-sig",这个带 BOM 的编码 Excel 能正确识别。如果已经写错了,用记事本打开另存为 UTF-8 with BOM 也能救回来。

4.5 数值列出现 NaN 导致统计报错

现象:算平均播放量时结果变成 NaN,或者画图时报类型错误。

原因:某些记录的播放量字段缺失或格式异常,转换后成了 NaN,后续聚合没处理。

解决:在清洗阶段统一fillna(0),并在转换后用df[col].isna().sum()检查还有没有漏网的。如果缺失比例很高,说明字段路径可能取错了,要回头查结构,而不是硬填 0 掩盖问题。

5. 趋势分析与进阶技巧:让播放量数据真正说话

数据落到 CSV 只是起点,能看出趋势才有价值。这一章给一套可复用的分析套路,再补两个让脚本更耐用的技巧。

5.1 按天聚合与环比计算

先把发布时间按天归组,算每天的发布量和平均播放量,再看环比变化。

def daily_trend(df): df = df.copy() df["date"] = df["create_time"].dt.date daily = df.groupby("date").agg( posts=("aweme_id", "count"), avg_play=("play_count", "mean"), total_play=("play_count", "sum"), ).reset_index() # 环比:当天平均播放量相对前一天的变化率 daily["play_pct_change"] = daily["avg_play"].pct_change().round(4) return daily

groupby("date")按天聚合,agg里同时算发布条数、平均播放、总播放。pct_change()算环比,正数说明涨,负数说明跌。round(4)保留四位小数,避免输出一长串浮点尾数。这个表可以直接喂给 matplotlib 画折线,也可以导出给运营看。

5.2 请求重试与断点续抓

网络请求不可能每次都成功,加一层重试能省很多手动重跑的功夫。

from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(): session = requests.Session() retry = Retry( total=3, # 最多重试 3 次 backoff_factor=1, # 退避:1s, 2s, 4s status_forcelist=[429, 500, 502, 503], ) session.mount("https://", HTTPAdapter(max_retries=retry)) return session

total=3是重试上限,backoff_factor=1让每次重试间隔翻倍,避免密集重试加重限制。status_forcelist列出需要重试的状态码,429 是频率限制,5xx 是服务端临时错误。把fetch_once里的requests.get换成session.get,就接上了这套重试。

断点续抓的思路是:每抓完一页就把结果追加写进 CSV,同时记录最后一条的aweme_id。重跑时先读已有 CSV,跳过已抓的 ID。这样即使中途挂了,也不用从头再来。我一般会在data/下放一个progress.txt记游标,比记 ID 更直接。

5.3 字段映射表与常见返回差异

不同页面返回的字段名可能不一样,维护一张映射表能减少改代码的次数。

业务含义常见字段名备选字段名类型
作品 IDaweme_iditem_idstring
描述desctitlestring
发布时间create_timecreate_time_msint
播放量play_countview_countint
点赞量digg_countlike_countint
评论量comment_countcomment_cntint

遇到字段名对不上时,先查这张表,再回原始 JSON 确认。不要直接改解析函数里的键名,改映射表更安全,因为映射表是唯一改动点。

5.4 一个我踩过的坑

有次跑一个批量任务,抓了三百多条,CSV 打开发现播放量全是 0。排查了半天,发现是parse_item里statistics的层级取错了,实际数据在item["stats"]下,.get("statistics", {})返回空字典,所有字段都填了 None,清洗时又统一fillna(0),把错误掩盖成了 0。从那以后我每次写完解析函数,都强制先打印三条原始记录和解析结果做对照,确认字段真的取到了再跑全量。这个习惯帮我省掉了至少三次类似的返工。

希望这套流程能帮你把播放量数据真正用起来,少走几个我走过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询