ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Tinder研究工具:从抓包到滑动自动化,破解推荐机制与匹配数据

Tinder研究工具:从抓包到滑动自动化,破解推荐机制与匹配数据 简介Tinder-1.2.2 是一份面向 Java 开发者与算法学习者的开源工具源码包聚焦数据匹配、推荐系统或算法优化场景可帮助读者理解工具内核与实现原理。资源共53个文件其中 Java 源文件达50个覆盖核心逻辑与测试用例另有 HTML 说明、XML 配置和 serialized 数据文件压缩包仅127KB结构紧凑便于快速下载和本地分析。通过精读源码可以学习模块化、可扩展、可维护的代码组织方式掌握哈希表快速查找与并行计算等高效数据处理策略并借鉴其错误处理和异常管理的最佳实践同时 Tinder-1.2.2 作为次要更新版本包含功能增强与缺陷修复为二次开发提供了清晰参考。在实际项目中这些实现可用于数据清洗、行为偏好匹配或模型训练辅助显著提升开发效率。已有1632人学习下载适合具备一定 Java 基础、希望深入剖析匹配/推荐算法实现并在此基础上进行定制优化的开发者。1. tinder 研究工具不是黑匣子先弄清楚这份源码能做什么收到过不止一位同行问类似的问题tinder 没有开放官方 API想研究它的推荐机制、想做数据整理难道只能靠手工截图结果是反直觉的——移动端请求用的不是 OAuth而是一套私有令牌协议核心字段叫 x-auth-token。拿到这个令牌之后推荐列表、滑动判定、匹配记录都会以普通 JSON 的形式出现在你面前。这套源码工具做的事情就是把抓包、令牌管理、滑动动作、数据入库和统计验证串成一条本地可跑的流水线适合想拆接口的客户端开发者、做社交产品调研的数据同学以及所有不想靠手速刷卡的研究者。一句话它不教你“怎么多匹配”它把 tinder 变成一份可离线分析的数据集。2. 抓包与令牌机制X-Auth-Token 从哪来、怎么续命拿到 tinder 核心请求链路的入口只有两步把 App 的流量导到 mitmproxy然后从请求头里找到 x-auth-token。这个 header 几乎是所有接口的通用钥匙后续的刷卡片、看匹配、拉详情全都走它。下面按我实际操作的顺序展开。2.1 先搭抓包环境mitmproxy 证书安装与只过滤 tinder 流量mitmproxy 是我在客户端协议分析里最常用的一套工具它比 Charles 更适合后续脚本化处理因为过滤、保存、回放都能走命令行和 Python API。启动方式很多我一般用 mitmweb它自带 Web 界面方便快速看请求列表mitmweb --listen-port 8080 --set block_globalfalse --set stream_large_bodies5m说明--listen-port 8080是让 mitmweb 监听 8080 端口手机或模拟器把 Wi-Fi 代理指向这台机器的 IP:8080 即可block_globalfalse是允许非本机发来的连接否则真机流量会被直接拒绝stream_large_bodies5m是把超过 5MB 的响应体按流式处理避免照片、视频把内存撑爆。流量进来后界面会非常吵必须做过滤。推荐直接加 URL 过滤参数启动mitmproxy --listen-port 8080 --set filter~u api.gotinder.com --save-stream-file tinder_flows.flow说明~u api.gotinder.com是 mitmproxy 的过滤语法意思是只显示 URL 里包含该域名的请求--save-stream-file会把全部流量先落盘成.flow文件哪怕界面卡了数据也还在后面可以离线再分析。证书安装这一步决定后续能不能看到明文。iOS 上要下载证书后在“设置-通用-关于本机-证书信任设置”里打开开关Android 7 以上 App 默认不信任用户证书要么把证书装进系统分区要么用支持调试的包。这一步属于环境准备多数人卡在这里不是因为证书本身而是没意识到抓包工具和设备的时钟要一致时间偏差过大会导致 TLS 校验失败。提示抓包完毕记得关掉代理否则手机会一直断网。2.2 从请求头里提取 X-Auth-Token再从响应体读 refresh token抓包文件里已经包含了完整的请求和响应接下来要做的是把令牌抽出来。我一般直接写个一次性脚本扫 flow 文件避免开着界面翻半天from mitmproxy import io from mitmproxy.exceptions import FlowReadException with open(tinder_flows.flow, rb) as f: reader io.FlowReader(f) for flow in reader.stream(): req flow.request res flow.response if req.pretty_host.endswith(gotinder.com): token req.headers.get(x-auth-token, ) if token: print(req.method, req.path, token[:16], ...) # 登录/刷新接口的响应体里通常带 refresh_token if req.path.startswith(/v2/auth) and res: body res.get_text() or if refresh_token in body: print(REFRESH:, body[:200])逻辑说明x-auth-token出现在大多数 API 请求头里打印前 16 位做确认就够了/v2/auth路径下的响应才会返回refresh_token把它和x-auth-token一起存到本地作为后续会话恢复的凭据。注意这里用的是FlowReader.stream()迭代方式适合大文件不会一次性把整个文件读进内存。token 续命是一个关键点。tinder 的令牌不是永久的常见有效期也就一两个小时到期后接口直接返回 401。常见做法是保存refresh_token在需要时调刷新接口换新令牌def refresh_session(refresh_token): headers {content-type: application/json} data json.dumps({refresh_token: refresh_token}) r requests.post(https://api.gotinder.com/v2/auth/refresh, datadata, headersheaders) if r.status_code 200: return r.json()[data]说明刷新接口的入参只有一个refresh_token返回体里的data同时包含新的token和新的refresh_token两个值都要覆盖到本地。参数上有个坑refresh_token是一次性的一旦刷新成功旧值立刻失效不能留着当备用否则第二次用就会报错。3. 手动复刻核心流程swipe 自动化与匹配监控落地令牌链路打通以后剩下的就是业务动作。tinder 的移动端协议里推荐、滑动、匹配都是很规整的 HTTP 接口不需要走任何诡异的二进制协议一个 Python 类就能完全复刻。3.1 最小客户端把 recommendations 和 like/pass 封装成类先写一个最小的会话类把请求头、令牌刷新和核心动作包在一起import time import requests class TinderSession: API https://api.gotinder.com def __init__(self, token, refresh_tokenNone): self._token token self._refresh_token refresh_token self._start time.time() def _headers(self): return { x-auth-token: self._token, platform: android, content-type: application/json, } def recommendations(self, limit30): url f{self.API}/v2/recs/core r requests.get(url, headersself._headers(), params{limit: limit}) r.raise_for_status() return r.json().get(data, {}).get(results, []) def like(self, person_id): url f{self.API}/like/{person_id} r requests.get(url, headersself._headers()) return r.json() def pass_user(self, person_id): url f{self.API}/pass/{person_id} r requests.get(url, headersself._headers()) return r.json()逻辑说明recommendations返回的是推荐卡片列表每一条的user字段里有_id、姓名、出生日期、兴趣、照片等like和pass_user的路径直接把 person_id 拼在 URL 上返回体里的match字段是布尔值为true才表示真正匹配成功。参数上要注意limit别超过 100实测超过后接口会返回参数错误。还有一个反直觉的细节like 和 pass 在官方协议里是 GET 请求不是 POST。很多同学拿到接口后习惯写成 POST结果永远 404。追根溯源这个接口设计得比较老保留了一开始的风格我们按客户端实际抓到的请求方式来就行。调用策略上我一般不会直接全量刷加一个随机间隔session TinderSession(token..., refresh_token...) for person in session.recommendations(limit20): user person.get(user, {}) if user.get(birth_date): session.like(user[_id]) else: session.pass_user(user[_id]) time.sleep(random.uniform(1.2, 2.6))逻辑说明这里用birth_date是否存在做一次粗糙的筛选只是为了说明“滑动条件可以自己定”并不是什么玄学策略。参数说明随机间隔取 1.2 到 2.6 秒是模拟真人滑动卡片的经验值低于 0.8 秒连续请求几十次很容易触发风控验证这点后面避坑章还要展开。3.2 匹配监控增量落库与去重滑动只是过程真正的研究价值在匹配结果。我的做法是把每次查询到的 matches 增量写入 SQLite这样后面做统计时不用重新请求接口。import sqlite3 def init_db(conn): conn.execute( CREATE TABLE IF NOT EXISTS matches ( match_id TEXT PRIMARY KEY, person_id TEXT, name TEXT, matched_at TEXT, source_op TEXT ) ) def fetch_matches(session): url f{session.API}/v2/matches r requests.get(url, headerssession._headers(), params{count: 30}) data r.json()[data][matches] for m in data: person m.get(person) or m.get(user) or {} yield (m[_id], person.get(_id), person.get(name), m.get(matched_at))逻辑说明/v2/matches接口返回的每条匹配记录里对方资料字段可能是person也可能是user两个都做兜底更稳。入库时必须用match_id做去重因为同一匹配会在多次轮询里重复出现。source_op字段用于记录这次匹配是哪次滑动产生的后面统计“哪类画像匹配率高”时全靠它。conn sqlite3.connect(tinder.db) init_db(conn) session TinderSession(token..., refresh_token...) for match_id, person_id, name, matched_at in fetch_matches(session): conn.execute( INSERT OR IGNORE INTO matches VALUES (?, ?, ?, ?, like), (match_id, person_id, name, matched_at), ) conn.commit()说明INSERT OR IGNORE是主键冲突时的默认策略重复写入直接跳过比先 SELECT 再 INSERT 简单。参数说明count30是单页上限接口返回里如果有next_page_token还要继续请求下一页否则会漏掉较早的记录完整分页逻辑建议加一个while循环我这里只给出单页处理是为了把去重逻辑讲清楚。注意轮询频率不要太密。匹配列表不是实时消息拉一次管几分钟就够了我一般控制在 5 分钟一拉。4. 把 JSON 变成可用画像字段抽取、照片去重与本地存储推荐接口返回的 JSON 很臃肿一层套一层直接用 Excel 打开没有可读性。必须把字段抽出、清洗、落盘才能做后面的对照实验。4.1 画像字段抽取处理嵌套 user 对象和缺失值写解析脚本时最容易翻车的地方是字段路径在不同账号、不同版本下不一致。我的习惯是写一个防御式读取函数def extract_profile(user: dict) - dict: def safe(d, *keys): cur d for k in keys: if not isinstance(cur, dict): return None cur cur.get(k) return cur return { user_id: safe(user, _id), name: safe(user, name), age: calc_age(safe(user, birth_date)), bio: safe(user, bio), interests: safe(user, interests, name) or [], distance_mi: safe(user, distance_mi), schools: safe(user, schools, name) or [], jobs: [j.get(title) for j in (safe(user, jobs) or []) if j], }逻辑说明safe函数按 key 顺序逐层取只要中间某一层不是 dict就返回None不会抛TypeError。这样不管是interests结构还是schools结构发生变化脚本都不会整个崩掉最多是某个字段为空。参数说明age需要从birth_date推算原始值一般是 ISO 格式时间串计算时要固定时区否则跨日边界会差一岁interests的路径在不同地区版本里出现过两种写法一种是interests[].name另一种是topics[].name建议先打印一次真实结构再定抽取规则。4.2 照片归档按 user_id 建目录、用文件哈希去重图片下载是资源包里最容易被人忽视的部分。照片 URL 上带 CDN 签名参数同一个图在不同时间拿到的 URL 可能不一样但二进制内容一模一样所以不能用 URL 当文件名必须用内容哈希。import os import requests from hashlib import sha1 def save_photo(user_id, url, base_dirphotos): d os.path.join(base_dir, user_id) os.makedirs(d, exist_okTrue) r requests.get(url, timeout10) if r.status_code ! 200: return content_hash sha1(r.content).hexdigest() ext url.split(?)[0].split(.)[-1] ext jpg if ext not in (jpg, png, webp) else ext path os.path.join(d, f{content_hash}.{ext}) if not os.path.exists(path): with open(path, wb) as f: f.write(r.content)逻辑说明先按user_id建目录再写文件方便后面按人查看写入前用content_hash判断文件是否已存在完全相同的图片不会重复落盘。参数说明timeout10比较关键推荐流里图片源很杂偶发一个慢请求不至于把整个任务卡死扩展名判断不能直接用 URL 尾部因为 URL 后面还有?和一堆参数必须先split(?)[0]否则拼出来的文件名会带一串无效字符。批量下载时不要一股脑并发几十个请求我一般用 4 个线程from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers4) as pool: for person in persons: photos person.get(photos, [])[:1] # 只取第一张当头像 for photo in photos: pool.submit(save_photo, person[_id], photo[url])说明只取第一张是做调研时的常用策略能控制数据量max_workers4是带宽和频率之间的折中跑本机抓包环境时足够不用上大并发。5. 排查手册Token 失效、429 限流与字段缺失怎么处理这套流程跑起来不难真正烦人的是跑着跑着忽然 401、429或者解析出来全是空值。下面几条踩坑记录都是从实际复现里整理出来的按“现象→原因→解决”写。5.1 请求突然 401Token 失效还是入口参数变了现象连续运行 20 分钟后所有请求突然返回 401打印 header 里的 token 看起来还是原来的字符串。原因x-auth-token的有效期并不长而且它不会在响应头里给你任何“即将过期”的提示。等到第一轮 token 失效后后续请求全部被拒。还有一个隐蔽情况如果在多线程里同时调用刷新接口旧refresh_token被第一次请求用掉后第二次刷新必然失败导致“越刷新越 401”。解决在TinderSession里加一个ensure_token方法每次发起请求前检查时间超过 30 分钟就先刷新一次并且加锁import threading class TinderSession: def __init__(self, token, refresh_tokenNone): self._token token self._refresh_token refresh_token self._start time.time() self._lock threading.Lock() def ensure_token(self): with self._lock: if time.time() - self._start 1800: data refresh_session(self._refresh_token) self._token data[token] self._refresh_token data[refresh_token] self._start time.time()说明锁的作用是防止多个线程同时进入刷新逻辑。参数上30 分钟是一个保守值如果 token 实际有效期较短也不会踩到边界刷新成功后旧refresh_token就作废了所以本地存储务必覆盖更新。5.2 连续操作后被 429限流策略与间隔设置不合理现象快速刷了 40 张卡片后所有请求开始 429日志里没有任何业务错误响应头里偶尔带着retry-after。原因服务端对单条会话的请求频率有约束连续高频滑动会被判定为非人工操作。固定间隔也不是最优解因为固定节奏本身就容易被识别。解决读retry-after做退避同时把间隔改成随机抖动time.sleep(max(float(r.headers.get(retry-after, 3)), random.uniform(1.2, 2.6)))说明retry-after存在时以它为准不存在时按 3 秒兜底。参数上我一般每刷 40 次主动休息 15 分钟而不是等 429 出现后再处理这个 40 次不是官方值只作为自己脚本里的软限实际调整看日志里 429 出现的频率。5.3 返回的字段对不上接口版本回退与字段迁移现象同样一段解析代码换一个账号或换一台设备后interests抽取结果全是空数组其他字段都正常。原因不同版本接口对同一语义的字段路径做过调整比如兴趣字段从interests[].name变成了topics[].name生物字段也可能从bio变成bio_short。解析脚本只写死了一条路径遇到新结构就静默失败。解决把原始 JSON 完整落盘一份按user_id命名文件。遇到空字段时先打印 key 树再决定改哪条路径def walk_keys(obj, prefix): if isinstance(obj, dict): for k, v in obj.items(): print(f{prefix}.{k}, type(v).__name__) if isinstance(v, (dict, list)): walk_keys(v, f{prefix}.{k}) elif isinstance(obj, list) and obj: walk_keys(obj[0], prefix [0])说明这个方法只递归第一个元素足够定位字段路径不会刷屏。参数上obj传推荐接口返回的user对象即可。从那以后我遇到字段对不上第一反应不是改代码而是先跑一次walk_keys。5.4 已入库的数据想重跑一遍原始响应没存现象入库后第二天发现画像抽取规则有 bug想重新解析但库里只有解析结果没有原始 JSON等于数据作废。原因只存了加工后的profiles表忽略了recommendations接口的完整响应。接口会分页返回原始响应是最接近真相的素材丢了只能重新请求。解决所有接口响应按日期落盘成 JSON 文件路径按日期分目录比如raw/2025-01-12/recs_001.json。重跑解析时直接读本地文件不碰线上接口。参数上文件名里带上时间戳和页号方便回放时保持顺序。6. 画像特征与匹配率验证用最少样本做 A/B 对照6.1 用一组 SQL 把匹配率按画像分组算出来数据入库之后最直观的验证是哪一类画像的匹配率更高我用 likes 表做分母、matches 表做分子按画像特征分组聚合SELECT CASE WHEN p.bio LIKE %travel% THEN travel WHEN p.bio LIKE %coffee% OR p.bio LIKE %food% THEN coffee_food ELSE other END AS group_key, COUNT(*) AS total, SUM(CASE WHEN m.match_id IS NOT NULL THEN 1 ELSE 0 END) AS matched, ROUND(SUM(CASE WHEN m.match_id IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) AS match_rate FROM likes l LEFT JOIN matches m ON l.person_id m.person_id LEFT JOIN profiles p ON l.person_id p.user_id GROUP BY group_key ORDER BY match_rate DESC;逻辑说明likes是我们主动执行过的滑动记录matches是服务端返回的匹配记录通过person_id关联。注意这里LEFT JOIN matches的含义是“这条 like 是否产生了 match”没有匹配就是NULL。参数说明COUNT(*)是分母样本量分组前最好先看下总量样本太少时匹配率没有任何参考意义。6.2 样本量不足时怎么判断匹配率是二项分布样本量太小误差极大。我自己的判读标准如下样本量结论可信度建议小于 15不具统计意义只当个案记录15 到 30可观察趋势继续采集别下结论大于 50可做粗略排序结合画像特征做验证匹配率这个指标本身也有口径问题同一个 like 可能在多次分页里被重复记录入库时如果没去重分子分母都会被虚增所以建表时person_id action_time要建唯一索引。后来我还发现真正让这套工具可用的是在 likes 表里多记了source_op和action_time两列统计时能区分“主动筛选”和“无脑刷”。从那以后我每次调解析规则都会先跑一遍本地 JSON确认字段路径没变化再碰真实接口这个顺序看起来慢实际省下了不少因为字段迁移导致的重跑时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表