抖音APP抓包分析实践

做内容/用户行为分析,抖音这类头部短视频APP的数据基本绕不开——但网页版要么功能阉割要么反爬天天更,APP版又有SSL Pinning、复杂签名、设备绑定这些门槛。

今天从入门级极简方案切入,梳理「抓包流量→识别核心API→抓包后响应解析→避开签名取数据」的完整思路,附能直接用抓包数据测试的Python简化工具。


一、前置操作:先拿到「能看懂的明文流量」

抓包是所有API分析、爬虫开发的第一步,不能跳过。

为什么要选「旧组合」?

为了绕过两大最基础的入门反抓

  • 系统证书信任限制:Android 12+默认不信任用户CA,必须装到Root后的系统目录;iOS同理也需越狱。选Android 9及以下/带Root/Frida的旧模拟器/旧真机最省心。
  • SSL Pinning(证书锁定):2024年中之后的新版抖音几乎全开启,只认内置的字节跳动根证书,抓包工具装CA没用。优先找23.5.0-23.9.0的旧版APK。

极简入门方案:雷电9模拟器 + Mitmproxy + 旧版抖音

这套组合门槛最低,新手15分钟内大概率能搞定:

1. 工具安装与基础配置

角色推荐工具安装/配置要点
流量捕获+HTTPS解密Mitmproxy/Mitmweb下载最新版Mitmproxy解压,终端/CMD运行mitmweb -p 8080(会自动打开8081的Web监控页)
模拟器WiFi代理设置雷电模拟器9(Android 9)右上角「设置」→「WiFi」→长按默认的LeidianWiFi→「修改网络」→「高级选项」→代理「手动」,IPv4填本机WLAN的局域网IP(Win11:cmdipconfig→找WLAN适配器的IPv4地址),端口填8080
模拟器安装CA证书Mitmproxy自带CA模拟器浏览器访问mitm.it→选Android下载→拖入模拟器「文件传输」→找到后安装,命名随意但要记住是「根证书」

2. 验证配置成功

  1. 在Web监控页(http://localhost:8081)先看有没有基础网络请求;
  2. 安装23.7.0左右的旧版抖音(别搜应用商店,找APKpure或历史版本下载站);
  3. 打开抖音刷3-5条推荐流,看Web监控页有没有aweme.snssdk.com开头的明文JSON请求——有就说明双关通过!

二、流量筛选:抓有用的「核心API」

抓到几百几千条请求别慌,重点看域名和返回格式:

  • 静态资源忽略p*.douyinpic.com(图片)、v*.douyinvod.com(视频);
  • 只留JSON明文aweme.snssdk.com开头、Content-Type是application/json的;
  • 高频好用的核心接口先记
功能接口路径片段关键必带参数
用户主页信息/aweme/v1/user/sec_user_id(加密稳定ID,比纯数字user_id好)
用户发布的全部视频(分页)/aweme/v1/aweme/post/sec_user_idcount(单页数量,最大一般20-30)、max_cursor(翻页游标,首次0)
单个视频详情/aweme/v1/aweme/detail/aweme_id(视频唯一ID)
推荐流数据/aweme/v1/feed/type=0count

三、代码实践:抓包后的「API响应解析工具」

⚠️ 特别声明:真实的抖音签名算法(_signaturex-gorgonx-khronosx-ss-stub等)极其复杂,涉及native层、动态生成函数,新手短时间内很难复现。这里提供的是「用抓包的真实完整URL/JSON做解析+保存」的工具,完全避开签名,能直接验证API响应结构和数据提取逻辑。

完整Python代码

import requests
import json
import csv
from typing import Dict, Any, List, Optional
from datetime import datetime


class DouyinDataParser:
    """抓包后抖音API响应解析与结构化保存工具"""

    @staticmethod
    def extract_user_posts(
        response_data: Dict[str, Any],
        only_save_public: bool = True
    ) -> List[Dict[str, Any]]:
        """
        从「用户发布视频列表」接口提取结构化数据
        :param response_data: 抓包后JSON转的Python字典
        :param only_save_public: 是否只保存公开可见的视频
        :return: 结构化后的视频列表
        """
        structured_videos = []
        raw_aweme_list = response_data.get("aweme_list", [])

        if not raw_aweme_list:
            print("未找到aweme_list字段,请检查是否传入了正确的接口响应!")
            return []

        for aweme in raw_aweme_list:
            try:
                # 过滤非公开视频
                if only_save_public and aweme.get("is_top", 0) != 1 and aweme.get("status", {}).get("allow_share", 1) != 1:
                    continue

                # 转换时间戳
                create_datetime = datetime.fromtimestamp(aweme.get("create_time", 0)).strftime("%Y-%m-%d %H:%M:%S")

                # 提取数据
                video_data = {
                    "视频ID": aweme.get("aweme_id"),
                    "视频标题": aweme.get("desc", "").strip().replace("\n", " "),
                    "发布时间": create_datetime,
                    "作者UID": aweme.get("author", {}).get("uid"),
                    "作者昵称": aweme.get("author", {}).get("nickname", "").strip(),
                    "作者抖音号": aweme.get("author", {}).get("unique_id", "").strip(),
                    "视频播放地址(CDN链接,可能失效快)": aweme.get("video", {}).get("play_addr", {}).get("url_list", [None])[0],
                    "视频封面地址": aweme.get("video", {}).get("cover", {}).get("url_list", [None])[0],
                    "点赞数": aweme.get("statistics", {}).get("digg_count", 0),
                    "评论数": aweme.get("statistics", {}).get("comment_count", 0),
                    "转发数": aweme.get("statistics", {}).get("share_count", 0),
                    "播放数": aweme.get("statistics", {}).get("play_count", 0),
                    "是否置顶": "是" if aweme.get("is_top", 0) == 1 else "否",
                }
                structured_videos.append(video_data)
            except Exception as e:
                print(f"解析单个视频数据失败,跳过:{str(e)}")
                continue

        return structured_videos

    @staticmethod
    def save_to_csv(data: List[Dict[str, Any]], filename: Optional[str] = None) -> None:
        """
        把结构化数据保存到CSV文件
        :param data: extract_*函数返回的结构化列表
        :param filename: 保存的文件名,默认带时间戳
        """
        if not data:
            print("没有可保存的数据!")
            return

        if not filename:
            filename = f"douyin_posts_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"

        try:
            # 用UTF-8 with BOM保存,避免Excel打开乱码
            with open(filename, mode="w", newline="", encoding="utf-8-sig") as f:
                writer = csv.DictWriter(f, fieldnames=data[0].keys())
                writer.writeheader()
                writer.writerows(data)
            print(f"数据保存成功!文件路径:{filename}")
        except Exception as e:
            print(f"保存CSV失败:{str(e)}")


def main():
    print("=" * 60)
    print("抖音抓包后API响应解析工具(仅用于学习API结构)")
    print("=" * 60)
    print("\n使用步骤:")
    print("1. 完成前置抓包,拿到「用户发布视频列表」接口的完整响应JSON;")
    print("2. 把JSON复制到当前目录下的「response.json」文件中;")
    print("3. 运行本工具即可自动解析并保存为CSV。")
    print("=" * 60 + "\n")

    # 读取本地JSON文件
    try:
        with open("response.json", mode="r", encoding="utf-8") as f:
            raw_response = json.load(f)
    except FileNotFoundError:
        print("❌ 未找到「response.json」文件,请按使用步骤操作!")
        return
    except json.JSONDecodeError:
        print("❌ 「response.json」格式错误,请检查是否是有效的JSON!")
        return

    # 解析数据
    parser = DouyinDataParser()
    posts = parser.extract_user_posts(raw_response)
    print(f"\n✅ 成功解析 {len(posts)} 条视频数据!")

    # 保存到CSV
    if posts:
        parser.save_to_csv(posts)


if __name__ == "__main__":
    main()

四、后续进阶与合规提示

进阶取数据方案(避开/解决签名)

如果不想只停留在抓包解析,想实现半自动/全自动取数据,可以试试这两个新手友好的方向:

  1. Appium + Mitmproxy 联动:用Appium模拟真人滑动、点击,Mitmproxy拦截真实接口的请求和响应,直接保存结构化数据,完全不需要复现签名
  2. Frida Hook 签名函数:如果有一定逆向基础,可以用Frida hook 抖音的libxgorgon.so或Java层的签名生成类,实时调用生成真实的x-gorgon等参数,配合Python requests模拟请求。

合规提示(非常重要)

⚠️ 请务必遵守《中华人民共和国网络安全法》《中华人民共和国数据安全法》《中华人民共和国个人信息保护法》:

  1. 只爬取公开可见的非敏感数据
  2. 不要高频请求,避免影响抖音的正常服务;
  3. 不要用于商业用途
  4. 不要传播爬取到的个人信息(如手机号、地址等)。