
简介这是一份基于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, paramsparams)是不够的你得能读懂一段混淆过的 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列表GETsec_user_id、max_cursor、count用户主页视频列表视频详情GETaweme_id、a_bogus单条视频完整信息评论列表GETaweme_id、cursor、count视频下的评论话题详情GETch_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(pathcookies.txt): with open(path, r, encodingutf-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, paramsparams, headersheaders, timeout10) data resp.json() return data这里重点解释几个参数的作用。max_cursor是翻页游标第一页传 0后续页面从上一页响应的max_cursor字段里取count不是每页条数它会被后端忽略或做上限限制实测传 18 左右最稳sec_user_id是用户主页 URL 里那串很长的 ID注意不是数字 UID。第一次跑这段代码时大概率会返回一个status_code0的 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_pathdouyin.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_idsec_user_id, max_cursorcursor) if not data or data.get(aweme_list) is None: print(fPage {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(fRequest error: {e}) time.sleep(60) continue except Exception as e: print(fUnexpected error: {e}) break conn.close()这里有两个关键点。第一每页抓完必须随机睡眠 3 到 6 秒这个时间不是随便拍的——它能保证你每分钟的请求数控制在 10 到 20 个之间属于相对安全的低频请求范围。第二遇到网络异常不要立刻重试更不要立刻补包先睡 60 秒再说大量案例证明越急越容易被封。4. 常见问题与排查技巧实录4.1 响应码 200但拿不到数据的三种典型场景JSON 类接口最常见的一种“假成功”现象是HTTP 状态码 200响应体却不是你想要的列表。我做这个项目的过程中碰到的可以归纳为三种现象原因处理方式aweme_list为空数组Cookie 失效或登录态过期重新打开浏览器复制 Cookie返回status_code0但字段全是默认值触发了风控后端返回了“降级数据”停手至少 30 分钟再试降低频率返回 JSON 里带验证码图片链接IP 被临时标记更换出口 IP 或等待 1~2 小时这三种情况里第二种最坑。因为它不报错、不弹验证码看起来像是“正常响应”其实后端已经在给你返回假数据或者说空壳数据了。我的排查方法是打印aweme_list的len()如果连续三页都是 0就立刻中止程序而不是无限重试。4.2 签名参数a_bogus频繁失效怎么办a_bogus失效最典型的报错信息是“请求参数错误”或“签名过期”而且这种失效往往发生在你稍微改动某个参数之后。我的经验是先在本地写一组“已确认能用”的参数快照然后一个一个地改动参数做对比测试快速定位出哪个参数变化影响了签名。如果你不想做这么细的对比测试也可以走“纯直连”路线找一个不要求签名参数的接口把数据从那边读回来。以我的实际测试结果来看移动端 H5 接口的device_platformandroid和aid1128组合在某些场景下拿数据比 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, headersheaders, streamTrue, timeout30) if resp.status_code 200: with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): f.write(chunk)还有一个细节段落视频的清晰度越高文件越大下载耗时越长。如果批量下载较多建议先用HEAD请求拿到Content-Length小于 500KB 的直接跳过不做下载这样可以省掉大量无意义流量。5. 合规边界与个人实操体会5.1 哪些玩法可以做哪些坚决不能碰这个话题我在不同社区里反复说过但还是想讲透。爬虫本身只是一种技术工具黑白取决于你用来做什么。可以做的是采集自己账号能看到的数据用于个人学习比如分析某个话题下视频热度的变化趋势可以做的是采集公开视频的基础信息用于非商业用途的研究控制采集频率不破解任何付费或隐私内容。不能做的是绕过登录态获取非公开数据批量爬取用户个人信息手机号、微信号等把数据用来二次贩卖或提供付费查询接口以及对平台造成实质性压力的大规模并发采集。我做这个项目时只保留了最基本的信息字段评论数据和用户详情我完全没碰原因只有一个采集范围越窄风险越小项目也越容易长期跑下去。5.2 写在最后的几点经验和可扩展方向如果要从头再做一遍这个项目我一定会在一开始就把“Cookie 自动续期”和“IP 轮换”这两个模块写好而不是等到被封了再补。Cookie 自动续期可以这样设计每次请求后检查响应头里有没有新的Set-Cookie有就自动更新本地 cookie 文件保持 Cookie 新鲜。IP 轮换则是给不同请求分配不同的出口 IP需要额外基础设施支持适合数据量特别大的场景不适合个人小项目。扩展方向上这个项目可以继续接上数据可视化把采集结果导入pandas之后做热度趋势分析、关键词词云、发布时间分布统计甚至可以做简单的爆款预测。如果你对数据处理感兴趣还可以把全量数据导出成 Parquet 格式后面丢给数据分析流程一点都不浪费。我自己的体会是爬虫项目最迷人的地方不是“能取到别人取不到的数据”而是你被迫去了解一个系统的运行逻辑接口设计、流量控制、反爬思路每一层都藏着工程师的思考。耐心拆解、细心验证、守住规矩比任何技巧都重要。本文还有配套的精品资源点击获取