)
1. 项目定位与学习边界先看清楚自己能碰什么前阵子有个朋友问我想拿Python练手写爬虫又不想只爬那种几百行数据的练习网站能不能直接用抖音当学习对象抓点用户主页的公开资料。说实话这个想法非常典型Python爬虫本来就是个“在真实平台上打磨出来的技术”你光爬本地静态页面永远学不到请求构造、数据解析、异常处理这些硬功夫。但抖音这类平台的数据接口涉及用户信息边界要比普通练习网站敏感得多所以动手之前必须先把这个项目到底在做什么、哪些能做、哪些绝对不能碰掰扯清楚。这篇文章要拆解的项目简单来说就是用Python抓取抖音平台上公开可见的用户主页基础信息包括用户昵称、签名、作品数量、粉丝数、关注数等这类在浏览器里打开主页就能看到的内容并且整个过程只停留在请求公开网页和解析返回数据这一步不涉及任何私密信息、不绕过登录权限、不批量采集非公开数据。标题里那句“仅供学习参考 切勿用于商业”就是整篇文章的安全红线也是你在任何真实项目里都应该有的基本觉悟。这个项目适合谁一类是刚学完Python基础语法、想找一个真实场景练手requests和JSON解析的同学另一类是想了解爬虫在真实平台会遇到哪些坑、如何设计合规采集流程的开发者。它不教你那些见不得光的对抗手段而是把“请求—解析—存储—限速—容错”这条爬虫基本功链路完整走一遍。如果你能把这套思路吃透换到其他公开数据源上也是同一套方法论。2. 环境与工具准备把地基打牢2.1 Python环境安装与会话隔离写爬虫的第一步不是写代码而是把Python环境收拾干净。很多初学者直接在自己系统里装了Python就开始pip install结果全局环境越搞越乱版本冲突、权限报错全来了。我个人的习惯是每个项目独立虚拟环境尤其是爬虫项目依赖库更新快今天装requests 2.31明天可能因为某个老项目要锁在2.28虚拟环境能让你在项目之间自由切换互不干扰。安装Python本身没有太多可说的去官网下载对应系统的安装包Windows下安装时记得勾选“Add Python to PATH”Linux下可以用包管理器装也可以自己编译但建议直接用官方安装包或发行版仓库里的版本省心。装完之后在终端验证一下python --version pip --version确认没问题再为当前项目创建虚拟环境并激活python -m venv .venv source .venv/bin/activate # Windows下是 .venv\Scripts\activate这里多说一句虚拟环境不是可选项是必选项。你以后维护的爬虫项目越多越能体会到环境隔离的好处。我见过太多人因为全局环境依赖冲突最后干脆重装系统那才叫耗时耗力。2.2 核心依赖库不贪多够用就好这个项目用到的库其实不多核心就这几个requests发起HTTP请求获取网页内容。比Python自带的urllib好用太多也是整个爬虫技术栈里出场率最高的库。beautifulsoup4lxml解析HTML页面结构用的。虽然抖音公开主页的数据通常嵌在JSON里而不是纯粹的HTML标签里但解析思路是通用的这两个库依然是爬虫学习的标配。jsonPython自带不需要额外安装但它是解析接口返回数据的核心工具。pandas主要是最后做数据清洗和存储时方便如果你只想把结果存成JSON也可以不装。retrying或tenacity重试机制的实现库网络请求不稳定时很管用。这个不强求自己写个循环重试也完全可以。安装命令也很简单pip install requests beautifulsoup4 lxml pandas注意如果你的项目有机会从本机写数据到Excel保存CSV时编码要指定为utf-8-sig否则用Excel打开中文会乱码。这个坑我后面还会专门说。3. 爬虫的核心工作流别被“抓取”两个字吓住3.1 从四次握手到数据落盘很多人一说爬虫就以为是什么高深黑客技术其实拆开了看它就是一个“模拟人在浏览器里操作”的过程。你平时打开抖音网页版输入一个用户主页的网址浏览器会做三件事向服务器发起请求、接收服务器返回的HTML页面、把页面渲染成你看到的样子。爬虫做的事情也一样只不过最后一步不是渲染成界面而是从返回内容里把需要的数据提取出来存到本地。完整的爬虫工作流可以拆成四步:构造请求确定你要访问的URL带上必要的请求头Headers、参数Params发送HTTP请求。获取响应服务器返回的可能是HTML、JSON或者是图片视频等二进制内容。解析数据用正则表达式、DOM解析库或JSON解析方法从响应内容中提取出你需要的信息。存储数据把提取出的数据写入CSV、JSON、数据库或Excel完成整个采集闭环。这四步看起来简单但每一步在真实平台都有无数细节。比如请求头里的User-Agent你用requests默认的python-requests/2.31.0去访问抖音服务器一看到这个标识就知道不是真人浏览器大概率就直接拒绝或者不返回完整数据。再比如请求频率你每秒钟请求十次和每十秒请求一次在服务器眼里是完全不同的风险等级。3.2 为什么要拿抖音练手真实平台的复杂性能逼你成长练习网站的好处是稳定、不反爬但坏处也明显它们反爬太弱了你写的代码在练习网站上能跑通换到真实平台就各种失败。抖音这类平台给我的感觉是它的网页端对“公开数据”和“非公开数据”是有明确边界的。对于公开主页你会遇到请求头校验、验证码、访问频率控制、数据字段缺失等问题这些问题恰恰是爬虫技术人员需要学习和积累经验的地方。把目标设定成“抓取公开主页资料”还有一个好处它不需要登录也不需要处理复杂的签名算法你可以把注意力完全集中在爬虫基础流程上。等基础流程熟练了再去研究更深层的东西那就是后话了。4. 实操过程一步步抓取抖音公开个人资料4.1 分析公开主页的数据结构我拿抖音网页版举例。在浏览器里打开一个用户的公开主页地址栏里的URL通常是这样的形式https://www.douyin.com/user/一串字符这串字符就是用户的sec_uid可以理解为用户在平台上的公开身份标识。如果你只有一个用户的普通抖音号比如abc123平台通常也提供了通过抖音号搜索到用户主页的入口但解析搜索接口的复杂度更高不适合作为入门项目。打开主页后按下F12进入开发者工具切到Network网络标签页刷新页面你会看到浏览器发了很多请求。其中有一个请求返回的是整个页面的HTML内容而HTML里往往嵌入了一段被转义过的JSON数据里面包含了这个主页展示的大部分用户信息。这就是我们的突破口。这里我说一个适用于绝大多数“网页端公开数据”的通用套路**先尝试直接从HTML页面里找数据再看有没有专门的JSON接口返回数据。**很多网站为了首屏渲染速度会把初始数据直接塞进HTML的script标签里这一段数据往往比异步接口返回的数据更完整也更容易获取因为它不需要额外的签名参数。4.2 编写核心代码请求、解析、存储一条龙下面这段代码是一个示例实现思路是请求用户公开主页的HTML从HTML中提取嵌入的JSON数据解析出用户基础资料字段最后保存到CSV文件。import requests import json import re import csv import time import random from typing import Optional, Dict, Any # 合规声明本项目仅供学习爬虫与数据分析流程使用切勿用于商业目的 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.douyin.com/, Accept-Language: zh-CN,zh;q0.9, } def fetch_profile_html(sec_uid: str, max_retries: int 3) - Optional[str]: 请求用户公开主页返回HTML文本失败时按重试策略多次尝试。 url fhttps://www.douyin.com/user/{sec_uid} for attempt in range(max_retries): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: return resp.text else: print(f请求失败状态码{resp.status_code}第{attempt 1}次重试) except requests.RequestException as e: print(f网络异常{e}第{attempt 1}次重试) time.sleep(random.uniform(2, 5)) return None def extract_user_data(html: str) - Dict[str, Any]: 从HTML中提取嵌在script标签里的JSON数据并取出用户公开信息。 # 在抖音网页版中RENDER_DATA 或 __UNIVERSAL_DATA_FOR_REHYDRATION__ 是常见的数据承载字段 # 以下示例展示的是从脚本内容中按JSON结构解析的思路 match re.search(rscript id__UNIVERSAL_DATA_FOR_REHYDRATION__[^]*(.*?)/script, html, re.S) if not match: # 不同版本可能字段名不同这里只做一个兜底 match re.search(rscriptwindow\._ROUTER_DATA\s*\s*(\{.*?\})/script, html, re.S) if not match: raise ValueError(未能从页面中找到嵌入的JSON数据页面结构可能已更新) raw_text match.group(1).strip() # 嵌入式数据常常做了HTML转义比如 quot; 需要还原成 这一步很容易被忽略 raw_text raw_text.replace(quot;, ).replace(amp;, ).replace(lt;, ).replace(gt;, ) data json.loads(raw_text) # 注意不同版本的页面结构内部层级会不一样。 # 下面通过递归查找的方式找到包含 user 关键字的字典区块提升代码容错性。 user_info {} def _find_user(obj): nonlocal user_info if isinstance(obj, dict): if user in obj and isinstance(obj[user], dict): user_info obj[user] return True for value in obj.values(): if _find_user(value): return True elif isinstance(obj, list): for item in obj: if _find_user(item): return True return False _find_user(data) if not user_info: raise ValueError(已解析JSON但未找到user字段可能需要调整解析路径) return { uid: user_info.get(uid, ), sec_uid: user_info.get(sec_uid, ), nickname: user_info.get(nickname, ), signature: user_info.get(signature, ).replace(\n, ).strip(), avatar_url: (user_info.get(avatar_thumb) or {}).get(url_list, [])[0], following_count: (user_info.get(following) or {}).get(count, 0), follower_count: (user_info.get(follower) or {}).get(count, 0), aweme_count: (user_info.get(aweme) or {}).get(count, 0), total_favorited: (user_info.get(total_favorited) or 0), } def save_to_csv(data_list: list, filename: str douyin_profiles.csv): 将解析后的用户资料列表写入CSV文件。 if not data_list: print(没有数据可写入) return fieldnames data_list[0].keys() # utf-8-sig 编码可以保证Excel打开时中文不乱码 with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(data_list) print(f已保存 {len(data_list)} 条数据到 {filename}) if __name__ __main__: # 这里替换成你想观察的公开用户sec_uid test_sec_uid 你的目标sec_uid html fetch_profile_html(test_sec_uid) if html: try: info extract_user_data(html) print(json.dumps(info, ensure_asciiFalse, indent2)) save_to_csv([info]) except Exception as e: print(f解析失败{e})这段代码里我在三个地方刻意加了处理逻辑这三个地方恰恰是新手最容易踩坑的第一是请求头。User-Agent一定要用完整浏览器标识Referer表示请求来源同时加上Accept-Language这些都是模拟真人浏览器的基础配置。你只带一个UA虽然也能请求成功但被识别为爬虫的概率会大很多。第二是HTML实体字符反转义。很多网页把JSON塞进HTML时会做一遍转义比如把双引号转成quot;逗号转成comma;等。如果你直接json.loads会直接报错。我见过不少人在这一步卡了很久其实原因就是忘了反转义。第三是递归查找user字段。抖音网页版的结构隔几个月变一次写死路径的情况下页面一改版代码就废了。用递归去查找能在一定程度上提升代码的鲁棒性。当然递归也有风险比如找到嵌套里的其他user字段但作为学习项目这个容错方向是对的。4.3 运行结果与字段说明如果你指定的sec_uid是公开可访问的主页且请求频率正常上面的代码大概率能跑通控制台会输出类似下面的JSON{ uid: 1234567890123456, sec_uid: MS4wLjABAAAA..., nickname: 示例用户, signature: 这是一个公开主页签名, avatar_url: https://p3-sign.douyinpic.com/..., following_count: 123, follower_count: 4567, aweme_count: 89, total_favorited: 12345 }各个字段的含义我整理成了一张表方便你对照理解字段名含义数据类型信息来源uid平台内部用户ID字符串公开主页sec_uid公开主页标识符字符串公开主页nickname用户昵称字符串公开主页signature个人签名字符串公开主页avatar_url头像图片链接字符串公开主页following_count关注数整数公开主页follower_count粉丝数整数公开主页aweme_count作品数整数公开主页total_favorited总获赞数整数公开主页需要注意的是抖音的页面结构会不断调整字段名和嵌套层级不是永恒的所以你的代码里解析函数最好写得灵活一点不要把某个路径写死。4.4 如何获取目标sec_uid换个角度看公开标识很多初学者会卡在第一步我没有sec_uid怎么办其实很简单你只需要在抖音网页版搜索一个用户点进他的主页地址栏里那串字符就是他的sec_uid。这个过程完全可以通过浏览器手动操作完成不需要写代码去破解什么搜索接口。但如果你确实有教学或非商业研究场景下的批量需求想实现“抖音号转sec_uid”的自动化那就要去研究网页端的搜索接口了。这个接口的请求参数里会有一些动态生成的值对新手来说难度跨度太大。我的建议是学习阶段先手动复制sec_uid把精力集中在“请求—解析—存储”这条主链路上等主链路通了再考虑更复杂的自动化。这也呼应了标题里的“仅供学习参考”——越界的自动化风险也会跟着翻倍。5. 常见问题与排查技巧实录5.1 请求返回200但里面没有用户数据这是我在真实项目里遇到最多的情况。状态码是200看起来请求成功了但你打印HTML内容要么是一段“访问过于频繁”的提示要么是验证码页面要么是空壳的框架页面。原因很简单服务器没有直接拒绝你但它识别出你不是真人浏览器于是返回了非目标页面。排查思路是这样的在浏览器里用无痕模式打开同一个URL确认公开主页确实可以正常访问。对比浏览器请求和代码请求的请求头差异缺什么补什么尤其是User-Agent和Referer。把请求频率降下来两次请求之间加2到5秒的随机延迟再用代码跑一次。检查是不是被验证码拦了。如果响应里出现验证码相关关键字那说明IP或请求特征被标记了最稳妥的方法是停止爬取等一段时间或重启路由器换个IP然后降低频率再试。5.2 HTML里确实有JSON但json.loads就是解析失败这种问题十有八九不是JSON本身坏了而是转义没有处理干净。有些网页会把、、、等字符全部做实体转义JSON字符串里充斥着quot;、lt;、gt;。直接在浏览器里看到的是正常JSON但你复制到代码里调试就会报错。处理办法就是我在示例代码里做的那样在json.loads之前先做一次替换把常见HTML实体字符还原成普通字符。如果替换之后还是报错还有一种可能JSON数据被压缩成了单行放在某个script标签里正则匹配时抓取范围不对需要调整script标签的id或class选择器从页面源码里观察它到底挂在哪里。5.3 频繁请求后IP被限制怎么办爬虫做久了一定会遇到请求频率过高导致的临时封禁。这种封禁通常是短期的几小时到一天不等平台会用验证码或直接拒绝响应来表达不满。对学习项目来说最直接的解决方案就是不要用一个脚本开着无限循环去刷每次请求之间加随机等待时间。import time import random # 在循环请求之间加上随机延时模拟真人浏览节奏 time.sleep(random.uniform(2, 5))更进一步的做法是设置重试机制当请求失败时等待更长时间再重试而不是立即疯狂重试疯狂重试只会让封禁时间延长。从设计思路上讲爬虫要像“一个正常人”那样访问网站而不是像“一个不知疲倦的机器人”那样冲击服务器。这一点在任何真实项目中都是铁律。5.4 数据保存不下来字段对不上字段对不上的情况也很常见特别是当你批量抓取多个用户资料时会发现有些用户没有签名有些用户没开通作品展示这时候代码里就要对缺失字段做兜底处理。我在示例代码里把字段统一用dict.get()取值并给默认值就是为了防止这种情况。保存CSV时的编码问题也容易踩坑。如果你用open(filename, w, newline, encodingutf-8)去写然后用Excel打开中文会乱成一片。解决办法是改用utf-8-sig编码它在文件开头加了一段BOM标记Excel和WPS都能正确识别。这是个小细节但对结果体验影响很大。6. 合规红线学习归学习边界不能踩6.1 哪些行为绝对不要碰聊了这么多技术细节最后一部分是这篇文章里最关键的也是最容易被新手忽略的。爬虫本身是一项中性的技术但它落在不同的使用场景里法律和道德边界完全不同。对这个项目而言有几条红线是绝对不能碰的不抓取任何非公开信息。用户设置了私密主页你通过技术手段绕过权限去查看这是明确违规的。不恶意高频请求。抓取公开数据也必须有节制服务器资源不是你一个人的实验室逼近极限的并发请求很可能造成服务异常。不把抓取的数据用于商业用途。这是标题里原话也是这个项目的基本立场。用户公开在主页上的资料不等于你有权把它打包卖给别人。不进行数据倒卖和用户画像分析。无论是姓名、签名、头像还是粉丝数这些数据经过批量收集之后一旦落到不该落的人手里就可能被用来做骚扰、诈骗或恶意营销。不绕过平台的访问控制机制。如果网站明确通过验证码来限制自动化访问那就应该停下来而不是钻研怎么破解。我在这个项目里反复强调“公开资料”和“仅供学习”不是喊口号而是因为这是爬虫技术能正常交流讨论的前提。你只有在这个前提下堂堂正正地练手写出来的代码、总结出来的经验才拿得上台面。6.2 规则不是束缚而是保护的边界我从自己多年的经验说句实话真正值钱的爬虫能力反而不是那些“能搞定多难反爬”的本事而是“知道什么数据该抓什么数据碰都不能碰”的判断力。技术上的难题多花点时间总能绕过去但法律和道德上的分寸一旦越界代价是不可逆的。所以我建议所有想在这条路上走远的人第一次动手之前就给自己立几条规矩不做商业化采集、不碰非公开数据、控制请求频率、数据用完即删。把这几条当成习惯比学会任何招数都管用。6.3 我的个人体会和下一步方向这个项目做完之后我自己最大的收获不是那几行能跑通的代码而是对“公开数据到底意味着什么”这件事有了更深的理解。平台公开展示的数据是为正常用户浏览服务的不是为自动化批量采集服务的。所以“能访问”和“可以随便抓”之间距离其实很远。如果这个项目你已经跑通了下一步想继续深入我建议你往这几个方向走一是把数据存储从CSV换成SQLite或MySQL顺带练练数据库操作二是设计一个更科学的限速和重试策略模拟真人访问三是给程序加一个简单的日志系统方便分析每次请求的成败原因。至于分布式爬虫、可视化界面这些等单机版本稳定了再考虑也不迟基础不稳的话上分布式只会放大问题。最后再分享一个小经验每次跑完爬虫把抓到的数据认真看一遍你会从这些公开字段里学到很多关于平台产品设计的思路。比如为什么有的用户把作品数设成私密、有的人却把签名写成联系方式这些东西能帮你更好地理解爬虫技术的应用场景和边界。保持好奇保持克制这条路你才能走得更远。