masolupo/vinted-realtime-monitor
GitHub: masolupo/vinted-realtime-monitor
通过监控 Vinted 商品 ID 前沿而非搜索索引,实现比公开目录提前数秒检测到新上架商品的实时监控工具。
Stars: 0 | Forks: 0
# Vinted 实时监控器 — 通过商品 ID 读取新商品



无需账号,无需购买,无需破解身份验证或第三方机密。核心思路:
**监控 ID 前沿,而不是搜索索引** — 外加实现此目标且不被封禁 IP 所需的工程技术。

## 核心思路
几乎所有 Vinted 监控器都在轮询搜索 endpoint:
```
GET /api/v2/catalog/items?order=newest_first
```
问题在于:**该索引已经存在几秒钟的延迟。** 无论你多快地轮询它,
都无法突破这个下限 — 每个轮询目录的人看到的都是同样滞后的画面。
但 Vinted 会为新品分配**递增的 ID**,并且商品在创建后几秒钟内(远早于它进入搜索索引之前),
就可以通过其 ID 进行访问:
```
GET /items/{id} → 307 redirect (born) | 404 (not yet)
```
307 的重定向目标(`/items/{id}-{slug}`)甚至在 slug 中直接携带了**标题**,
完全免费且无需身份验证:
```
GET /items/9361111097
→ 307 Location: /items/9361111097-maleta-viaje-mano
└──────────┬──────────┘
the title
```
因此,与其轮询一个过时的列表,不如直接监控 **ID 前沿**,并在商品刚诞生时读取其
ID 和标题。
## 工作原理
三个活动组件,各自对应一个文件:
**1. 寻找前沿** (`vinted.py`)。从目录中读取最新的 ID(已知其存在,但有延迟 — 并且
通过添加缓存破坏剂多次读取,以确保严重滞后的 CDN 快照不会误导我们),然后
通过并发的二分搜索 `GET /items/{id}` 来定位当前返回 307 的最高 ID。这就是
实时前沿。
**2. *耐心*追踪** (`monitor.py`)。刚好高于前沿的 ID 仍然是 `404`(尚未诞生)。
我们将每个尚未诞生的 ID 保存在一个 `pending` 集合中,并按一定间隔重新探测,直到
其状态翻转为 `307`(**打印它**) — 或者直到其在 `pending` 状态停留的时间超过 `MAX_AGE`,
此时它就是一个真正的空缺(废弃的草稿/已删除的插槽),我们将其丢弃。
我们惨痛吸取的教训是:ID 出现的时间会比其页面可访问的时间稍微*早*一点,并且
`404→307` 的翻转是**无序发生的**,且带有可变的延迟。如果只是简单地移动指针
并放弃难以处理的 404,就会漏掉那些晚出现的商品;而基于时间的耐心集合可以捕获它们。
**3. 不要被封禁** (`extractor.py`)。Vinted 的反机器人机制会封禁请求量过大的 IP,因此
长生命周期的 session 是一条死路。相反,我们运行一个**自我补充的短生命周期 session 池**
(其理念类似于轮换使用的浏览器标签页):每个 session 在随机使用 100-200 次后退役
并替换;后台循环会保持池子得到补充;并且任何收到 `403/429` 的 session 都会立即退役。
session 通过独占式检出进行分配,因此不会出现某个探测在请求中途被另一个探测关闭的情况。
标题可以从 307 的 slug 中免费获取 — 类别、价格和照片则是你需要额外添加的丰富信息,
而丰富信息(而非检测)才是真正消耗带宽的地方。
## 完整度如何?
在一次测试运行中,它读取了 **~99.9%** 的新商品。该工具会记录它放弃的所有内容
(设置 `GAVEUP_LOG=gaveup.log`),而 `check.py` 会重新探测该列表以进行分类:
```
$ python check.py gaveup.log
=== SUMMARY (audit of ABANDONED ids only — NOT a coverage figure) ===
checked : 102
404 (real holes): 34 — never a listing (draft/deleted), correctly skipped
307 (MISSES) : 10 — real listings that appeared after MAX_AGE
other : 58 — anti-bot 200 pages masking a 404 (a browser shows "not found")
```
请仔细阅读:这些数字**仅仅是监控器放弃的 ID**(约占该次运行读取总量的 ~1.4%),
*而不是*覆盖率百分比。其中,只有 **10** 个是在耐心窗口之后出现的真实商品 — 因此
实际的遗漏率是 `10 ÷ ~7000 读取 ≈ 0.14%`,即 **~99.9% 的覆盖率**。提高 `MAX_AGE` 可以
捕获更多晚出现的商品;其余的则是真正的空缺(从未成为公开商品的 ID)。
## 测量领先时间
领先时间并不是猜测出来的 — `latency.py` 负责测量它。它会在即将诞生的 ID 上设置哨兵,
并同时以*两种*方式监控每一个 ID — 即通过 ID 和通过目录 — 然后打印出每一个的时间差,
以及中位数:
```
$ python latency.py
#9424054263 gap cropped tee xl
by ID : 00:48:03.517
catalog : 00:48:14.975
LEAD : +11.5s
…
=== RESULT ===
sentinels timed by ID (real 404->307) : 7
of those, later seen in catalog : 6
catalog poll : 142 reads, median gap 0.74s → each 'catalog' timestamp is accurate to ~0.74s
lead (catalog − ID) : median +10.2s (min 7.7 / max 11.5)
```
领先时间 ≈ 目录落后于前沿的程度 ÷ 当前的生成速率,因此它**取决于具体时段**:
在高峰期(快速生成时)大约为 **~6-8秒**,而在非高峰期则*更长* — 上面那次在深夜的
运行测得中位数为 +10.2秒。无论哪种情况,在你自己的代理上,ID 路径都能稳定地以秒级优势胜出。
**为什么这是一个公平的比较(而不是轮询造成的假象)。** 目录的延迟是一种*服务端索引
延迟*,不是你通过轮询就能消除的:无论你访问得多快,`catalog/items?newest_first` 总是
落后于真实前沿几秒钟。`latency.py` 依然快速轮询它 — 采用流水线方式,并加上缓存破坏剂
以确保每次读取都是最新的 — 并且它**会测量并打印自己的轮询节奏**
(即上面的 `catalog poll` 行:中位数间隔约 0.74秒,不到领先时间的 10%),因此你可以验证
其粒度,而不是盲目信任。它还会在目录的最新 ID *越过* 哨兵的瞬间算作目录捕获成功 — 这是
最早且对目录最有利的合理时刻 — 因此测量出的领先时间其实还是稍微保守的估计。
## 包含内容
| 文件 | 作用 |
| --- | --- |
| `monitor.py` | 主应用:追踪前沿,实时打印 `#id title`,并打印心跳信息 |
| `vinted.py` | Vinted 交互层:配置、预热 session、`probe()`、前沿搜索 |
| `extractor.py` | 自我补充的短生命周期 session 池(用于防封禁) |
| `check.py` | 诊断工具:重新探测 ID 列表(或 `GAVEUP_LOG`)并分类为 307/404 |
| `latency.py` | 诊断工具:在同一批商品上测量 ID 路径相对于目录的领先时间 |
## 快速开始
**1. 创建虚拟环境并安装依赖**
```
python3 -m venv .venv # Windows: python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
```
**2. 配置代理**(必需 — 见下文说明)
```
cp .env.example .env
```
打开 `.env` 并设置你的代理及匹配的域名:
```
HTTP_PROXY=http://user:pass@host:port # an IP in the same country as the domain
VINTED_DOMAIN=www.vinted.it # the national TLD for that country
```
**3. 运行**
```
python monitor.py # defaults: 200 sessions, 150 concurrency
# 较轻量,适用于小型或缓慢的 proxy(在高峰时段可能会落后):
SESSIONS=100 CONCURRENCY=100 python monitor.py
```
诊断信息和心跳会输出到 **stderr**;纯净的 `#id title` 数据流输出到
**stdout**(这样你就可以通过管道进行传输)。新商品会像这样滚动显示:
```
14:41:07 #9302874961 blouse rose zara taille s
14:41:07 #9302874962 nintendo ds lite complete
14:41:08 #9302874964 nike air max 90 42
```
**必须使用代理。** Vinted 会迅速封锁裸 IP,因此如果不设置 `HTTP_PROXY`,该工具将拒绝
启动 — 这是为了保护你自己的 IP 地址。代理的国家/地区、`VINTED_DOMAIN` 的 TLD(顶级域)
以及请求语言必须完全匹配(例如:意大利代理对应 `www.vinted.it`,法国代理对应
`www.vinted.fr`,…);请避免使用 `www.vinted.com` — 虽然它能用,但经常会提供缓存/过期的数据。
如果你使用**轮换**代理,请将其锁定在单一国家(与 TLD 匹配的国家过滤器) — 在不同
国家之间轮换的资源池会定位到错误的地理位置,导致 Vinted 停止响应。`Accept-Language` 会根据
`VINTED_DOMAIN` 自动推导。
## 心跳信息
每隔几秒钟,该工具就会向 stderr 打印一行状态信息 — 这能让你看出它是否跟上了进度,
而且它永远不会保持沉默(如果某个循环停滞了,它会如实报告):
```
… 8933 read | 104 listings/s | frontier ~#9370629097 | pool 200/200 (+434 -254) | probe ~0.6s med | last 5s: born 518 absent 31 | ERR 16 (200×7, net×6) | pending 335 | gave-up 0
```
- **`pool 200/200`** — 存活 session 与目标数量的对比。如果耗尽,请提高 `SESSIONS` / 降低
`CONCURRENCY`;如果你看到 `no-session` 错误,说明 session 池正在饥饿状态。
- **`probe ~0.6s med`** — 探测延迟的中位数。你的吞吐量即为 `CONCURRENCY ÷ latency`。
- **`gave-up`** — 超过 `MAX_AGE` 后放弃的 ID(主要是空缺;上面的审计显示了其中
有多少是真实的)。这是你的覆盖率指标。
- **`ERR (403×…)`** — 真正的反机器人封禁。如果这些数字上升,请降低 `MAX_USES`。
## 调优
所有配置均通过环境变量进行(括号内为默认值):
| 变量 | 作用 |
| --- | --- |
| `SESSIONS` (200) | session 池的目标大小。保持其大于等于 `CONCURRENCY`。 |
| `CONCURRENCY` (150) | 最大并行探测数。吞吐量 ≈ 该值 ÷ latency。 |
| `PROBE_INTERVAL` (1.0) | 重新探测同一 ID 之间的最小秒数(可减轻代理负担)。 |
| `MAX_AGE` (90) | 放弃某个 ID 之前持续重新探测的时长。 |
| `PROBE_TIMEOUT` (2) | 每次探测的超时时间(约为典型延迟的 3-6 倍)。 |
| `MAX_USES_MIN/MAX` (100/200) | 每个 session 退役前的随机请求配额。如果出现 `403` 则调低;如果池子耗尽则调高。 |
| `CREATE_CONCURRENCY` (40) | 同时预热的 session 数量。 |
| `GAVEUP_LOG` (未设置) | 将放弃的 ID 追加到此文件中,以便使用 `check.py` 审计覆盖率。 |
## 难点 — 检测很简单,规模化是一场基础设施的博弈
本仓库实现的是**检测**:实时获取 ID + 标题,且无需账号。要*完整*且*持续*地做到这一点,
存在两道真实的壁垒,使其成为一个基础设施难题,而不是单靠某个更巧妙的 endpoint 就能解决的:
- **吞吐量。** 在高峰期,新 ID 的全局生成速率最高可达约 140个/秒。要读取*全部*新商品,
你的探测速率必须超过该生成速率 — `每秒探测数 = CONCURRENCY ÷ latency` — 这在住宅代理的
延迟下,意味着需要一个庞大的并发 session 池。
- **session 流失。** 每个 session 在必须退役和替换之前只能存活约 100-200 次请求,
而替换一个 session 需要完成一整套完整的 cookie 预热流程。维持 session 池意味着需要
持续不断地生成新 session;如果代理预热速度不够快,池子就会耗尽,覆盖率也会随之下降。
另一半工作 — **信息丰富化**(描述、价格、尺码、成色、照片、卖家信息)— 位于
Vinted 的**经过身份验证的**单品 endpoint 之后,需要登录账号并且受到严格的速率限制。
因此,“全套功能”(实时检测*加上*每件商品的丰富数据)并不是一个更聪明的技巧:它是
**两套基础设施** — 一个用于检测的代理池,以及一个用于获取深度信息的账号矩阵。
*(大规模运行该操作 — 尤其是批量养殖账号 — 完全违反了 Vinted 的服务条款,并且超出了
本教育性质概念验证的范围。重点在于明确成本究竟体现在哪里,而不是构建一个滥用型版本。)*
## 免责声明
仅用于教育和研究目的。本项目**不附属于、不受认可于,也并未与
Vinted 或任何第三方工具产生关联**。
它使用了 Vinted **自有的、未公开的内部 endpoint**(商品页面路由和内部的
`catalog/items` API) — 这些接口是*公开可访问的*,但它们**不是公开/官方认可的
API**(Vinted 唯一官方的 API 是加入白名单的 Vinted Pro)。它们的行为是通过
**观察**得出的,而不是通过反编译或破解任何身份验证系统获取的。
它通过**自动化 ID 枚举以及浏览器/反机器人模拟**来访问这些接口,
这违背了 Vinted 的服务条款。
其与托管型监控工具运行速度的任何相似之处,都是**基于观察行为的合理推测**,
而非泄露:没有对任何第三方系统进行逆向工程,且本仓库**不包含任何第三方密钥、渠道或凭证**。
这是一个旨在*研究*该技术的概念验证,而不是一个可大规模运行的工具:请勿进行
大规模爬取、滥信息或自动化购买。请负责任地使用,并自行承担风险。
## 许可证
MIT — 详见 [LICENSE](LICENSE)。
标签:API监控, PoC, Python, Vinted, 性能测试, 无后门, 暴力破解, 监控工具, 逆向工具