抖音博主数据爬取实战:抓包分析与Python自动化实现
1. 项目概述与核心价值
最近在做一个关于社交媒体内容趋势分析的小项目,需要批量获取一些抖音博主的主页数据,比如粉丝数、作品数、获赞总量这些基础信息。一开始想找现成的API或者数据服务,但要么收费不菲,要么限制多多,数据还不一定准。于是,琢磨着自己动手写个爬虫。这活儿听起来简单,不就是发个请求解析个HTML嘛?但真上手才发现,抖音这类大型App的防护措施相当严密,直接请求网页端要么拿不到数据,要么拿到的是加密或混淆过的内容。所以,这次实战的核心思路就变成了: 先通过抓包分析App的真实数据接口,再用Python模拟这些接口请求,最终实现稳定、自动化的数据采集 。
这个项目非常适合有一定Python基础,想深入理解现代App数据抓取流程的朋友。它不仅能让你学会如何使用抓包工具(如Fiddler/Charles)逆向分析移动端App的通信协议,还能让你掌握如何用 requests 库模拟复杂的请求头、签名参数,甚至处理动态令牌。整个过程就像一次小型的“逆向工程”,充满了挑战和乐趣。最终,我会附上完整的、可运行的Python代码,你拿到后稍作修改(比如替换目标博主ID)就能直接使用。
2. 核心思路与技术选型解析
2.1 为什么选择“抓包”而非“网页爬虫”?
很多新手的第一反应是用 Selenium 或 Playwright 这类浏览器自动化工具去模拟操作、解析页面。对于抖音,这条路不是不能走,但效率极低且极不稳定。首先,抖音的网页端(m.douyin.com)对未登录用户展示的信息有限,很多关键数据(如粉丝数)需要滚动触发加载或直接不显示。其次,大量内容是通过JavaScript动态渲染的,直接解析初始HTML拿不到数据。最后,频繁的页面加载和自动化操作非常消耗资源,容易被识别为机器人行为导致IP被封。
而移动端App(无论是安卓还是iOS)是抖音的主战场,其用户体验和数据完整性都远优于网页端。App与服务器通信是通过一系列设计良好的API接口进行的,返回的数据通常是结构清晰的JSON格式。我们的目标就是找到这些接口,并理解它们的调用规则。因此, “抓包分析”是获取真实、稳定数据接口的唯一高效途径 。
2.2 技术栈与工具选型
整个项目可以拆解为三个核心环节,每个环节都有对应的工具和技术:
-
抓包与分析环节 :
- 核心工具 :
Fiddler Classic或Charles。两者都是强大的HTTP/HTTPS抓包代理工具。我个人更习惯用Fiddler,因为它对Windows的支持更原生,配置流程也更直观。Charles在Mac上更流行,功能上大同小异。 - 关键技能 :配置代理、安装CA证书到手机或模拟器、解密HTTPS流量、筛选和查看请求/响应。
- 核心工具 :
-
请求模拟与数据获取环节 :
- 核心库 :
requests。Python中最简单易用的HTTP库,足以应对绝大多数API请求模拟。 - 辅助库 :
json(解析响应)、time(控制请求频率,避免封禁)、hashlib(如果需要计算签名)。
- 核心库 :
-
数据解析与存储环节 :
- 核心库 :内置的
json库足以处理接口返回的JSON数据。 - 存储方案 :根据数据量选择。小批量测试可以用
csv或json文件,用pandas或内置的csv库操作。如果数据量大或需要后续分析,可以考虑SQLite或MySQL。
- 核心库 :内置的
注意 :本项目仅用于学习网络协议、自动化技术和数据分析方法。任何数据采集行为都必须严格遵守目标网站的
robots.txt协议、服务条款以及相关法律法规。务必控制请求频率,避免对目标服务器造成压力,严禁将数据用于商业牟利或侵犯他人隐私等非法用途。
3. 实战第一步:抓包分析与接口定位
这是整个项目最关键也最需要耐心的一步。我们的目标是找到那个能返回博主主页核心数据的API。
3.1 环境准备与代理配置
- 安装抓包工具 :以Fiddler为例,从其官网下载并安装Fiddler Classic。
- 配置Fiddler允许远程连接 :打开Fiddler,进入
Tools -> Options -> Connections。勾选Allow remote computers to connect,记住默认的监听端口(通常是8888)。配置完成后需要重启Fiddler。 - 获取电脑的局域网IP地址 :在命令行输入
ipconfig,找到当前无线局域网或以太网适配器的IPv4地址(例如192.168.1.105)。 - 配置手机网络代理 :
- 确保手机和电脑在同一个Wi-Fi网络下。
- 进入手机的Wi-Fi设置,长按当前连接的Wi-Fi,选择“修改网络”或“高级选项”。
- 将代理设置为“手动”,主机名填入电脑的IP地址(如
192.168.1.105),端口填入Fiddler的监听端口(如8888)。
- 在手机上安装Fiddler的CA证书 :这是为了解密HTTPS流量。用手机浏览器访问
http://电脑IP:端口,例如http://192.168.1.105:8888。你会看到Fiddler的页面,下载并安装名为“FiddlerRoot certificate”的证书。在安卓手机上,安装后可能还需要在“设置->安全->加密与凭据->用户凭据”中信任该证书。
3.2 捕获抖音App的请求
- 清空Fiddler左侧的会话列表。
- 打开手机上的抖音App,进入任意一个你感兴趣的博主主页。缓慢地上下滑动,浏览一下他的作品列表。
- 回到Fiddler,你会看到瞬间捕获了大量的HTTP/HTTPS请求。这些请求来自抖音以及其依赖的CDN、日志、统计等众多服务。
3.3 筛选与定位目标接口
海量的请求中,我们需要找到那个“真命天子”。这里有一些筛选技巧:
- 使用过滤器 :在Fiddler右侧的“Filters”标签页中,可以勾选“Show only the following Hosts”,并填入
*.douyin.com或*.snssdk.com(抖音的API常用域名),这样可以过滤掉大量图片、视频等媒体资源请求。 - 寻找JSON响应 :目标API的响应内容通常是JSON格式。在会话列表中,查看“Response Content Type”列,寻找
application/json类型的响应。也可以直接看“Body”大小,JSON数据包通常大小在几KB到几十KB。 - 分析请求URL特征 :目标API的URL路径往往包含有意义的单词,如
/user/profile/、/aweme/v1/user/、/web/api/v2/user/等。多点击几个疑似请求,查看其“Inspectors”标签页下的“JSON”视图,如果能直观看到nickname、follower_count、total_favorited等字段,那就找对了。 - 一个关键的线索 :博主主页的“粉丝数”、“关注数”、“获赞数”这些数据,通常在同一个接口中返回。找到一个包含了这些字段的JSON响应,就成功了一大半。
实操心得 :这个过程可能需要反复尝试。有时需要退出博主主页再重新进入,有时需要刷新。重点关注你在进行“进入主页”、“下拉刷新”操作时Fiddler新出现的请求。找到目标接口后, 务必记录下完整的请求URL、所有的请求头(Headers)以及请求方法(通常是GET) 。
4. 接口参数解密与Python请求模拟
假设我们通过抓包,找到了一个疑似接口: https://www.douyin.com/aweme/v1/web/user/profile/other/?sec_user_id=MS4wLjABAAAAxxxx...&device_platform=webapp&...
4.1 关键参数分析
一个典型的抖音API请求会包含几十个参数,但核心的主要是以下几类:
-
身份标识参数 :
sec_user_id: 这是最关键的一个参数 。它是抖音为每个用户(包括博主)生成的唯一且相对稳定的标识符,类似于用户的“身份证号”。我们爬取不同博主的数据,主要就是更换这个ID。这个ID可以从博主主页的分享链接中提取,格式通常为MS4wLjABAAAA...的一长串字符。msToken,X-Bogus,_signature: 这些是抖音用于反爬的 签名参数 。它们通常是根据其他参数、时间戳甚至客户端环境计算出来的,具有时效性。在抓包时,你会发现每次请求这些值都不同。直接复制抓包时的值可以用于单次请求,但无法用于自动化。 这是爬虫最大的难点 。
-
设备与环境参数 :
device_platform: 设备平台,如webapp、android。device_id,os_version,version_code,app_name等:模拟一个真实设备的身份和App版本。
-
其他通用参数 :
aid: 抖音的App ID,通常是固定值如6383(国内版)。cookie: 用户会话凭证。携带有效的cookie可以获取更多数据(如私密账号信息),但无cookie或无效cookie通常也能拿到基础公开信息。
4.2 Python模拟请求的初步实现
我们先实现一个“静态”版本,即直接使用抓包时捕获的、尚未过期的全部请求头和参数进行一次请求。这能验证我们找到的接口是否正确。
import requests
import json
# 目标博主的 sec_user_id (需要替换成你实际抓取到的)
target_sec_user_id = “MS4wLjABAAAAxxxx...”
# 从Fiddler中复制过来的完整URL(包含所有参数)
url = f“https://www.douyin.com/aweme/v1/web/user/profile/other/?sec_user_id={target_sec_user_id}&device_platform=webapp&aid=6383&...&msToken=xxx&X-Bogus=xxx”
# 从Fiddler中复制过来的请求头
headers = {
‘User-Agent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...’,
‘Accept’: ‘application/json, text/plain, */*’,
‘Accept-Language’: ‘zh-CN,zh;q=0.9,en;q=0.8’,
‘Accept-Encoding’: ‘gzip, deflate, br’,
‘Referer’: ‘https://www.douyin.com/’,
# ‘Cookie’: ‘...’, # 如果有且需要,可以加上
‘Connection’: ‘keep-alive’,
}
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status() # 检查请求是否成功
data = response.json() # 解析JSON响应
# 提取我们需要的数据
user_info = data.get(‘user’, {})
nickname = user_info.get(‘nickname’, ‘N/A’)
signature = user_info.get(‘signature’, ‘N/A’)
follower_count = user_info.get(‘follower_count’, 0) # 粉丝数
following_count = user_info.get(‘following_count’, 0) # 关注数
total_favorited = user_info.get(‘total_favorited’, 0) # 总获赞
aweme_count = user_info.get(‘aweme_count’, 0) # 作品数
print(f“博主昵称: {nickname}”)
print(f“个性签名: {signature}”)
print(f“粉丝数量: {follower_count}”)
print(f“关注数量: {following_count}”)
print(f“总获赞数: {total_favorited}”)
print(f“作品数量: {aweme_count}”)
# 可以将数据保存到文件
with open(‘douyin_user_info.json’, ‘w’, encoding=‘utf-8’) as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except requests.exceptions.RequestException as e:
print(f“请求失败: {e}”)
except json.JSONDecodeError as e:
print(f“JSON解析失败: {e}”)
print(“原始响应:”, response.text[:500]) # 打印前500字符以便调试
运行这段代码,如果成功打印出博主信息,恭喜你,接口找对了。但你会发现,这个代码里的 msToken 和 X-Bogus 是写死的,过一段时间(可能几分钟到几小时)就会失效。
5. 实现自动化:动态签名参数的处理
要让爬虫真正自动化,必须解决动态签名参数的问题。这里有几种思路,难度和稳定性依次递增:
5.1 方案一:复用Web端签名(中等难度)
观察发现,抖音Web端( www.douyin.com )的页面在加载时,会通过JavaScript生成这些签名参数。我们可以尝试模拟浏览器环境,执行JS代码来生成参数。这需要用到 execjs 库或 PyExecJS 库。
- 提取JavaScript代码 :通过抓包或查看网页源代码,找到负责生成
X-Bogus等参数的JS函数。这通常是一个被混淆过的、名字很奇怪的函数。 - 使用execjs执行 :将这段JS代码保存下来,在Python中用
execjs调用它,传入必要的参数(如URL、User-Agent等),得到生成的签名。
import execjs
# 假设我们已经把生成签名的JS代码保存到了 generate_xb.js 文件中
with open(‘generate_xb.js’, ‘r’, encoding=‘utf-8’) as f:
js_code = f.read()
ctx = execjs.compile(js_code)
# 调用JS函数,参数需要根据实际JS函数定义来传
x_bogus = ctx.call(‘generateX-Bogus’, url, user_agent)
这个方案的难点在于找到并正确提取那个不断更新的JS函数,且JS引擎的执行效率相对较低。
5.2 方案二:逆向移动端App(高难度)
这是最稳定但也是最复杂的方法。通过反编译安卓APK,分析其Native代码(C++)或Java代码,找到签名算法的实现,然后用Python重写。这涉及到逆向工程、算法还原,需要很强的技术背景,且抖音的签名算法会频繁更新以对抗破解。
5.3 方案三:使用现成的第三方库或服务(快速实现)
对于一些流行的平台,开源社区可能有维护相关的爬虫SDK。例如,对于抖音,可以搜索 douyin-python 、 dy-api 等关键词。使用这些库可以快速绕过签名问题,但需要注意:
- 可靠性 :库可能随时因平台更新而失效。
- 安全性 :不要使用来路不明的、需要输入账号密码的库。
- 法律风险 :明确库的许可协议和使用范围。
对于学习和实战目的,我建议先采用一种折中的“半自动化”方案 :我们承认签名参数会过期,但通过程序自动检测过期并提醒我们手动更新。同时,我们通过优化请求头、使用代理IP池、严格遵守请求间隔等手段,来最大化单次签名参数的有效期和请求成功率。
6. 完整自动化爬虫代码架构与实现
结合上面的分析,我们设计一个相对健壮、易于维护的爬虫结构。这里我们采用“半自动化”思路,将易变的签名参数放在配置文件中,并实现错误重试和速率限制。
6.1 项目目录结构
douyin_crawler/
├── config.yaml # 配置文件,存放签名参数、请求头等
├── crawler.py # 主爬虫逻辑
├── utils.py # 工具函数,如请求重试、数据清洗
├── requirements.txt # 项目依赖
└── data/ # 数据存储目录
└── users_info.csv
6.2 核心代码实现 ( crawler.py )
import requests
import yaml
import time
import random
import csv
from typing import Dict, Any, Optional
from utils import retry, safe_request
class DouyinUserCrawler:
def __init__(self, config_path: str = ‘config.yaml’):
“”“初始化爬虫,加载配置”“”
with open(config_path, ‘r’, encoding=‘utf-8’) as f:
self.config = yaml.safe_load(f)
self.base_url = self.config[‘api’][‘base_url’]
self.headers = self.config[‘headers’]
self.common_params = self.config[‘params’]
# 请求间隔配置,避免请求过快
self.min_delay = self.config.get(‘delay’, {}).get(‘min’, 3)
self.max_delay = self.config.get(‘delay’, {}).get(‘max’, 7)
def _construct_url(self, sec_user_id: str) -> str:
“”“构造完整的API请求URL”“”
# 复制通用参数
params = self.common_params.copy()
# 更新目标用户ID
params[‘sec_user_id’] = sec_user_id
# 如果有动态参数(如从JS生成),可以在这里添加
# params[‘X-Bogus’] = self._generate_x_bogus(params)
# 将参数字典转换为URL查询字符串
query_string = ‘&’.join([f“{k}={v}” for k, v in params.items()])
return f“{self.base_url}?{query_string}”
@retry(max_retries=3, delay=2)
def fetch_user_profile(self, sec_user_id: str) -> Optional[Dict[str, Any]]:
“”“获取单个用户主页信息”“”
url = self._construct_url(sec_user_id)
print(f“正在抓取: {sec_user_id}”)
try:
# 使用工具函数发起请求,内置了异常处理
response = safe_request(‘get’, url, headers=self.headers)
if response is None:
return None
data = response.json()
# 检查接口返回的状态码(抖音的接口通常在JSON里)
if data.get(‘status_code’) != 0:
print(f“接口返回错误: {data.get(‘status_msg’, ‘Unknown error’)}”)
return None
user_info = data.get(‘user’, {})
if not user_info:
print(“未找到用户信息”)
return None
# 提取核心字段
profile = {
‘sec_user_id’: sec_user_id,
‘nickname’: user_info.get(‘nickname’, ‘’),
‘unique_id’: user_info.get(‘unique_id’, ‘’), # 抖音号
‘signature’: user_info.get(‘signature’, ‘’),
‘avatar_url’: user_info.get(‘avatar_larger’, {}).get(‘url_list’, [‘’])[0],
‘follower_count’: user_info.get(‘follower_count’, 0),
‘following_count’: user_info.get(‘following_count’, 0),
‘total_favorited’: user_info.get(‘total_favorited’, 0),
‘aweme_count’: user_info.get(‘aweme_count’, 0),
‘is_verified’: user_info.get(‘is_verified’, False),
‘verify_info’: user_info.get(‘custom_verify’, ‘’),
‘crawl_time’: time.strftime(‘%Y-%m-%d %H:%M:%S’)
}
return profile
except Exception as e:
print(f“解析用户 {sec_user_id} 数据时发生异常: {e}”)
return None
finally:
# 随机延迟,模拟人类操作
delay = random.uniform(self.min_delay, self.max_delay)
time.sleep(delay)
def save_to_csv(self, profile: Dict[str, Any], filename: str = ‘data/users_info.csv’):
“”“将用户信息保存到CSV文件”“”
file_exists = False
try:
with open(filename, ‘r’, encoding=‘utf-8-sig’) as f:
file_exists = True
except FileNotFoundError:
pass
with open(filename, ‘a’, newline=‘’, encoding=‘utf-8-sig’) as f:
fieldnames = profile.keys()
writer = csv.DictWriter(f, fieldnames=fieldnames)
if not file_exists:
writer.writeheader()
writer.writerow(profile)
print(f“已保存: {profile[‘nickname’]}”)
def run(self, sec_user_id_list: list):
“”“主运行函数,遍历用户ID列表进行抓取”“”
for sec_user_id in sec_user_id_list:
profile = self.fetch_user_profile(sec_user_id)
if profile:
self.save_to_csv(profile)
else:
print(f“抓取失败: {sec_user_id}”)
if __name__ == ‘__main__’:
# 实例化爬虫
crawler = DouyinUserCrawler()
# 这里替换成你想要抓取的博主sec_user_id列表
# 如何获取sec_user_id?在抖音App中分享博主主页,链接里通常包含这个参数。
target_users = [
‘MS4wLjABAAAAxxxx...’, # 博主A
‘MS4wLjABAAAAyyyy...’, # 博主B
# ... 更多博主
]
crawler.run(target_users)
6.3 工具函数 ( utils.py )
import requests
import time
from functools import wraps
def retry(max_retries=3, delay=1, backoff=2, exceptions=(Exception,)):
“”“重试装饰器”“”
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
mtries, mdelay = max_retries, delay
while mtries > 0:
try:
return func(*args, **kwargs)
except exceptions as e:
mtries -= 1
if mtries == 0:
print(f“函数 {func.__name__} 重试 {max_retries} 次后失败: {e}”)
raise
print(f“函数 {func.__name__} 调用失败: {e}, {mdelay}秒后重试… ({max_retries - mtries}/{max_retries})”)
time.sleep(mdelay)
mdelay *= backoff # 指数退避
return wrapper
return decorator
def safe_request(method, url, **kwargs):
“”“安全的请求函数,包含基础异常处理”“”
try:
response = requests.request(method, url, timeout=15, **kwargs)
response.raise_for_status()
# 检查内容类型,确保是JSON
if ‘application/json’ in response.headers.get(‘Content-Type’, ‘’):
return response
else:
print(f“警告: 响应不是JSON格式。URL: {url}”)
# 有时错误信息可能是HTML,可以打印一部分看看
print(response.text[:200])
return None
except requests.exceptions.Timeout:
print(f“请求超时: {url}”)
except requests.exceptions.HTTPError as e:
print(f“HTTP错误 {e.response.status_code}: {url}”)
# 如果是签名过期等错误,可以在这里识别并触发更新配置的逻辑
if e.response.status_code in [403, 418]:
print(“可能遇到签名验证失败或频率限制,请检查config.yaml中的参数是否已过期。”)
except requests.exceptions.RequestException as e:
print(f“请求异常: {e}”)
return None
6.4 配置文件示例 ( config.yaml )
api:
base_url: “https://www.douyin.com/aweme/v1/web/user/profile/other/”
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 Edg/120.0.0.0”
Accept: “application/json, text/plain, */*”
Accept-Language: “zh-CN,zh;q=0.9,en;q=0.8”
Accept-Encoding: “gzip, deflate, br”
Referer: “https://www.douyin.com/”
# Cookie: “你的Cookie(如果需要)” # 注意:Cookie是敏感信息,不要上传到公开仓库
params:
device_platform: “webapp”
aid: “6383”
channel: “channel_pc_web”
# 以下为需要定期从抓包结果中更新的动态参数
msToken: “你抓包获取的最新msToken”
X-Bogus: “你抓包获取的最新X-Bogus”
# _signature: “…” # 如果有也需要
delay:
min: 3 # 最小请求间隔(秒)
max: 7 # 最大请求间隔(秒)
7. 常见问题、排查技巧与优化建议
7.1 请求失败与错误码解读
- 返回
status_code不为0 :查看status_msg字段。常见的有“请求参数错误”、“用户不存在”、“签名校验失败”等。签名失败意味着X-Bogus等参数过期,需要重新抓包更新config.yaml。 - HTTP 403 Forbidden :通常是由于请求头不完整、签名错误或IP被暂时限制。检查请求头是否与抓包时一致,特别是
User-Agent、Referer。立即停止请求,等待一段时间再试,并考虑使用代理IP。 - HTTP 429 Too Many Requests :请求频率过高触发了风控。必须大幅增加请求间隔(
delay),建议设置在5秒以上,并加入随机抖动。 - 长时间无响应或超时 :可能是网络问题或目标服务器暂时故障。确保代理设置正确,并实现重试机制。
7.2 数据字段为空或为0
- 粉丝数/获赞数为0 :这可能是因为该博主设置了隐私权限,或者我们使用的接口权限不足(例如缺少有效的Cookie)。尝试在抓包时使用已登录抖音账号的手机,并将抓包到的Cookie填入配置文件的
headers中再试。 -
unique_id为空 :有些老用户或特殊账号可能没有设置“抖音号”。sec_user_id才是唯一可靠的标识。
7.3 自动化与稳定性优化
- 代理IP池 :对于大规模爬取,使用代理IP是必须的。可以购买付费代理服务,或者自建代理池。在
requests请求中通过proxies参数设置。proxies = {“http”: “http://your-proxy:port”, “https”: “https://your-proxy:port”} response = requests.get(url, headers=headers, proxies=proxies) - 用户代理(User-Agent)轮换 :准备一个UA列表,每次请求随机选择一个,降低被识别风险。
- 异步请求 :如果爬取目标很多,可以使用
aiohttp库进行异步IO请求,能极大提升效率。但务必注意控制并发量,避免对服务器造成攻击。 - 定期更新配置 :将更新
config.yaml中动态参数的过程脚本化。可以写一个辅助脚本,定期手动运行一次,它会打开Fiddler提示你进行抓包操作,然后自动解析最新的请求并更新配置文件。 - 数据去重与增量更新 :在保存数据前,检查
sec_user_id是否已存在,避免重复存储。可以记录爬取时间,实现增量更新。
7.4 法律与道德边界
最后必须再次强调,技术是一把双刃剑。
- 遵守
robots.txt:检查https://www.douyin.com/robots.txt,尊重网站的爬虫协议。 - 控制频率 :我们的代码中已经设置了随机延迟,这是最基本的道德和技术要求。切勿为了速度而疯狂请求。
- 明确用途 :确保你的数据爬取行为是用于个人学习、研究或合法的数据分析项目,而非商业爬虫、骚扰用户或侵犯隐私。
- 数据存储安全 :妥善保管爬取到的数据,不要公开传播他人的个人信息。
这个项目从抓包分析到代码实现,完整地走通了一个现代App数据爬取的流程。其中最大的挑战不在于写代码,而在于对网络协议的理解和逆向分析的耐心。当你成功运行代码,看到数据一条条被保存下来时,那种成就感是无可替代的。希望这份详细的实战指南和代码能为你打开一扇窗,更重要的是,理解其背后的原理和边界,负责任地使用这项技术。
更多推荐
所有评论(0)