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好用太多,也是整个爬虫技术栈里出场率最高的库。beautifulsoup4+lxml:解析HTML页面结构用的。虽然抖音公开主页的数据通常嵌在JSON里而不是纯粹的HTML标签里,但解析思路是通用的,这两个库依然是爬虫学习的标配。json:Python自带,不需要额外安装,但它是解析接口返回数据的核心工具。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;q=0.9", } def fetch_profile_html(sec_uid: str, max_retries: int = 3) -> Optional[str]: """请求用户公开主页,返回HTML文本;失败时按重试策略多次尝试。""" url = f"https://www.douyin.com/user/{sec_uid}" for attempt in range(max_retries): try: resp = requests.get(url, headers=HEADERS, timeout=10) 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(r'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>(.*?)</script>', html, re.S) if not match: # 不同版本可能字段名不同,这里只做一个兜底 match = re.search(r'<script>window\._ROUTER_DATA\s*=\s*(\{.*?\})</script>', html, re.S) if not match: raise ValueError("未能从页面中找到嵌入的JSON数据,页面结构可能已更新") raw_text = match.group(1).strip() # 嵌入式数据常常做了HTML转义,比如 " 需要还原成 ",这一步很容易被忽略 raw_text = raw_text.replace(""", '"').replace("&", "&").replace("<", "<").replace(">", ">") 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="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) 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_ascii=False, indent=2)) save_to_csv([info]) except Exception as e: print(f"解析失败:{e}")这段代码里我在三个地方刻意加了处理逻辑,这三个地方恰恰是新手最容易踩坑的:
第一是请求头。User-Agent一定要用完整浏览器标识,Referer表示请求来源,同时加上Accept-Language,这些都是模拟真人浏览器的基础配置。你只带一个UA,虽然也能请求成功,但被识别为爬虫的概率会大很多。
第二是HTML实体字符反转义。很多网页把JSON塞进HTML时会做一遍转义,比如把双引号转成",逗号转成,等。如果你直接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字符串里充斥着"、<、>。直接在浏览器里看到的是正常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="", encoding="utf-8")去写,然后用Excel打开,中文会乱成一片。解决办法是改用utf-8-sig编码,它在文件开头加了一段BOM标记,Excel和WPS都能正确识别。这是个小细节,但对结果体验影响很大。
6. 合规红线:学习归学习,边界不能踩
6.1 哪些行为绝对不要碰
聊了这么多技术细节,最后一部分是这篇文章里最关键的,也是最容易被新手忽略的。爬虫本身是一项中性的技术,但它落在不同的使用场景里,法律和道德边界完全不同。
对这个项目而言,有几条红线是绝对不能碰的:
- 不抓取任何非公开信息。用户设置了私密主页,你通过技术手段绕过权限去查看,这是明确违规的。
- 不恶意高频请求。抓取公开数据也必须有节制,服务器资源不是你一个人的实验室,逼近极限的并发请求很可能造成服务异常。
- 不把抓取的数据用于商业用途。这是标题里原话,也是这个项目的基本立场。用户公开在主页上的资料,不等于你有权把它打包卖给别人。
- 不进行数据倒卖和用户画像分析。无论是姓名、签名、头像,还是粉丝数,这些数据经过批量收集之后,一旦落到不该落的人手里,就可能被用来做骚扰、诈骗或恶意营销。
- 不绕过平台的访问控制机制。如果网站明确通过验证码来限制自动化访问,那就应该停下来,而不是钻研怎么破解。
我在这个项目里反复强调“公开资料”和“仅供学习”,不是喊口号,而是因为这是爬虫技术能正常交流讨论的前提。你只有在这个前提下堂堂正正地练手,写出来的代码、总结出来的经验,才拿得上台面。
6.2 规则不是束缚,而是保护的边界
我从自己多年的经验说句实话,真正值钱的爬虫能力,反而不是那些“能搞定多难反爬”的本事,而是“知道什么数据该抓,什么数据碰都不能碰”的判断力。技术上的难题,多花点时间总能绕过去;但法律和道德上的分寸,一旦越界,代价是不可逆的。
所以我建议所有想在这条路上走远的人,第一次动手之前就给自己立几条规矩:不做商业化采集、不碰非公开数据、控制请求频率、数据用完即删。把这几条当成习惯,比学会任何招数都管用。
6.3 我的个人体会和下一步方向
这个项目做完之后,我自己最大的收获不是那几行能跑通的代码,而是对“公开数据到底意味着什么”这件事有了更深的理解。平台公开展示的数据,是为正常用户浏览服务的,不是为自动化批量采集服务的。所以“能访问”和“可以随便抓”之间,距离其实很远。
如果这个项目你已经跑通了,下一步想继续深入,我建议你往这几个方向走:一是把数据存储从CSV换成SQLite或MySQL,顺带练练数据库操作;二是设计一个更科学的限速和重试策略,模拟真人访问;三是给程序加一个简单的日志系统,方便分析每次请求的成败原因。至于分布式爬虫、可视化界面这些,等单机版本稳定了再考虑也不迟,基础不稳的话,上分布式只会放大问题。
最后再分享一个小经验:每次跑完爬虫,把抓到的数据认真看一遍,你会从这些公开字段里学到很多关于平台产品设计的思路。比如为什么有的用户把作品数设成私密、有的人却把签名写成联系方式,这些东西能帮你更好地理解爬虫技术的应用场景和边界。保持好奇,保持克制,这条路你才能走得更远。