简介:这是一份基于Python的抖音移动端爬虫学习项目,核心思路是利用Appium驱动Genymotion模拟器中的抖音App、配合Mitmproxy拦截应用与服务器的通信流量,再通过Python脚本完成请求解析与数据提取,适合想实战移动应用爬虫、掌握App自动化测试与流量分析技术的Python开发者。资源压缩包仅4KB,包含3个文件:两个Python脚本分别负责自动化控制与数据下载,加上一份README说明文档,体积虽小但清晰展示了环境搭建、代理配置与脚本调用流程。已有455人学习下载,对于希望快速上手抖音数据抓取、理解移动端反爬应对策略的学习者,这份项目能提供可直接参考的代码骨架和调试思路,进一步拓展到其他App爬虫场景。 做爬虫这一年多,我踩过最大的坑就是把所有精力都放在“怎么绕过对方限制”上,结果项目上线没两周就被对方策略一改直接打回原形。后来我才慢慢意识到,像“DouYin_Spider”这种题目看起来是爬虫项目,实际上拼的是三件事:接口理解深度、数据落地方式、还有对平台规则的敬畏。这个项目我前后折腾了一个多月,从纯 requests 到接入 session 管理,从直接解析 JSON 到统一入库,最后稳定跑了两周没出大问题。今天就把整个思路和踩坑记录完整写下来,给同样想做短视频数据采集的朋友一个参考。
声明:本文所有内容仅用于个人学习与技术研究,请遵守目标平台用户协议与相关法律法规,合理控制请求频率,不要将采集数据用于商用或任何侵犯他人权益的场景。
1. 项目定位与整体思路拆解
1.1 先想清楚:这个项目到底要解决什么问题
很多新手拿到“抖音爬虫”这个需求,第一反应就是“模拟登录、翻页、解析视频链接”三件套,但实际操作起来会发现根本不是这么回事。抖音页面的数据是通过大量的 XHR 异步请求加载的,页面源码里基本看不到核心 JSON 数据,所有关键字段都藏在接口的响应里,而且接口带了一堆签名参数和校验逻辑。换句话说,这个项目的核心难点不是“怎么发起请求”,而是“怎么把请求参数、请求头、Cookie 状态这三样东西完整地模拟出来”。
我做这个项目的初衷很简单:想批量采集某个话题下的视频基础信息,包括标题、作者、点赞数、评论数、发布时间、视频封面图,顺带把无水印视频地址解析出来。这个需求听起来很常规,但真正落地时需要注意的点特别多。比如不同端(Web、移动端、小程序)返回的字段结构完全不同;同一个接口在不同登录态下返回的字段数量也不一样;另外,接口对请求频率特别敏感,稍微快一点就会触发验证码或者返回一段非常离谱的“数据为空”。
所以做项目之前,我的建议是先把需求明确到字段级。你到底要哪些字段?数据量级有多大?是一次性采集还是长期增量?这三个问题决定了后续所有技术选型。如果只是做几天的热点分析,那直接请求 Web 接口手动复制数据都行,但如果要做持续观察,那就必须走“cookie 管理 + 接口轮询 + 增量入库”这条正路。
1.2 为什么选“接口模拟”而不是“渲染抓取”或“OCR识别”
短视频平台的数据采集主流方案有三条路:一是用 Selenium 或 Playwright 控制浏览器渲染页面后抓取;二是用 OCR 识别截图;三是直接模拟接口调用。我最后选的是纯接口模拟,原因很实际:浏览器渲染方案单线程跑特别慢,开十个页面内存就飙到 5GB 以上,而且页面里很多数据其实在 iframe 和 shadow DOM 里,XPath 写起来非常痛苦。OCR 就更不靠谱了,数字和中文混排的准确率能达到 90% 就算不错,完全不适合做结构化数据。
接口模拟这条路的核心是“抓包分析”和“参数构造”。你把页面里真实发起的请求参数完整复制下来,用代码模拟同样的请求头、同样的参数顺序、同样的加密算法,就能拿到和浏览器里一模一样的 JSON 响应。这里要特别注意,请求参数里的签名类字段大多是由 JavaScript 动态生成的,你光靠“照着抄参数”是没用的,必须搞清楚它的生成逻辑。
举一个很典型的例子。Web 端接口的 URL 上通常会带a_bogus或X-Bogus这类参数,它们是根据请求路径、设备信息、时间戳、Cookie 里的某项值联合生成的。直接把参数写死会出现一个非常恼人的现象:上午还能跑,下午就失效。原因就是签名算法里带了时间因子,不同时间段算出来的签名不一样。所以做这个项目光会写requests.get(url, params=params)是不够的,你得能读懂一段混淆过的 JavaScript,把它还原成 Python 逻辑。
当然,只谈技术不谈风险是不行的。接口模拟方案最大的雷区是频率控制。你一个普通用户身份的 Cookie,突然在几秒内发起几十个请求,任何一个正常平台都会立刻把你标记为异常。职业一点的做法是加随机延时、随机 User-Agent、定时更换 Cookie,但这些操作都有一个前提:你采集的数据必须是公开的、合法的,而且不涉及任何个人隐私或商业敏感数据。
2. 核心细节解析:抓包、签名与接口设计
2.1 从开浏览器到拿到数据:一次完整抓包流程还原
我先说一个最基础但很多人容易走偏的点:抓包不是把请求记录下来就行,而是要理解“链路”。以 Web 端为例,你打开某个视频的详情页,浏览器会发的请求可分为三类:页面文档请求(HTML)、静态资源请求(JS/CSS/图片)、数据接口请求(XHR)。我们真正关心的只有第三类。
打开开发者工具(F12),切到 Network 面板,刷新页面后输入https://www.iesdouyin.com/或者直接打开某个分享链接,可以看到大量请求。这时候不要急,先过滤Fetch/XHR,再按Name找名字里带aweme、feed、post、comment字样的请求,这些大概率是核心数据接口。点击后可以在Payload或Query String Parameters里看到请求参数,在Response里看到返回的 JSON。
我习惯把每个关键接口的信息整理成一张表,方便后续写代码时对照:
| 接口标识 | 请求方式 | 主要参数 | 返回内容 |
|---|---|---|---|
| post列表 | GET | sec_user_id、max_cursor、count | 用户主页视频列表 |
| 视频详情 | GET | aweme_id、a_bogus | 单条视频完整信息 |
| 评论列表 | GET | aweme_id、cursor、count | 视频下的评论 |
| 话题详情 | GET | ch_id、cursor | 话题下的视频列表 |
这里我特别想强调一个细节:参数顺序也会影响签名校验。有些后端会校验参数的原始顺序,你整理参数时如果用了默认字典,可能会在请求签名正确的情况下仍被拒绝。所以我在代码里都是用OrderedDict或者 Python 3.7+ 的普通 dict(默认保序)来保存参数,并且严格按照抓包工具里看到的顺序来构造。
抓完包之后还要做一步关键动作:确认返回 JSON 里是否包含了你需要的所有字段。像点赞数、评论数、分享数这些在statistics字段里,作者信息在author字段里,视频无水印地址通常在video.play_addr.url_list里。但需要注意,play_addr的 URL 经常带有过期时间戳,你存下来隔几天再访问很可能就 403 了。要想长期保存,拿到 URL 后要尽快下载到本地。
2.2 签名参数背后的“黑盒”:怎么处理遇到的加密逻辑
签名参数是这类项目里最容易劝退新手的坎。我第一次拿到某个接口地址时,看到 URL 上有十几个参数,其中a_bogus和msToken两个值长得完全不像正常人能猜出来的东西,瞬间就头大了。后来琢磨了几天,总结出一套“能跑就行但不瞎搞”的处理方式。
先说msToken。这个值本质上是一段服务端下发的身份标识,一般在 Cookie 里能看到,也会在某个接口的响应里自动更新。它的有效期比较长,个别场景下能持续几小时甚至几天。最简单的处理方法是:在浏览器里访问一次页面,把 Cookie 里msToken的值复制出来,写死在代码的请求头里。这种方式的好处是立刻能跑,坏处是失效之后你得手动换一次。
再说a_bogus。这个参数在新版接口里几乎是必带的,而且它和User-Agent、Cookie、请求路径、时间戳都有关系。我踩过的坑是:同一个 URL,我在浏览器里复制下来的a_bogus放进代码里能用,但只要改一个参数值(比如翻页的cursor),整个签名就失效了。后来才明白,a_bogus是对“当前完整请求”的签名,不是对“某个固定路径”的签名,任何参数变化都会导致签名不一致。
处理a_bogus比较稳妥的方案是找到生成它的 JavaScript 文件,把对应函数用 Python 重写一遍。我不建议直接把整段 JS 丢给execjs去执行,因为抖音的 JS 体量非常大,而且很多地方用了动态代码生成,直接执行很容易内存溢出或报错。我当时是在 JS 里搜索a_bogus关键字,定位到一个核心函数,然后手动把它的逻辑翻译成了 Python。这个过程确实费时间,但做完之后稳定性好很多,再也不怕签名过期了。
如果你不想扣 JS,还有一个很取巧的办法:找那些不需要a_bogus的接口。比如某些移动端 H5 页面或分享页面的接口,校验等级比 Web 端低,参数里只有device_platform和aid,用固定参数就能请求到数据。这种接口适合做小批量数据采集,不适合做重负载项目。我自己的项目后来也是 Web 端接口和 H5 接口混合着用,哪个稳定用哪个,毕竟目标是拿到数据,不是跟对方的安全团队较劲。
3. 实操过程与核心环节实现
3.1 环境准备与依赖安装
正式写代码之前先把环境准备好,这个项目我推荐用 Python 3.9 以上版本,依赖方面不需要太多重型库,核心就是requests和pandas,额外加一个faker用来生成随机 User-Agent,一个retry装饰器用来处理临时网络异常。
pip install requests pandas faker retry有一个很容易被忽略的小点:项目目录下一定要单独建一个cookies.txt文件,把浏览器里复制出来的 Cookie 字符串整体放进去。不要把 Cookie 写死在代码里,因为 Cookie 频繁更换是常态,写在文件里方便每次启动时读取。我自己的代码里固定先读 Cookie,再拼装请求头。
import requests from faker import Faker fake = Faker() def load_cookie(path="cookies.txt"): with open(path, "r", encoding="utf-8") as f: return f.read().strip() def build_headers(referer: str = "") -> dict: return { "User-Agent": fake.user_agent(), "Referer": referer or "https://www.douyin.com/", "Cookie": load_cookie(), "Accept": "application/json, text/plain, */*", }这里用Faker生成 User-Agent 有一个好处:每次请求都不一样,能有效避免请求头雷同被检测。但要注意,请求头里的User-Agent实际会影响a_bogus这类签名参数,如果你的接口带签名校验,千万不能随机换 UA,必须保持和签名时一致的 UA,否则签名必失效。我实际项目里的做法是:签名接口用固定 UA,非签名接口用随机 UA,二者分开处理。
3.2 请求构造:从单条视频到批量翻页
接下来是核心请求逻辑。我以“获取某个用户主页的视频列表”为例,讲一下完整的代码结构和参数细节。
def fetch_user_posts(sec_user_id: str, max_cursor: int = 0, count: int = 18): base_url = "https://www.douyin.com/aweme/v1/web/aweme/post/" params = { "device_platform": "webapp", "aid": "6383", "channel": "channel_pc_web", "sec_user_id": sec_user_id, "max_cursor": str(max_cursor), "locate_query": "false", "show_live_replay_strategy": "1", "need_time_list": "1", "time_list_query": "0", "whale_cut": "1", "version_name": "0908", "version_code": "170100", "cookie_enabled": "true", "platform": "PC", "downlink": "10", } headers = build_headers() resp = requests.get(base_url, params=params, headers=headers, timeout=10) data = resp.json() return data这里重点解释几个参数的作用。max_cursor是翻页游标,第一页传 0,后续页面从上一页响应的max_cursor字段里取;count不是每页条数,它会被后端忽略或做上限限制,实测传 18 左右最稳;sec_user_id是用户主页 URL 里那串很长的 ID,注意不是数字 UID。
第一次跑这段代码时,大概率会返回一个status_code=0的 JSON,但aweme_list可能是空数组。不用慌,先检查你这几个地方:第一,Cookie 是否有效(打开页面确认你没有登出);第二,sec_user_id是否完整;第三,请求是否触发了滑块验证。把这三个点都排查一遍基本就通了。
拿到响应后,最关键的一步就是解析字段。每条视频的数据结构非常深,我封装了一个方法:
def parse_post(item: dict) -> dict: video = item.get("video", {}) play_addr = video.get("play_addr", {}) urls = play_addr.get("url_list", []) statistics = item.get("statistics", {}) return { "aweme_id": item.get("aweme_id"), "title": item.get("desc", "").strip(), "author": item.get("author", {}).get("nickname", ""), "like_count": statistics.get("digg_count", 0), "comment_count": statistics.get("comment_count", 0), "share_count": statistics.get("share_count", 0), "create_time": item.get("create_time"), "video_url": urls[0] if urls else "", }注意item.get("desc")里的视频标题经常是一长串带话题标签和 @ 的文本,做数据分析前最好先做一轮清洗,去掉#话题#和@用户这种噪音。
3.3 数据存储:别再只用 CSV,搭一个轻量 SQLite
很多教学贴会把采集结果直接存成 CSV,数据量小还好说,一旦到了几万条,CSV 的读写速度和字段管理问题就都暴露了。我这次项目用的是 SQLite,零配置文件、单文件存储、支持 SQL 查询,非常适合本地爬虫项目。
import sqlite3 def init_db(db_path="douyin.db"): conn = sqlite3.connect(db_path) conn.execute(""" CREATE TABLE IF NOT EXISTS videos ( aweme_id TEXT PRIMARY KEY, title TEXT, author TEXT, like_count INTEGER, comment_count INTEGER, share_count INTEGER, create_time INTEGER, video_url TEXT, fetched_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) conn.commit() return conn这里把aweme_id设为主键还有一个额外好处:避免重复插入。每次采集前执行INSERT OR IGNORE,同一视频只保留第一条记录,后面再抓到直接跳过,生产环境跑增量任务也不怕数据膨胀。
3.4 完整采集循环与异常处理
批量翻页的循环逻辑看着简单,但写起来很容易翻车。我提供一份我自己调试过的伪代码框架,你可以照着改成自己的业务场景:
def crawl_user(sec_user_id: str, max_pages: int = 30): conn = init_db() cursor = 0 for page in range(max_pages): try: data = fetch_user_posts(sec_user_id=sec_user_id, max_cursor=cursor) if not data or data.get("aweme_list") is None: print(f"Page {page} failed, response keys: {list(data.keys()) if data else 'None'}") break for item in data["aweme_list"]: parsed = parse_post(item) conn.execute( "INSERT OR IGNORE INTO videos (aweme_id, title, author, like_count, comment_count, share_count, create_time, video_url) VALUES (?,?,?,?,?,?,?,?)", (parsed["aweme_id"], parsed["title"], parsed["author"], parsed["like_count"], parsed["comment_count"], parsed["share_count"], parsed["create_time"], parsed["video_url"]) ) conn.commit() cursor = data.get("max_cursor", 0) has_more = data.get("has_more", 0) if not has_more: break time.sleep(random.uniform(3, 6)) except requests.exceptions.RequestException as e: print(f"Request error: {e}") time.sleep(60) continue except Exception as e: print(f"Unexpected error: {e}") break conn.close()这里有两个关键点。第一,每页抓完必须随机睡眠 3 到 6 秒,这个时间不是随便拍的——它能保证你每分钟的请求数控制在 10 到 20 个之间,属于相对安全的低频请求范围。第二,遇到网络异常不要立刻重试,更不要立刻补包,先睡 60 秒再说,大量案例证明越急越容易被封。
4. 常见问题与排查技巧实录
4.1 响应码 200,但拿不到数据的三种典型场景
JSON 类接口最常见的一种“假成功”现象是:HTTP 状态码 200,响应体却不是你想要的列表。我做这个项目的过程中碰到的可以归纳为三种:
| 现象 | 原因 | 处理方式 |
|---|---|---|
aweme_list为空数组 | Cookie 失效或登录态过期 | 重新打开浏览器复制 Cookie |
返回status_code=0但字段全是默认值 | 触发了风控,后端返回了“降级数据” | 停手至少 30 分钟再试,降低频率 |
| 返回 JSON 里带验证码图片链接 | IP 被临时标记 | 更换出口 IP 或等待 1~2 小时 |
这三种情况里,第二种最坑。因为它不报错、不弹验证码,看起来像是“正常响应”,其实后端已经在给你返回假数据或者说空壳数据了。我的排查方法是:打印aweme_list的len(),如果连续三页都是 0,就立刻中止程序,而不是无限重试。
4.2 签名参数a_bogus频繁失效怎么办
a_bogus失效最典型的报错信息是“请求参数错误”或“签名过期”,而且这种失效往往发生在你稍微改动某个参数之后。我的经验是先在本地写一组“已确认能用”的参数快照,然后一个一个地改动参数做对比测试,快速定位出哪个参数变化影响了签名。
如果你不想做这么细的对比测试,也可以走“纯直连”路线:找一个不要求签名参数的接口,把数据从那边读回来。以我的实际测试结果来看,移动端 H5 接口的device_platform=android和aid=1128组合在某些场景下拿数据比 Web 端更稳定,但返回字段会少一些。这就是典型的“取舍”,没有万能的方案。
4.3 视频下载 403 的根源与处理
好不容易解析到了video_url,结果用requests.get下载的时候直接 403,这是另一个高频雷区。原因非常简单:视频 CDN 的 URL 是带防盗链的,它要求请求头里的Referer和User-Agent与浏览器访问时一致。
解决办法也不是很复杂,把下载请求的Referer固定成视频页面的地址,把User-Agent固定成和解析时一致的值,基本就能正常下载。
def download_video(url: str, save_path: str): headers = { "User-Agent": "Mozilla/5.0 ...", "Referer": "https://www.douyin.com/", } resp = requests.get(url, headers=headers, stream=True, timeout=30) if resp.status_code == 200: with open(save_path, "wb") as f: for chunk in resp.iter_content(chunk_size=8192): f.write(chunk)还有一个细节:段落视频的清晰度越高,文件越大,下载耗时越长。如果批量下载较多,建议先用HEAD请求拿到Content-Length,小于 500KB 的直接跳过不做下载,这样可以省掉大量无意义流量。
5. 合规边界与个人实操体会
5.1 哪些玩法可以做,哪些坚决不能碰
这个话题我在不同社区里反复说过,但还是想讲透。爬虫本身只是一种技术工具,黑白取决于你用来做什么。可以做的是:采集自己账号能看到的数据用于个人学习,比如分析某个话题下视频热度的变化趋势;可以做的是:采集公开视频的基础信息用于非商业用途的研究,控制采集频率,不破解任何付费或隐私内容。不能做的是:绕过登录态获取非公开数据,批量爬取用户个人信息(手机号、微信号等),把数据用来二次贩卖或提供付费查询接口,以及对平台造成实质性压力的大规模并发采集。
我做这个项目时只保留了最基本的信息字段,评论数据和用户详情我完全没碰,原因只有一个:采集范围越窄,风险越小,项目也越容易长期跑下去。
5.2 写在最后的几点经验和可扩展方向
如果要从头再做一遍这个项目,我一定会在一开始就把“Cookie 自动续期”和“IP 轮换”这两个模块写好,而不是等到被封了再补。Cookie 自动续期可以这样设计:每次请求后检查响应头里有没有新的Set-Cookie,有就自动更新本地 cookie 文件,保持 Cookie 新鲜。IP 轮换则是给不同请求分配不同的出口 IP,需要额外基础设施支持,适合数据量特别大的场景,不适合个人小项目。
扩展方向上,这个项目可以继续接上数据可视化:把采集结果导入pandas之后做热度趋势分析、关键词词云、发布时间分布统计,甚至可以做简单的爆款预测。如果你对数据处理感兴趣,还可以把全量数据导出成 Parquet 格式,后面丢给数据分析流程一点都不浪费。
我自己的体会是,爬虫项目最迷人的地方不是“能取到别人取不到的数据”,而是你被迫去了解一个系统的运行逻辑:接口设计、流量控制、反爬思路,每一层都藏着工程师的思考。耐心拆解、细心验证、守住规矩,比任何技巧都重要。
本文还有配套的精品资源,点击获取