1. 项目概述与核心价值最近在做一个关于社交媒体数据分析的项目其中一个核心需求是持续、稳定地获取抖音账号的粉丝数据比如粉丝数、粉丝画像如性别、地域分布的变动趋势。直接调用官方API对于大多数普通开发者来说这条路基本走不通权限申请门槛高数据维度也有限。用传统的网页爬虫去抓取移动端数据你会发现抖音的网页端m.douyin.com和PC端展示的信息非常有限很多关键数据特别是动态更新的粉丝列表和详情都只在手机App里完整呈现。这就引出了一个经典的自动化测试工具——Appium。它能像真人一样操作手机App点击、滑动、输入文字。理论上我们可以写个脚本让Appium自动打开抖音进入个人主页然后不断地滑动粉丝列表把看到的数据“抄”下来。但这个方法效率太低了想象一下要采集一个百万粉账号的所有粉丝ID得让手机模拟滑动多久而且频繁的UI操作极其耗电、耗资源App很容易因为异常弹窗或网络波动而卡死稳定性堪忧。于是我引入了另一个神器Mitmproxy。它是一个支持HTTP/HTTPS的中间人代理工具。简单说就是让手机的所有网络请求都先经过你的电脑这样你就能看到抖音App在背后偷偷和服务器交换了哪些数据。粉丝列表、用户详情这些数据肯定是通过网络请求获取的我们如果能抓到这些请求并解析出来不就相当于拿到了数据源头吗所以这个项目的核心思路就清晰了“Appium负责自动化启动和登录抖音App模拟必要的用户操作如进入个人主页Mitmproxy负责在后台监听并解密App发出的网络请求从中精准提取出粉丝数据。”两者联动Appium解决“身份认证”和“触发数据请求”的问题Mitmproxy解决“高效、静默抓取数据包”的问题。最终实现的效果是脚本运行后你几乎看不到手机屏幕在疯狂滑动但粉丝数据已经 quietly 地流入了你的数据库。这套方案特别适合需要长期、定时监控特定账号粉丝变化或者进行小规模、多账号粉丝数据分析的场景。2. 环境搭建与工具链配置工欲善其事必先利其器。这套方案的环境搭建稍微有点繁琐但一步步来完全可以搞定。核心是配置好Appium和Mitmproxy并让它们能协同工作。2.1 Appium环境搭建以Android为例Appium是一个跨平台的移动端自动化测试框架。我们用它来操控手机。第一步基础环境安装安装Node.js和npmAppium服务器是基于Node.js的。去Node.js官网下载并安装LTS版本安装后会自带npm包管理器。安装Appium Server打开命令行通过npm全局安装Appium。npm install -g appium。安装完成后可以通过appium -v检查版本。为了更好的体验我强烈建议再安装一个图形化客户端appium-doctor来检查环境npm install -g appium-doctor然后运行appium-doctor它会告诉你还缺什么。安装Appium Client库这是用来写Python脚本连接Appium Server的。在你的项目虚拟环境中执行pip install Appium-Python-Client。第二步Android开发环境配置安装Android SDK推荐直接安装Android Studio它会帮你管理SDK。安装后打开SDK Manager确保安装了以下内容SDK Platforms至少选择一个Android版本如Android 13.0 “Tiramisu”的API Level 33。SDK Tools必须安装Android SDK Build-Tools、Android SDK Platform-Tools包含adb命令、Android Emulator如果你用模拟器。配置环境变量将Android SDK的platform-tools和tools目录路径添加到系统的PATH环境变量中。这样你才能在命令行里直接使用adb命令。准备测试设备可以是真机也可以是模拟器。真机用USB连接电脑在手机上开启“开发者选项”和“USB调试”模式。连接后在命令行输入adb devices应该能看到你的设备序列号。模拟器在Android Studio的AVD Manager中创建一个虚拟设备。启动后同样用adb devices确认连接。注意使用真机时部分品牌手机可能需要额外安装驱动或进行授权确认。如果adb devices显示设备为unauthorized去手机上查看是否有“允许USB调试”的弹窗点击允许。第三步Desired Capabilities配置这是Appium脚本的核心它告诉Appium你要启动哪个App、在什么设备上启动。一个基础的配置示例如下Pythonfrom appium import webdriver desired_caps { platformName: Android, # 平台 platformVersion: 13, # 安卓版本根据你的设备填写 deviceName: your_device_name, # 设备名adb devices 查到的名称 appPackage: com.ss.android.ugc.aweme, # 抖音的包名 appActivity: .splash.SplashActivity, # 抖音的启动Activity noReset: True, # 不重置App数据避免每次重新登录 automationName: UiAutomator2, # 自动化引擎Android推荐这个 newCommandTimeout: 600, # 命令超时时间设长一点 udid: your_device_udid # 设备唯一标识防止多设备冲突 } driver webdriver.Remote(http://localhost:4723/wd/hub, desired_caps)这里的关键是appPackage和appActivity。如何获取有一个简单的方法打开抖音App停留在你想启动的页面如首页然后在命令行输入adb shell dumpsys window | findstr mCurrentFocusWindows或adb shell dumpsys window | grep mCurrentFocusMac/Linux输出结果里就有当前的包名和Activity。2.2 Mitmproxy环境搭建与证书配置Mitmproxy是我们的“数据监听员”。第一步安装Mitmproxy直接用pip安装即可pip install mitmproxy。这会同时安装mitmproxy命令行交互式、mitmdump命令行非交互式我们主要用这个和mitmwebWeb图形界面三个工具。第二步配置代理与安装证书这是最关键的步骤目的是让手机信任Mitmproxy颁发的证书从而解密HTTPS流量。启动Mitmproxy在电脑上启动mitmdump监听默认端口8080mitmdump -s your_script.py。-s参数后面跟你的Python处理脚本可以先不加。配置手机代理确保手机和电脑在同一个Wi-Fi网络下。在手机的Wi-Fi设置中修改当前网络为手动代理服务器地址填写你电脑的局域网IP在命令行输入ipconfig或ifconfig查看端口填写8080。安装CA证书在手机浏览器中访问mitm.it。这是一个Mitmproxy提供的便捷安装页面。根据你的手机系统Android/iOS点击对应的图标下载证书。对于Android下载后进入系统设置 - 安全 - 加密与凭据 - 安装证书 - CA证书找到下载的文件进行安装。安装时可能需要设置锁屏密码。证书安装成功后务必重启mitmdump否则可能不生效。实操心得Android 7.0以上系统App默认不信任用户安装的CA证书导致无法解密部分App包括抖音的HTTPS流量。解决方法有两种一是将Mitmproxy的CA证书安装到系统信任的凭据存储区这通常需要Root权限二是对测试用的App进行重新打包在其网络配置中信任用户证书。对于抖音我们采用另一种更实用的方案只抓取我们能解密的请求。幸运的是抖音的部分API接口尤其是数据接口使用的证书校验策略可能不那么严格或者我们只需要关注其HTTP请求的URL和参数规律有时即使不解密内容也能通过请求模式进行分析。但为了最大化成功率建议使用已Root或可调试的系统镜像如模拟器或开发测试机。第三步验证代理是否生效在手机代理设置好、mitmdump运行、证书安装后用手机浏览器访问一个普通HTTP网站如http://example.com观察mitmdump的命令行窗口应该能看到滚动的请求日志。如果能抓到包说明代理配置基本成功。3. 联动策略设计与核心脚本解析环境搭好了现在来设计Appium和Mitmproxy如何“打配合”。核心思想是Appium做“手”和“眼”触发数据加载Mitmproxy做“耳”窃听数据通道。3.1 分工与协作流程启动阶段首先启动Mitmproxymitmdump并加载我们编写的Python解析脚本。然后启动Appium Server最后运行我们的主控Python脚本该脚本通过Appium Client连接Server并启动抖音。登录与导航主控脚本利用Appium驱动抖音完成登录如果未登录并导航到目标账号的个人主页。这部分完全模拟真人操作可能通过搜索用户名或者直接访问个人主页链接。触发数据请求进入粉丝列表页面。这里Appium只需要执行一个关键动作点击“粉丝”Tab。一旦点击抖音App就会向服务器发起请求获取粉丝列表数据。监听与抓取当粉丝列表的请求发出时经过手机代理的设置请求会流经我们电脑上的Mitmproxy。我们在Mitmproxy的脚本中预先写好了规则专门筛选URL中包含特定关键词如/aweme/v1/user/follower/list/的请求。解析与存储Mitmproxy脚本捕获到目标请求后直接解析其响应内容通常是JSON格式提取出粉丝ID、昵称等信息然后存入本地文件或数据库。在这个过程中Appium不需要再去滑动列表加载更多因为Mitmproxy抓取的是接口返回的原始数据包一个请求可能就包含了20个、50个甚至100个粉丝数据。我们只需要分析接口的分页参数如max_time,cursor然后在Mitmproxy脚本里模拟构造后续分页请求或者简单地控制Appium触发几次“上拉加载更多”即可。循环与结束重复步骤3-5直到抓取到足够的数据或翻页结束。最后主控脚本关闭Appium驱动Mitmproxy也停止工作。3.2 Appium脚本核心触发与容错Appium脚本的任务相对单纯主要是“导航到粉丝列表页面”。但实际写起来需要考虑各种弹窗和状态判断。from appium.webdriver.common.touch_action import TouchAction import time def navigate_to_follower_list(driver, target_username): 导航到指定用户的粉丝列表页 # 1. 处理可能的启动页、青少年模式弹窗等 time.sleep(3) # 这里可以加入一些通用的弹窗关闭逻辑例如通过ID或XPath查找“我知道了”、“跳过”按钮并点击 # driver.find_element(AppiumBy.ID, com.ss.android.ugc.aweme:id/close_btn).click() # 2. 搜索目标用户假设从首页开始 search_btn driver.find_element(AppiumBy.ACCESSIBILITY_ID, 搜索按钮) # 可能需要用XPath search_btn.click() time.sleep(2) search_box driver.find_element(AppiumBy.ID, com.ss.android.ugc.aweme:id/search_input) search_box.send_keys(target_username) driver.press_keycode(66) # 模拟键盘回车键 time.sleep(3) # 3. 进入用户主页 # 假设第一个搜索结果就是目标用户 first_result driver.find_element(AppiumBy.XPATH, //android.widget.ListView/android.widget.RelativeLayout[1]) first_result.click() time.sleep(4) # 等待主页加载 # 4. 点击“粉丝”Tab # 粉丝Tab的定位可能比较麻烦需要借助UI Automator Viewer或Appium Inspector来查看具体控件信息 # 这里是一个示例XPath实际情况可能需要调整 follower_tab driver.find_element(AppiumBy.XPATH, //*[text粉丝]) follower_tab.click() time.sleep(5) # 等待粉丝列表加载这个时间很关键确保网络请求发出 print(已成功进入粉丝列表页面Mitmproxy可以开始抓包。) # 5. 可选触发多次加载。简单做法缓慢滑动几次模拟人工浏览。 # 获取屏幕尺寸 size driver.get_window_size() start_x size[width] / 2 start_y size[height] * 0.7 end_y size[height] * 0.3 for i in range(5): # 计划滑动5次加载更多数据 driver.swipe(start_x, start_y, start_x, end_y, duration800) time.sleep(3) # 每次滑动后等待数据加载 print(f已触发第{i1}次滑动加载。)注意事项UI自动化最大的敌人是界面变化和网络延迟。上述代码中的time.sleep是简单的等待在实际项目中应替换为更可靠的“显式等待”WebDriverWait等待特定元素出现后再操作这样脚本更健壮。此外所有控件的定位符ID、XPath都可能随抖音版本更新而改变需要定期维护。3.3 Mitmproxy脚本核心过滤、解析与存储这才是数据采集的“大脑”。我们编写一个addons.py脚本供mitmdump -s addons.py调用。import json from mitmproxy import http, ctx import sqlite3 import re class DouyinFollowerCapture: def __init__(self): self.follower_list_url_pattern re.compile(r/aweme/v1/user/follower/list/) # 粉丝列表接口特征 self.db_conn sqlite3.connect(douyin_followers.db) self.init_db() def init_db(self): 初始化数据库表 cursor self.db_conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS followers ( uid TEXT PRIMARY KEY, nickname TEXT, unique_id TEXT, signature TEXT, captured_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) self.db_conn.commit() def request(self, flow: http.HTTPFlow): 请求阶段可以修改请求。这里我们用来添加自定义Header或记录URL。 # 如果需要可以在这里添加一些请求头例如模拟特定的User-Agent pass def response(self, flow: http.HTTPFlow): 响应阶段处理我们感兴趣的响应。 # 1. 检查URL是否匹配粉丝列表接口 if self.follower_list_url_pattern.search(flow.request.pretty_url): ctx.log.info(f捕获到粉丝列表请求: {flow.request.url}) # 2. 检查响应状态码和内容类型 if flow.response.status_code 200 and application/json in flow.response.headers.get(content-type, ): try: # 3. 解析JSON响应 response_data json.loads(flow.response.text) # 4. 提取粉丝数据。抖音的JSON结构需要实际抓包分析。 # 通常数据在 response_data[followers] 或 response_data[data] 下 followers response_data.get(followers, []) or response_data.get(data, []) if not isinstance(followers, list): ctx.log.warn(f未找到粉丝列表响应结构: {response_data.keys()}) return # 5. 遍历并存储 cursor self.db_conn.cursor() for follower in followers: # 提取关键字段字段名需根据实际抓包结果调整 uid follower.get(uid) or follower.get(user_id) nickname follower.get(nickname) unique_id follower.get(unique_id) # 抖音号 signature follower.get(signature) # 个性签名 if uid: # 确保有用户ID try: cursor.execute( INSERT OR REPLACE INTO followers (uid, nickname, unique_id, signature) VALUES (?, ?, ?, ?) , (uid, nickname, unique_id, signature)) ctx.log.info(f保存粉丝: {nickname} (UID: {uid})) except sqlite3.Error as e: ctx.log.error(f数据库插入失败: {e}) self.db_conn.commit() ctx.log.info(f本轮成功处理 {len(followers)} 个粉丝数据。) # 6. 高级解析分页参数为自动化翻页做准备 has_more response_data.get(has_more, 0) max_time response_data.get(max_time) # 或 cursor ctx.log.info(f分页信息 - has_more: {has_more}, max_time: {max_time}) # 可以将 max_time 传递给Appium脚本用于控制是否继续滑动触发加载 except json.JSONDecodeError as e: ctx.log.error(fJSON解析失败: {e}, URL: {flow.request.url}) except Exception as e: ctx.log.error(f处理响应时发生未知错误: {e}) else: ctx.log.warn(f请求未成功或非JSON响应: {flow.response.status_code}) # 可以添加其他接口的匹配规则例如用户详情接口 /aweme/v1/user/profile/other/ 等 def done(self): 组件关闭时调用用于清理资源 self.db_conn.close() ctx.log.info(数据库连接已关闭。) # 将插件实例添加到mitmproxy addons [DouyinFollowerCapture()]这个脚本做了几件关键事URL过滤只处理包含特定路径的请求避免处理大量无关流量。数据解析解析JSON定位到粉丝数据数组。数据存储将解析出的粉丝信息存入SQLite数据库使用INSERT OR REPLACE避免重复。分页感知记录接口返回的has_more和max_time字段为后续自动化翻页提供决策依据。实操心得抖音的接口参数和返回结构可能经常变动。最可靠的方法是先手动操作一遍用Mitmproxy抓包找到真正的粉丝列表请求URL和响应格式。使用mitmweb工具可以图形化地查看请求和响应非常方便。找到正确的接口后再用正则表达式或字符串匹配来过滤。4. 高级技巧与实战优化基础流程跑通后我们会发现很多实际问题。下面分享一些提升稳定性、效率和隐蔽性的技巧。4.1 应对Appium的UI识别难题抖音的UI控件ID经常变化或者在不同机型上表现不一致。纯靠ID或固定XPath定位非常脆弱。策略一使用相对定位和模糊匹配from appium.webdriver.common.appiumby import AppiumBy from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 使用文本内容定位“粉丝”Tab比用ID更稳定 follower_tab WebDriverWait(driver, 10).until( EC.element_to_be_clickable((AppiumBy.XPATH, //*[contains(text, 粉丝)])) ) follower_tab.click() # 如果“粉丝”Tab在某个特定的布局容器里可以结合父节点定位 # 例如先找到“关注”、“粉丝”、“获赞”所在的父布局再在其中找“粉丝” tab_container driver.find_element(AppiumBy.ID, com.ss.android.ugc.aweme:id/tab_container) follower_in_container tab_container.find_element(AppiumBy.XPATH, .//*[text粉丝])策略二图像识别辅助备用方案当UI结构完全无法定位时可以借助OpenCV进行简单的图像模板匹配作为最后的手段。但这会显著增加复杂度和运行时间。4.2 提升Mitmproxy抓包成功率与效率1. 处理未知的HTTPS请求 如果发现关键的粉丝列表请求无法解密响应体是乱码可能是抖音使用了证书绑定SSL Pinning。应对方法有使用已Root的手机并安装JustTrustMe等Xposed模块这类模块可以绕过证书绑定。但这属于较深度的系统修改。使用Frida等动态插桩工具在运行时Hook掉SSL验证逻辑。这需要一定的逆向工程能力。我们的策略对于数据采集如果主要接口无法解密可以退而求其次分析未解密的请求模式。即使看不到响应内容我们也能看到请求的URL、频率、参数变化规律。结合Appium获取的少量界面数据如第一页粉丝昵称可以尝试反推接口逻辑。或者集中精力抓取那些能被解密的次要接口来获取补充信息。2. 优化过滤规则减少性能开销 Mitmproxy会处理所有流经的请求如果规则太宽泛会消耗大量CPU和内存。要精确过滤。# 更精确的URL匹配结合请求方法 if flow.request.method GET and /aweme/v1/user/follower/list/ in flow.request.path: # 进一步检查查询参数例如是否包含特定的user_id query flow.request.query target_user_id 123456789 # 你要监控的目标用户ID if query.get(user_id) target_user_id: # 这才是我们要处理的目标请求 process_follower_list(flow)3. 实现自动化分页 粉丝列表是分页的。我们可以在Mitmproxy的response方法中解析出has_more和max_time或cursor。然后通过某种方式通知Appium脚本“这一页抓完了可以滑动加载下一页了”。一个简单的实现方式是使用一个共享文件或队列。Mitmproxy脚本将has_more和max_time写入一个临时文件。Appium脚本在每次滑动后读取这个文件如果has_more为1则继续滑动如果为0则停止。更优雅的方式是使用进程间通信IPC比如Redis或简单的Socket但这会增加架构复杂度。4.3 稳定性与反爬考量1. 请求频率控制 无论是Appium的操作还是Mitmproxy触发的接口请求都要模拟人类行为加入随机延迟。import random, time def human_like_delay(min_s1, max_s3): time.sleep(random.uniform(min_s, max_s))在Appium滑动和Mitmproxy处理完一批数据后都调用这个函数。2. 账号安全 长期用同一个账号高频采集数据有被风控甚至封禁的风险。使用多个账号轮换准备几个“小号”在Appium脚本中实现自动切换登录。避免全天候采集设置合理的采集时间窗口例如每天只在活跃时段运行几小时。模拟完整用户行为除了点击粉丝列表偶尔可以模拟浏览视频、点赞等行为让账号行为更像真人。3. 异常处理与状态恢复 脚本必须健壮。要考虑网络中断、App崩溃、意外弹窗等情况。Appium使用try...except包裹关键操作捕获NoSuchElementException,TimeoutException等。出现异常时可以尝试截图保存现场然后重启App或从某个检查点恢复。Mitmproxy做好数据库操作的异常捕获和日志记录。定期检查数据库连接状态。4. 数据去重与增量更新 使用数据库的UNIQUE约束或INSERT OR IGNORE/REPLACE语句。在采集前可以先查询已采集的最新max_time从那个时间点之后开始请求实现增量采集。5. 常见问题排查与解决方案实录在实际操作中你几乎一定会遇到下面这些问题。这里是我踩过坑后的经验总结。5.1 Appium连接或操作失败问题1adb devices能识别设备但Appium连接时报错Cannot start the ‘app’ activity。原因appActivity参数不正确或者App有多个启动Activity。解决使用adb shell dumpsys window | grep mCurrentFocus命令时确保抖音App正处于你希望脚本启动后进入的初始页面通常是启动后的主页面。获取到的Activity可能不是标准的启动页。一个更稳妥的方法是查阅资料或使用APK分析工具如apkanalyzer来查找主Activity。也可以尝试不指定appActivity只指定appPackage让Appium自动探测。问题2脚本运行时找不到页面元素NoSuchElementException。原因页面还没加载完就执行查找UI结构已更新元素在嵌套的WebView中。解决增加等待用WebDriverWait替代time.sleep。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC element WebDriverWait(driver, 15).until( EC.presence_of_element_located((AppiumBy.ID, some_id)) )更新定位符使用最新的appium inspector或UI Automator Viewer重新查看元素属性。检查上下文Context如果元素在H5页面WebView里需要先切换到WebView上下文driver.switch_to.context(WEBVIEW_com.ss.android.ugc.aweme)操作完再切回来driver.switch_to.context(NATIVE_APP)。5.2 Mitmproxy抓不到包或HTTPS解密失败问题1手机设置了代理但mitmdump看不到任何请求。原因电脑防火墙阻止了8080端口手机和电脑不在同一网络代理设置未保存。解决关闭电脑防火墙或为mitmdump添加入站规则。确保手机连接的Wi-Fi和电脑是同一个局域网。在手机Wi-Fi设置中保存代理后有时需要关闭再打开Wi-Fi开关。尝试在电脑上使用mitmweb通过浏览器界面查看更直观。问题2能看到HTTP请求但抖音的HTTPS请求显示TLS handshake failed或响应是乱码。原因CA证书未正确安装或不被抖音信任SSL Pinning。解决重新访问mitm.it下载并安装证书。Android 10 系统安装证书时一定要选“VPN和应用”或“所有应用”的凭据存储位置如果只有“用户”选项则可能对部分App无效。对于无法绕过SSL Pinning的情况可以尝试抓取模拟器的流量。一些Android模拟器如官方模拟器、夜神的系统镜像可以更容易地安装系统级证书通过拖拽证书文件到模拟器并设置。如果只是为了分析请求规律可以关注未被解密的请求的URL和查询参数这些信息仍然是明文可见的。问题3抓包过程中抖音App提示“无网络连接”或无法加载内容。原因Mitmproxy的拦截影响了某些长连接或CDN请求。解决在Mitmproxy脚本的request方法中将某些域名加入忽略列表。def request(self, flow: http.HTTPFlow): ignore_hosts [log-upload.tiktokv.com, monitor.tiktokv.com, *.snssdk.com] # 示例 if any(flow.request.pretty_host.endswith(host.replace(*., )) for host in ignore_hosts): ctx.log.info(f忽略请求: {flow.request.url}) return这需要观察哪些域名的请求失败导致了App异常然后将其排除。5.3 数据解析与存储问题问题Mitmproxy脚本能抓到请求但解析JSON时出错或提取不到数据字段。原因抖音接口返回的数据结构复杂或已加密接口版本更新导致字段名变化。解决仔细分析原始响应使用mitmweb或将响应体保存到文件用JSON格式化工具仔细查看结构。关键数据可能藏在多层嵌套下或者是一个经过Base64编码的字符串。打印调试在脚本中打印出response_data.keys()或整个响应结构的概要找到数据真正的路径。关注加密字段有时粉丝列表中的用户ID或昵称可能是加密的。需要观察其他接口如用户主页信息接口是否返回了明文信息或者尝试寻找其解密规律这涉及逆向分析难度较大。对于基础数据采集可能只需要关注接口的宏观调用规律。这套AppiumMitmproxy的联动方案将UI自动化的“触发能力”与网络抓包的“窃听效率”相结合在移动端数据采集上开辟了一条实用路径。它最大的优势在于只要App本身能正常展示数据我们就有机会通过监听网络通道来获取。整个过程就像是在数据的“高速公路”旁设立了一个检查站车Appium把货数据请求引出来我们Mitmproxy再对货物进行快速分拣。当然这条路需要你耐心地铺设“铁轨”环境配置和应对各种“天气变化”反爬与变更。在实际操作中保持工具的更新多分析抓包数据勤于编写异常处理你的数据采集管道就会越来越稳定。最后务必在法律法规和平台用户协议允许的范围内使用此类技术将数据用于正当的分析和学习目的。