# Rakuten Scraper Service 面向乐天集团两个购物站点的 HTTP API 服务: | 站点 | 域名 | 形态 | | --- | --- | --- | | **乐天市场**(楽天市場) | `rakuten.co.jp` | B2C 商城:店铺 × 商品 × SKU | | **ラクマ**(Rakuma) | `fril.jp` | C2C 二手集市:个人卖家 × 单件商品 | 两站均支持**搜索**、**商品详情**、**商家信息**与**商家名下商品**。 ## 三个部署单元 同一个仓库出**三个服务**,分进程运行: | | 抓取服务 `app.scraping` | 交易服务 `app.trading` | 下单任务网关 `app.gateway` | | --- | --- | --- | --- | | 启动 | `python -m app.scraping.main`(:31107) | `python -m app.trading.main`(:31108) | `python -m app.gateway.main`(:31109) | | 部署位置 | 服务器 | 本地(NAT 后) | 服务器 | | 账号 | 全程匿名 | 必须带登录态 cookie | 不接触账号 | | 状态 | 无状态,请求-响应 | 有状态:订单、页面证据、付款进度 | 有状态:任务队列 + 状态镜像 | | 失败重试 | 幂等,重试无代价 | **不可逆**,重复提交即重复下单 | 任务编排侧,**不自动重投** | | 实例数 | 想开几个开几个 | **只能一个**(或按账号分片) | **只能一个**(SQLite + 全局并发度 1) | | 出口 IP | 被限速换掉即可 | 频繁漂移会触发风控 | 不出站到站点 | 抓取与交易拆开的决定性理由是「实例数」那一行,而不是「要不要登录」:登录态 cookie 全局唯一、订单监控是常驻轮询,一旦与抓取同进程,抓取横向扩容就会把登录态和轮询任务 复制 N 份,让同一个账号被多个进程并发操作。 网关单独成第三个部署单元,是因为它**有状态**(任务队列),不能塞进可多开的抓取服务, 也不能塞进在本地、零入站端口的交易服务——本地通过出站长轮询从这里领任务。 完整规格见 [docs/order-gateway.md](docs/order-gateway.md)。 依赖方向固定为 `scraping → shared`、`trading → shared`、`gateway → shared`,三方互不 import (`tests/test_architecture.py` 会守着)。交易侧需要商品信息时走抓取服务的 HTTP 接口, 需要任务调度时走网关的 HTTP 接口——下单要用的 `purchase` 块本来就是抓取服务的对外契约。 抓取服务部署在服务器,交易服务部署在本地(便于管理账号、排查支付问题),本地在 NAT 后 没有公网入口,因此下单请求不是推进来的,而是由本地长轮询主动领取。 任务网关与本地 worker 的规格见 [docs/order-gateway.md](docs/order-gateway.md)。 ``` app/ shared/ 配置、错误码、日志、响应信封与鉴权、任务/订单状态枚举(三方共用,不认识任何一侧) scraping/ 站点常量 / 会话 / 解析器 / 抓取路由(本 README 的绝大部分) trading/ 登录态、加购下单付款(在建)与本地下单 worker(领任务、留证据) gateway/ 下单任务队列 + 状态镜像 + 长轮询领取接口 ``` ## 抓取原理 两站的页面形态与防护完全不同,因此各走一条独立链路。 ### 乐天市场:内联 JSON + Akamai 搜索页与(手机版)商品详情页都把整页数据以 JSON 形式内联在 `window.__INITIAL_STATE__` 里, 因此不需要解析 DOM,直接取这段 JSON 即可拿到完整结构化数据。 站点前置 **Akamai Bot Manager**,**不需要真浏览器交互**: | 请求方式 | 实测耗时 | | --- | --- | | 请求头不完整、无 cookie | ~11s(与响应体大小无关,是限速而非封禁) | | 完整浏览器请求头 + 复用 Akamai cookie | ~0.6–0.9s | 所以主链路是纯 httpx,Playwright 仅作为被拦截时的兜底(取 cookie 回灌后重试)。 服务按「指纹画像」维护两条独立通道,各自持有独立 cookie 罐: - **PC 通道** → 搜索页 `search.rakuten.co.jp`、店铺页 `www.rakuten.co.jp` - **手机通道** → 商品详情页 `item.rakuten.co.jp` (详情页只有手机 UA 才返回带 `__INITIAL_STATE__` 的统一模板;PC UA 返回的是各店铺自定义的 EUC-JP 老页面) ### ラクマ:服务端渲染 HTML,无限速 ラクマ 是 Rails 服务端渲染的传统 HTML,**没有** `__INITIAL_STATE__` 这类内联状态, 只能解析 DOM。好在页面上挂了成套的埋点属性,比可见文案稳定得多,也带有 DOM 上 没有的字段(商品数值 ID、卖家 ID、分类/品牌 ID),解析优先取这些: - `data-gtm-click` / `onclick` 里的 dataLayer JSON — 商品卡片的结构化字段 - 页面级 `data-rat-cp-*` 属性 — 成色、运费负担、发货地(已售出商品的规格表会消失,靠它兜底) - `