
我前段时间做了一个小项目用 Python 写爬虫去采集抖音上部分博主的主页资料——昵称、签名、粉丝数、作品数这些公开信息。起因很朴素有个朋友想研究某个垂直领域博主的主页数据变化一个个网页复制粘贴实在太蠢于是我用一个晚上把整个链路跑通从找到接口、模拟请求到数据落库。今天把整个过程整理出来既是复盘自己的实现思路也正好给正在学 Python 爬虫的朋友一些可参考的经验。开始前先把话放在前头这个项目只碰用户主动公开的主页信息不碰私信、通讯录、设备信息等任何非公开数据且仅供学习参考切勿用于商业用途。1. 项目需求拆解到底要抓什么为什么值得做1.1 个人资料到底指哪些字段很多人一听爬取个人资料第一反应就是侵犯隐私。其实在抖音这类平台上每个用户的主页本身就是对外公开的任何人都可以通过网页版直接看到博主的昵称、抖音号、简介、头像、粉丝数、关注数、获赞数、作品数这些信息。我们做爬虫本质上只是把人眼能看到的公开页面数据变成结构化文本数据。我这次需要采集的核心字段如下字段名含义我在项目里的用途nickname昵称博主身份识别signature个人简介判断博主的内容定位avatar_url头像链接后续做头像聚类分析follower_count粉丝数观察账号成长趋势following_count关注数判断账号属性total_favorited累计获赞衡量内容受欢迎程度video_count作品数判断更新频率sec_uid加密用户ID拼接主页链接、去重这里要特别注意一个概念sec_uid和uid不是一回事。uid是用户数字IDsec_uid是一串经过加密编码的字符串抖音的网页端和个人主页链接用的基本是sec_uid。早期很多工具叫抖音号转uid在线工具本质就是去解析页面数据里的sec_uid再用它拼出完整主页地址。理解这一点后面写代码时才不会被参数名绕晕。1.2 这个项目真正练到手的三项能力网上很多爬虫教程喜欢拿小说网站、招聘网站举例页面结构简单几乎没有任何反爬。抖音这类平台虽然谈不上铜墙铁壁但该有的防护一样不少拿它练手能让你在真实环境里快速成长。第一项能力是接口分析。你得学会打开浏览器开发者工具在 Network 面板里找到哪条 XHR 请求返回了你想要的数据这是爬虫工程师最基本功。第二项能力是请求构造与伪装。别人的服务器凭什么把数据给你你得让请求看起来像一个正常的浏览器行为User-Agent、Referer、Cookie、请求参数缺一不可。第三项能力是数据解析与清洗。拿到一段嵌套很深的 JSON 后怎么快速提取字段、处理空值、统一格式最后落成 CSV 或数据库这同样需要练习。坦白讲这三项能力放到任何一个 Web 开发或数据分析项目里都通用。不是说会了爬虫就能怎样而是你会更理解浏览器一次点击背后到底发生了什么。1.3 合规边界先说清楚后面代码才会安全我知道有些人看到抖音爬虫三个字就兴奋脑子里全是批量抓取、注册小号、自动养号这些操作。我这里直接把边界画出来越线的行为一概不做只采集用户主动公开的主页信息不碰私信、手机号、通讯录、浏览记录等任何非公开数据。不做撞库、不做批量关注/点赞/评论这类行为既违反平台规则也影响其他用户体验。控制请求频率不让自己的脚本对平台服务器造成压力。不把数据用于商业目的不建立用户画像不批量转售。不绕过登录态获取需要权限才能看到的内容。说白了爬虫本身只是个工具就像一把螺丝刀。用螺丝刀拆开自己的电脑研究硬件结构这叫学习用螺丝刀撬别人家的门锁那就是犯罪。你能学到的 HTTP 知识、JSON 解析能力、异常处理思路完全可以通过低频、公开、学习这三个原则来获得没必要去踩线。2. 技术方案选型与运行环境准备2.1 方案横评requests、Selenium、官方API怎么选做爬虫之前最纠结的一步往往是选什么技术方案。我在这个项目里一开始也犹豫过最后选的是requests理由在下面这张对比表里能看得很清楚。方案优点缺点适用场景requests 手动解析轻量、快、可控性强需要手工处理 Cookie、签名、请求头数据量不大、接口明确、适合学习Selenium / Playwright模拟真人点击能跑复杂交互慢、耗资源、更容易被识别必须要过复杂前端流程时才用官方开放平台 API最合规、最稳定权限受限、需要申请、字段有限有正规授权、做长期稳定数据服务requests之所以胜出是因为它的学习曲线最平滑而且能逼着你搞清楚 HTTP 协议的底层逻辑。比如服务器返回 200 不代表你真的拿到了数据可能返回的是一个验证页面又比如有些参数是动态算出来的少了它请求会直接失败。这些问题用 Selenium 都会被隐藏掉但用requests时你躲不掉只能一个个面对。恰是这个过程最有成长价值。2.2 环境装配Python、依赖库、编辑器做 Python 爬虫环境问题通常是新手的第一道坎。我见过不少朋友卡在装不上 requests这一步其实思路特别简单。第一装 Python。无论 Windows、Linux 还是 macOS到官网下载对应版本安装包安装时记得勾选Add Python to PATH。装好之后在终端里输入python --version能出版本号就说明成功了。如果你用的是 VSCode再顺手装一个 Python 扩展调试时要方便得多。第二装依赖库。这个项目只需要三个库requests发 HTTP 请求、pandas数据处理和 CSV 导出、chardet做字符编码检测。在终端里执行pip install requests pandas chardet如果pip下载速度慢把源切到国内镜像会快很多这里就不展开讲了。装好之后执行pip list能看到这三个库就说明环境 OK。第三准备浏览器开发者工具。我用的是 Chrome按 F12 打开 DevTools切到 Network 面板。这是整个项目的侦察重地能不能找到数据接口全看这一下。2.3 抓包工具的基础用法从抖音网页版找接口准备工作做完接下来就是本项目的第一个关键动作打开抖音网页版找到一个目标博主的主页然后分析浏览器发出了哪些请求。操作路径很简单Chrome 打开目标主页 → 按 F12 切到 Network 面板 → 刷新页面 → 在筛选栏里选择Fetch/XHR这一步很重要HTML 文档请求里看不到我们要的 JSON 数据接口→ 慢慢往下翻列表找名字里带profile、user、aweme这类关键词的请求。点开某条请求后先看 Response 标签页。如果里面的 JSON 里出现nickname、follower_count这些字段恭喜这就是我们要找的接口。这时候再把请求头里的完整 URL、Cookie、User-Agent 复制出来备用。这里要提个醒抖音的接口路径和参数不是一成不变的几个月前可能叫/aweme/v1/web/user/profile/other/后面可能换了名字或加了版本号。所以比起死记硬背一个 URL更重要的是掌握怎么用 Network 面板去定位这套方法。方法会了接口再怎么变都拦不住你。3. 核心实现从解析接口到代码落地3.1 请求URL与关键参数Cookie、sec_user_id、msToken和X-Bogus当我们从 Network 面板里复制出那条数据请求的完整 URL 后会看到很长一串查询参数。乍一看眼花缭乱其实拆开来看就三类。一类是用户标识参数比如sec_user_id这一眼就能看出来就是你想查的那个博主的加密 ID。第二类是业务参数比如device_platform、aid、channel这些判断当前请求来自什么设备和场景。第三类是安全参数比如msToken、X-Bogus、a_bogus这些参数往往是动态生成的用来让服务器确认这真的来自一个正常浏览器而不是一个无脑脚本。很多教程会把这三类参数混在一起讲看得人一头雾水。我的理解方式很简单前两类你可以当成门牌号告诉服务器你要访问哪一间房第三类是门禁卡证明你有权限来敲这个门。这里要特别说明X-Bogus这类参数。它是抖音反爬体系里比较核心的一环通常是页面脚本根据请求参数、时间戳、浏览器指纹等信息实时计算出来的。学习阶段最稳妥的做法是先把浏览器里现成的完整请求复制下来包括 Cookie、签名、所有参数在自己的代码里做实验体会带着门禁卡能进门不带就被拦在门外的区别。你可以理解它、敬畏它但不要想着去逆向破解它——那既费时也触碰了平台的底线一个学习项目完全没必要走到那一步。3.2 用requests还原一次人工请求思路清楚了代码就很简单。我们先用 requests 构造一个和浏览器几乎一模一样的 GET 请求代码骨架长这样import requests import json # 从浏览器 Network 面板完整复制注意替换成你自己的值 COOKIE 换成你浏览器里复制出来的完整Cookie UA Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 SEC_UID 目标博主的sec_uid headers { User-Agent: UA, Referer: https://www.douyin.com/, Cookie: COOKIE, Accept: application/json, text/plain, */*, } params { sec_user_id: SEC_UID, device_platform: web_app, aid: 6383, # 其他从浏览器里复制出来的 query 参数按需补充 } url https://www.douyin.com/aweme/v1/web/user/profile/other/ resp requests.get(url, headersheaders, paramsparams, timeout10) print(resp.status_code) print(resp.text[:500])第一次跑通的时候你会看到终端里刷出一大段 JSON那一刻确实挺有成就感。但我要提醒一句你把整段Cookie和UA硬编码在脚本里这只是为了学习验证。时间一长 Cookie 会过期请求频繁了还会触发验证所以它不能作为长期稳定的方案。如果你真想长期、定期地拉某个博主的资料正确姿势是去走官方开放平台的授权接口而不是和维护方搞猫鼠游戏。3.3 解析JSON数据并整理成表格请求返回的 JSON 往往很长但只要顺着结构找字段就在那里。以我当时看到的数据为例最外层的status_code是 0 表明请求成功user对象里是用户基本信息stats对象里是各类计数数据。解析代码很好写我直接用json模块加载返回文本再一层层取字段data json.loads(resp.text) if data.get(status_code) 0: user data.get(user, {}) stats user.get(stats, {}) row { nickname: user.get(nickname), signature: user.get(signature), uid: user.get(uid), sec_uid: user.get(sec_uid), avatar_url: user.get(avatar_thumb, {}).get(url_list, [None])[0], follower_count: stats.get(follower_count), following_count: stats.get(following_count), total_favorited: stats.get(total_favorited), video_count: stats.get(aweme_count), } print(row) else: print(请求失败状态码, data.get(status_code), data.get(status_msg))这里面有个小坑值得单独拎出来说不同接口版本里user和stats的嵌套层级可能不一样。我在调试时就遇到过一次明明看着像数据对象打印出来却是None后来才发现是我用的那个接口返回的是一个列表需要先取[0]再取字段。所以解析之前建议先用json.dumps(data, indent4, ensure_asciiFalse)把返回结果格式化打印一遍肉眼确认层级关系再动手写提取逻辑能省很多时间。3.4 完整脚本单博主资料抓取与存储单条数据验证可行后我顺手写了一个最小可用的完整脚本输入一个博主的sec_uid请求数据提取字段保存成 CSV。核心逻辑如下import requests import json import csv import time def fetch_user_profile(sec_uid, headers, params_base, save_pathdouyin_user.csv): url https://www.douyin.com/aweme/v1/web/user/profile/other/ params dict(params_base) params[sec_user_id] sec_uid for attempt in range(3): try: resp requests.get(url, headersheaders, paramsparams, timeout10) data resp.json() if data.get(status_code) 0: user data[user] stats user[stats] row { nickname: user.get(nickname), signature: user.get(signature), sec_uid: sec_uid, follower_count: stats.get(follower_count), following_count: stats.get(following_count), total_favorited: stats.get(total_favorited), video_count: stats.get(aweme_count), create_time: time.strftime(%Y-%m-%d %H:%M:%S), } with open(save_path, a, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnameslist(row.keys())) if f.tell() 0: writer.writeheader() writer.writerow(row) print(f成功保存: {row[nickname]}) return row else: print(f接口返回异常: {data.get(status_code)} {data.get(status_msg)}) break except Exception as e: print(f第{attempt 1}次请求失败: {e}) time.sleep(2) return None if __name__ __main__: # 从浏览器复制的headers、params_base按 3.2 节方式准备 headers {} # 见上文 params_base {} # 见上文 fetch_user_profile(目标博主的sec_uid, headers, params_base)存储这块我特意用了utf-8-sig编码而不是普通的utf-8。为什么因为 CSV 用 Excel 打开时如果是纯utf-8中文大概率会乱码utf-8-sig会带一个 BOM 头Excel 能正确识别。这种细节一般教程不会提但实际项目里遇到一次就记住了。4. 踩坑记录高频问题与排查思路4.1 数据为空或报错先看状态码爬虫写完之后调试才是最磨人的环节。我第一次跑到真正的抖音接口时返回的数据结构和网上的教程对不上user对象里死活找不到stats字段。后来把返回结果完整打印出来才发现这个接口的嵌套层级和预期不一样数据被包在了一个user数组里。所以遇到数据为空的通用排查思路是不要慌先打印原始返回文本确认是不是真的拿到了数据。如果返回内容是一段 HTML说明请求被重定向到了别的页面如果返回的是 JSON 但status_code不是 0说明接口层面拒绝了你的请求。定位问题永远比盲目改代码高效。4.2 触发验证码频率与请求头的双重考验很多爬虫新手在跑通一次之后就疯狂循环请求结果很快就发现页面弹出滑块验证。这个现象背后的逻辑很简单服务器会根据请求频率、Cookie 行为、浏览器特征来判断你到底是不是人。我在实验中踩过一次坑因为调试时多次快速请求触发了验证页。经验教训有这么几条控制频率单次请求之间至少间隔 3~5 秒不要像开机关枪一样打。补全请求头除了 User-Agent 和 CookieReferer、Accept、Accept-Language 这些字段也要尽量从浏览器里完整复制越像真人越好。不要强解验证码自动打码、模拟滑块这类操作既费劲又踩红线。学习项目碰到验证码第一时间应该想到降低频率、补齐请求头而不是想着怎么绕过去。记住一个原则你是在学习不是在对抗。一旦产生对抗心态项目就容易跑偏。4.3 优雅退避重试、限速与容错爬虫最忌讳的是一条道走到黑。请求失败了一次就连续重试往往会加重服务器压力反而更容易被封。正确的做法是加入退避机制失败后先等几秒再重试并且设置最大重试次数。代码层面的实现很简单import time import random def fetch_with_retry(func, max_retries3): for i in range(max_retries): try: return func() except Exception as e: wait 2 ** i random.uniform(0, 1) print(f请求失败{wait:.2f}秒后重试...) time.sleep(wait) return None这里的等待时间用了指数退避的思路第一次失败等大约 2 秒第二次等大约 4 秒第三次等大约 8 秒。这种策略的好处是既留给服务器恢复的时间又不会因为等待过久而浪费效率。4.4 常见问题速查表把我在这个项目里遇到的问题汇总成一张表方便你对照排查现象可能原因解决办法请求返回大段 HTML不是 JSONCookie 过期或失效重新在浏览器登录网页版复制新 CookieJSON 里status_code非 0请求参数缺失、签名不合法完整复制浏览器里的查询参数确认sec_user_id正确数据里部分字段为None接口返回的字段名或层级变化先格式化打印 JSON重新定位字段路径请求触发滑块验证请求频率过高或请求头不完整降低频率、补全请求头、等待一段时间CSV 用 Excel 打开中文乱码编码用了utf-8而非utf-8-sig保存时指定encodingutf-8-sig粉丝数显示为1.2w而不是数字平台字段是格式化展示底层数据其实是数值直接从接口取数值不要从页面文本里抓这些坑看起来琐碎但每一个都值得你亲自踩一遍。踩过之后你对 HTTP 协议、字符编码、接口设计这些基础概念的理解会扎实很多。5. 学习建议与更有意思的扩展方向5.1 爬虫只是手段工程思维才是收获我见过不少朋友学爬虫目标是把某个网站的所有数据都抓下来好像抓得越多就越厉害。但真实的工作场景里爬虫只是数据链路最上游的一环后面还有清洗、存储、分析、可视化这一大堆事情。真正让我受益的不是我会向抖音接口发请求了而是我在这个项目里养成了几个习惯先看完整返回再写解析逻辑异常情况不要吞掉要打印出来频率控制要放在一开始就考虑。这些习惯放在任何后端开发任务里都很值钱。所以我的建议是不要追求采集了多少万条数据而是追求把一条数据的完整生命周期跑通。从拿到 URL、分析请求、解析响应、清洗字段到落库这条链路跑通一次比乱抓一千条数据有价值得多。5.2 把数据变成价值可视化与趋势分析数据抓下来不是终点分析才是。拿到一批博主的公开资料后你可以做不少有意思的事情。用 pandas 读入 CSV按follower_count排序你可以快速知道这个领域里哪些博主影响力最大。用 matplotlib 画一张粉丝数分布直方图你能直观看到头部效应到底有多明显。如果你连续几天采集同一个博主的follower_count还能画出他的粉丝增长曲线观察某个热点事件带来的涨粉效果。我后来就把采集到的数据组合成了一个非常简单的趋势图表虽然只是几条折线但跑出来的那一刻你会觉得前面的抓取工作没有白做。数据本身的乐趣往往在分析阶段才真正展开。5.3 一条更稳妥的进阶路线如果爬完抖音的公开资料不过瘾还想继续在数据分析这条路上走我会建议你往两个方向深入。第一个方向是结构化数据采集。爬虫真正的用武之地是那些公开、稳定、数据价值高的站点比如行业报告、天气数据、政府公开数据等。这些数据源本身鼓励大家去获取你不用担心合规问题可以把更多精力放在数据处理的工程化上。第二个方向是数据可视化与指标体系。把抓下来的数据做成自动化报表、趋势监控面板这比不停扩张爬取范围有意思也更有价值。这个时候你会发现Python 的数据分析生态pandas、matplotlib、streamlit和爬虫技能是天然互补的。还有一点想特别说数据量一旦大到需要引入分布式爬虫、IP 资源池的程度说明你的需求大概率已经跳出学习范畴了。到时候停下来想一想你的使用场景是不是真的走到了灰色地带。能主动喊停本身也是一种技术判断力。最后再分享一点个人经验。这个项目做完之后最大的收获不是我会爬抖音了而是我终于把浏览器开发者工具、HTTP 请求、JSON 解析这些零散的知识串联成了一条完整链路。如果你也想练手记得坚持三条底线公开数据、低频请求、学习用途。在这个范围内把一条数据的生命周期完整跑通你学到的东西绝对比任何花哨的采集量都扎实。