2026-07-27 10:34:53 +08:00
2026-07-27 10:34:53 +08:00
2026-07-27 10:34:53 +08:00
2026-07-27 10:34:53 +08:00
2026-07-27 10:34:53 +08:00
2026-07-27 10:34:53 +08:00
2026-07-27 10:34:53 +08:00
2026-07-27 10:34:53 +08:00

Rakuten Scraper Service

面向乐天集团两个购物站点的抓取 HTTP API 服务:

站点 域名 形态
乐天市场(楽天市場) rakuten.co.jp B2C 商城:店铺 × 商品 × SKU
ラクマ(Rakuma) fril.jp C2C 二手集市:个人卖家 × 单件商品

两站均支持搜索商品详情商家信息商家名下商品

抓取原理

两站的页面形态与防护完全不同,因此各走一条独立链路。

乐天市场:内联 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-* 属性 — 成色、运费负担、发货地(已售出商品的规格表会消失,靠它兜底)
  • <script type="application/ld+json"> — 商品的 Product、店铺的 Store 微数据(评价数只有这里给)

实测没有 Akamai 那类限速:冷请求(无 cookie、无预热)即 0.5–1.1s,与预热后持平; PC UA 在搜索页、详情页、店铺页上都能拿到完整模板。因此 ラクマ 只有一条通道、 不做 cookie 预热,也不接浏览器兜底。

⚠️ ラクマ 对无法识别的筛选取值不会报错,而是静默返回首页(如 statuses=99 返回 75KB 的首页 HTML)。 本服务的枚举值全部取自站点前端 bundle 里 SearchPanel._url() 的构造逻辑,并逐个实测过; 解析器也会校验页面确实是结果页,识别不出时报 4001 而不是当成「0 条结果」。

功能概览

  • FastAPI 提供 HTTP API
  • Bearer Token 接口鉴权(Authorization: Bearer <token>
  • Akamai cookie 自动预热与复用,过期自动重新预热(仅乐天链路)
  • 失败逐级升级:重新预热 → Playwright 兜底取 cookie → 结构化报错
  • 搜索结果自动剔除混入的 CPC 广告位(乐天)
  • 识别站点深翻页「静默回绕到第 1 页」的行为,避免上游重复入库(乐天)
  • 商品详情按落地域名分派解析,覆盖楽天ブックス / Rakuten Fashion / ビックカメラ 等官方旗舰子站

接口

接口 站点 说明
GET /health 健康检查,含各通道状态
POST /api/search 乐天 商品搜索
POST /api/genres 乐天 分类树
POST /api/item_detail 乐天 商品详情
POST /api/shop_detail 乐天 商家详情
POST /api/shop_items 乐天 商家名下商品
POST /api/rakuma/search ラクマ 商品搜索
POST /api/rakuma/item_detail ラクマ 商品详情
POST /api/rakuma/shop_detail ラクマ 卖家详情
POST /api/rakuma/shop_items ラクマ 卖家名下商品

接口按站点分前缀而非合并加 site 参数,因为两站的筛选体系差异很大 (乐天有 genre_id / 成色 / SuperDEAL,ラクマ 有 category_id / brand_id / 匿名配送 / 鉴定服务), 合并会让大半字段对另一站无效。

启动后可访问 http://127.0.0.1:31107/docs 查看完整 OpenAPI 文档。

安装

uv sync --extra dev

# 可选:启用浏览器兜底(不装也能正常跑,只是失去兜底能力)
uv sync --extra dev --extra browser
.venv/Scripts/python.exe -m playwright install chromium

启动

默认监听 0.0.0.0:31107。启动前建议先按 .env.example 配置 .env

.venv/Scripts/python.exe -m app.main
# 或
uvicorn app.main:app --host 0.0.0.0 --port 31107

测试

.venv/Scripts/python.exe -m pytest -q

单测全部离线运行:解析器用的是 tests/fixtures/ 下从真实页面抓取并裁剪后的状态样本。

请求示例

以下 /api/* 为乐天市场接口,/api/rakuma/* 为 ラクマ 接口。

搜索(乐天)

POST /api/search

三种用法,优先级从高到低:

1. 透传原始搜索 URLpage > 1 时会覆盖 URL 里的页码)

{ "search_url": "https://search.rakuten.co.jp/search/mall/カメラ/?s=3&f=101", "page": 3 }

2. 关键词 + 筛选

{
  "keyword": "nintendo switch",
  "page": 2,
  "sort": "price_asc",
  "min_price": 3000,
  "max_price": 30000,
  "free_shipping": true,
  "condition": "used"
}

3. 仅按分类

{ "genre_id": "565950" }

keywordgenre_idsearch_url 三者至少提供一个。

搜索参数

参数 类型 说明
keyword string 关键词
genre_id string 乐天分类 ID,可与关键词叠加
page int 页码,1–150
sort enum standard(默认)/ price_asc / price_desc / newest / review_count / review_score / price_with_shipping_asc / price_with_shipping_desc
min_price / max_price int 价格区间(日元)
shop_id int 限定店铺,取搜索结果里的 shop.shop_id
condition enum new / used / rental
free_shipping bool 仅免运费
include_sold_out bool 包含售罄商品
has_review bool 仅有评论
next_day_delivery bool 仅次日达
super_deal bool 仅 SuperDEAL
exclude_keyword string 排除词
title_only bool 仅在商品标题中匹配
or_query bool 关键词之间用 OR 而非 AND
min_review_score int 最低评分 1–5
tags string[] 站点标签 ID
exclude_ads bool 剔除 CPC 广告位,默认 true

分页注意事项

站点声明的命中总数(total_count)远大于实际可翻到的条数(reachable_count,随查询条件变化, 实测从 405 到 6750 不等)。超出可达窗口时站点不会返回空列表,而是静默回绕到第 1 页—— 本服务会识别这种情况,把 out_of_range 置为 true 并清空 items,避免上游把重复数据当新数据入库。

翻页时以 has_more 为准即可。

分类(乐天)

POST /api/genres

用于取得 /api/search 需要的 genre_id,逐层下钻即可定位到叶子分类。

顶层分类(39 个):

{}

下钻某个分类

{ "genre_id": "101205" }

返回该分类自身(name / full_name / description / is_leaf)、祖先路径 ancestors (从顶层到父级,不含自身,可直接拼面包屑)以及直接子分类 children

{}              → 39 个顶层分类
101205 テレビゲーム  → ancestors=[]                              children=27
565950 Nintendo Switch → ancestors=[テレビゲーム]                  children=3
566404 ソフト        → ancestors=[テレビゲーム, Nintendo Switch]   children=0, is_leaf=true

两点注意:

  • 子分类的 item_count 是该分类下的商品数;顶层列表不返回该值,因为站点在那一层 给的是「当前查询在该分类下的命中数」,并非分类自身的商品总量。
  • full_namedescription 只有顶层分类页才提供,下级分类为空串。

商品详情(乐天)

POST /api/item_detail

shop_code + item_code(即商品 URL 的两段路径),或直接传 item_url

{ "shop_code": "edion", "item_code": "4902370549263" }
{ "item_url": "https://item.rakuten.co.jp/edion/4902370549263/" }

include_sku_variants 默认 true;SKU 组合可能多达数百条(实测有 180 条的商品), 不需要明细时置为 false,此时仍会保留 sku.axissku.variant_count

搜索结果中的 shop.shop_code + item_code 可直接用作本接口入参。

部分官方店的商品页会跳转到独立子站,响应里的 source / source_url 会标明数据来源, 字段覆盖差异见下一节。

商家(乐天)

商家详情 POST /api/shop_detail

shop_code(店铺首页 URL 的路径段),或直接传 shop_url

{ "shop_code": "edion" }

返回店铺名、简介、评分与评价数、招牌图与 logo、是否 39ショップ、休息日等。

评价数过少时站点不展示评分,此时 review_displayedfalsereview_score 不可信。

商家名下商品 POST /api/shop_items

{ "shop_id": 272415, "page": 2, "keyword": "テレビ", "sort": "price_asc" }

站点没有可分页抓取的「店铺内商品」页——店铺商品实际就是搜索结果按 sid= 限定店铺后的产物, 因此本接口内部转成一次搜索,返回结构与 /api/search 完全一致,也支持在店铺内继续按 关键词、分类、价格、成色筛选。

shop_idshop_code 至少给一个;只给 shop_code 时服务端会先取一次店铺详情换出 shop_id(多一次请求),能直接给 shop_id 时优先给。shop_id 可从搜索结果的 shop.shop_id/api/shop_detail 取得。

官方旗舰子站

部分乐天官方店的商品页会 302 跳出 item.rakuten.co.jp,落到各自独立的站点上。 这些站点技术栈各不相同,服务按落地域名分派到对应解析器,统一收敛成同一份 ItemDetailData;响应里的 source 标明数据来自哪个站点,source_url 是实际解析的地址。

source 店铺代码 落地域名 页面数据格式
ichiba 其余全部 item.rakuten.co.jp __INITIAL_STATE__
books book(楽天ブックス) books.rakuten.co.jp schema.org 微数据 + DOM
brandavenue stylife(Rakuten Fashion) brandavenue.rakuten.co.jp __INITIAL_STATE__(结构与市场页不同)
biccamera biccamera biccamera.rakuten.co.jp __NUXT__

落到未登记的站点时返回错误码 4002OFF_ICHIBA_REDIRECT)并附上跳转目标地址, 便于上游区分「站点不支持」与「被反爬拦截」,而不是白白重试。

各来源字段覆盖差异

子站页面能提供的信息不同,以下字段并非各处都有:

字段 ichiba books brandavenue biccamera
名称 / 价格 / 图片 / 简介
genre_id / breadcrumbs
is_sold_out
review 异步加载 异步加载
sku.variants(规格组合) — 图书无规格 颜色 × 尺码 — 单一规格
sku.attributes(规格表) 出版社/ISBN 等 挂在各 variant 上
shipping 运费明细 仅库存措辞 仅「是否含运费」
shop.shop_id
breadcrumbs[].url 站内分类编码,拼不出链接
purchase(加购标识) 无件数/规格 shop_bid

抽样实测(6 个关键词 × 各 20 条,共 120 条)整体 99% 的商品可正常解析详情 (唯一失败是一条已下架商品,返回 4004):

类目关键词 可解析
switch 20/20
カメラ 20/20
Tシャツ 20/20
20/20
化粧品 20/20
iphone 19/20

来源分布:ichiba 91、books 19、brandavenue 8、biccamera 1。

ラクマ 接口

ラクマ 是 C2C 二手集市,与乐天市场的商业形态不同,字段语义也不同,因此模型不与市场侧合并

  • 每件商品都是独一无二的一件:没有 SKU 组合、没有库存数量、没有起订量
  • 「商家」就是个人卖家:没有店铺代码,用一串 hash 标识;没有运费门槛、店铺日历这些商城概念
  • 成色由卖家自己申告,分 6 档(市场侧只有新品/中古/租赁 3 档)

搜索(ラクマ)

POST /api/rakuma/search

{
  "keyword": "switch",
  "page": 2,
  "sort": "price_asc",
  "conditions": ["new", "almost_new"],
  "transaction": "on_sale",
  "min_price": 3000,
  "max_price": 20000,
  "free_shipping": true
}

也可以直接透传 search_urlpage > 1 时覆盖 URL 里的页码)。 keywordcategory_idbrand_idsearch_url 四者至少提供一个。

参数 类型 说明
keyword string 关键词
category_id string ラクマ 分类 ID(与乐天的 genre_id 是两套体系)
brand_id string ラクマ 品牌 ID
page int 页码,1–100(站点 page > 100 直接 404)
sort enum standard(默认)/ newest / price_asc / price_desc / like_count
min_price / max_price int 价格区间(日元)
conditions enum[] 成色,可多选:new / almost_new / no_damage / slight_damage / damaged / poor
transaction enum on_sale 仅在售 / sold_out 仅售罄;不传为不限
free_shipping bool 仅「送料込み」(卖家承担运费)
anonymous_shipping bool 仅匿名配送
except_for_no_brand bool 排除无品牌商品;与 brand_id 互斥
authenticity_types enum[] 鉴定服务:before_delivery / after_delivery
exclude_keyword string 排除词,必须与 keyword 同时使用(站点在无关键词时不认该参数)

每页固定 40 条。total_count 取自页面埋点里的精确值——页面上可见的「約1,190,000件」 是四舍五入后的展示值,不要拿它做分页计算;翻页以 has_more 为准。

商品详情(ラクマ)

POST /api/rakuma/item_detail

{ "item_id": "4aca1d6db3e422f3a251a8a8b61e1eff" }

item_id(商品 URL 的最后一段 hash)或 item_url。搜索结果里的 item_id 可直接用作入参。

返回名称、价格、描述、图片、成色、配送信息(负担方/方式/发货日/发货地)、 点赞与评论数,以及 seller 出品者摘要——其中的 seller.shop_id 可直接用于 /api/rakuma/shop_detail

已售出商品的页面会换一套布局,规格表整体消失。 此时服务改从页面级埋点属性取值, 成色、运费负担、发货地仍能拿到,但配送方式与尺码会为空;价格与名称来自 ld+json,不受影响。

卖家(ラクマ)

卖家详情 POST /api/rakuma/shop_detail

{ "shop_id": "422750cb7921557bc8dba2416915d968", "include_reviews": true }

返回店铺名、昵称、头像与封面、简介、评分与评价数、本人确认状态(is_verified)、 以及该卖家的商品总数 item_count

include_reviews 默认 false;置为 true 时会并发多抓一次站点的评价子页, 额外返回最新 100 条评价 reviews,以及好评/普通/差评分档计数:

  • rating_breakdown — 全部评价(出品 + 购入)
  • seller_rating_breakdown — 仅出品方评价

卖家名下商品 POST /api/rakuma/shop_items

{ "shop_id": "422750cb7921557bc8dba2416915d968", "page": 2 }

按页返回该卖家的全部商品(含已售出,用每条的 is_sold_out 区分)。 店铺页每页 36 条(与搜索页的 40 条不同)。

站点在店铺页不提供排序与筛选参数,因此本接口只有页码;需要筛选请改用 /api/rakuma/search

两站字段对照

概念 乐天市场 ラクマ
商品标识 shop_code + item_code 两段 item_id 单个 hash
商家标识 shop_code(如 edion)/ shop_id(数值) shop_id(hash)
分类 genre_id + /api/genres 分类树 category_id(无分类树接口)
品牌 brand_id
成色 3 档(new/used/rental 6 档卖家申告
规格 sku.axis + sku.variants 无(单件商品,仅一个 size 字段)
每页条数 45(搜索) 40(搜索)/ 36(店铺页)
翻页上限 reachable_count,随查询变化(405~6750) 固定 100 页
加购标识 purchase 无(未实现)

加购与下单(仅乐天市场)

详情响应里的 purchase 块给出构造「加入购物车」请求所需的标识。 本服务只提供数据,不执行加购——加购需要已登录的乐天账号会话,由上游采购流程持有。

"purchase": {
  "cart_url": "https://sp.basket.step.rakuten.co.jp/rms/mall/bss/cartadd/set",
  "cart_method": "POST",
  "form_fields": {          // 原样提交的固定字段
    "shop_bid": "231431",
    "item_id": "10008065",
    "inventory_flag": "2",  // 1=单一库存,2=多规格
    "__event": "ES01_003_001"
  },
  "quantity_field": "units",      // 购买件数填这里
  "variant_field": "variant_id",  // 选中的 SKU 填这里
  "options_field": "choice",      // 商品选项填这里
  "has_required_options": true,   // 有必填选项,缺失会被站点拒绝
  "options": [
    { "id": 1, "name": "名入れ", "type": "select", "is_required": true,
      "values": [{ "name": "希望する【次の項目で入力】" }] },
    { "id": 2, "name": "【お名前】", "type": "text", "is_required": false, "values": [] }
  ]
}

拼装规则:

  • form_fields 原样带上,再按 quantity_field 填件数。
  • inventory_flag=1variant_id 已在 form_fields 里预填好;inventory_flag=2必须由调用方从 sku.variants[].variant_id 选一个,填到 variant_field
  • 字段名为空字符串表示该站不支持该项(如 books 不能指定件数与规格)。
  • optionstype=selectvalues 中选,type=text 由买家填写;is_required=true 的项不能省。

几点差异值得留意:

  • cart_url 逐商品不同,不要写死。不同店铺落在不同 basket 集群(实测有 sp.basket…ts.sp.basket…),brandavenue 的端点还要再经一层编号映射。
  • booksitem_id 与 URL 上的商品编号不是同一个值(如 URL 17065211 对应 item_id 20600328)。下单请用 purchase.form_fields.item_iditem_code 只是市场侧编号。
  • biccamera 没有 shop_bid,走自己的 JSON 接口;它的选项取值结构未取到样本验证,因此只用 has_required_options 如实回报「有无选项」而不给出选项定义,这类商品需另行处理。

错误码

code 含义 HTTP
0 成功 200
1001 鉴权失败 401
1002 请求参数校验失败 422
1003 请求参数不合法(如 URL 不属于目标站点) 400
2002 抓取并发槽位等待超时 400
3001 上游网络异常 / 5xx 400
3002 被反爬阻断 400
4001 页面解析失败(ラクマ 传了站点不认的筛选取值时也归此类) 400
4002 商品页跳转至未登记的乐天子站 400
4004 商品/店铺不存在或已下架 404

错误码在两站间通用。ラクマ 链路不会出现 3002(无反爬拦截行为)与 4002(无子站跳转)。

常用环境变量

完整列表见 .env.example。配置项前缀统一为 RAKUTEN_,两站共用同一套抓取参数 (并发数、超时、重试次数);SESSION_TTL 与浏览器兜底只对乐天链路生效。

  • 服务:RAKUTEN_APP_HOSTRAKUTEN_APP_PORT(默认 31107)、RAKUTEN_APP_ENV
  • 鉴权:RAKUTEN_BEARER_TOKEN
  • 抓取:RAKUTEN_MAX_SITE_CONCURRENCY(默认 8,两站各自独立计数)、RAKUTEN_HTTP_MAX_ATTEMPTS(默认 3)、RAKUTEN_SESSION_TTL_SECONDS(默认 1800,仅乐天)
  • 浏览器兜底(仅乐天):RAKUTEN_BROWSER_FALLBACK_ENABLEDRAKUTEN_BROWSER_HEADLESSRAKUTEN_BROWSER_CHANNEL
  • 代理(需日本 IP 时):RAKUTEN_PROXY_SERVERRAKUTEN_PROXY_USERNAMERAKUTEN_PROXY_PASSWORD

从中国大陆直连实测可用、无需代理;RAKUTEN_PROXY_SERVER 留空即可。

S
Description
No description provided
Readme
820 KiB
Languages
Python 99.5%
Dockerfile 0.5%