masolupo/vinted-realtime-monitor

GitHub: masolupo/vinted-realtime-monitor

通过监控 Vinted 商品 ID 前沿而非搜索索引,实现比公开目录提前数秒检测到新上架商品的实时监控工具。

Stars: 0 | Forks: 0

# Vinted 实时监控器 — 通过商品 ID 读取新商品 ![Python](https://img.shields.io/badge/python-3.10+-blue.svg) ![License](https://img.shields.io/badge/license-MIT-green.svg) ![Status](https://img.shields.io/badge/status-educational-orange.svg) 无需账号,无需购买,无需破解身份验证或第三方机密。核心思路: **监控 ID 前沿,而不是搜索索引** — 外加实现此目标且不被封禁 IP 所需的工程技术。 ![延迟基准测试 — ID 前沿路径比公开目录早中位数 +10.2秒 检测到每件新商品;目录轮询间隔约为 0.74秒,因此这种领先并非轮询造成的假象](https://static.pigsec.cn/wp-content/uploads/repos/cas/7f/7f2fc38d871693bad967e3df0e10d875fe603ad2fb319fb1050fd4fe61e73871.png) ## 核心思路 几乎所有 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, 性能测试, 无后门, 暴力破解, 监控工具, 逆向工具