拆分抓取与交易服务
把需要账号登录态的链路从抓取服务里拆出成独立进程。分界线不是「要不要登录」, 而是抓取无状态、幂等、可多开实例,而交易的写操作不可逆、登录态全局唯一、 订单监控是常驻轮询——同进程时抓取一扩容就会复制出 N 份登录态与 N 个轮询, 同一账号会被并发操作。 - app/shared:配置、错误码、日志、ApiResponse 信封 + Bearer 鉴权 + 异常处理器、 导航请求头构造器 - app/scraping:站点常量、会话、解析器与 10 个抓取接口,:31107,可多开 - app/trading:登录态查询/重载与健康检查,:31108,只能单实例 - 依赖方向锁为 scraping→shared、trading→shared,两侧互不 import; tests/test_architecture.py 用 AST 检查 import 并校验两个 app 的路径不串 - 登录态 UA 在 trading 独立持有:与抓取 UA 值相同但变更理由不同,抓取 UA 为绕 反爬可随时调整,登录 UA 一改可能触发设备校验使已落盘 cookie 失效 - scripts/login.py 与 AuthSession 共用 auth_site.PROFILES 与 is_logged_in,判据只写一遍 - 同一镜像两个启动命令,交易容器覆盖 command 并设 RAKUTEN_HEALTH_PORT 同时带上此前未提交的 ラクマ 分类接口与登录态基础设施。 验证:239 个离线用例全绿;两个入口真实启动,/health 与鉴权正常。 未验证:真实探测登录态(当前开发机无外网,对站点的连接全部超时)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Rakuten Scraper Service
|
||||
|
||||
面向乐天集团两个购物站点的抓取 HTTP API 服务:
|
||||
面向乐天集团两个购物站点的 HTTP API 服务:
|
||||
|
||||
| 站点 | 域名 | 形态 |
|
||||
| --- | --- | --- |
|
||||
@@ -9,6 +9,34 @@
|
||||
|
||||
两站均支持**搜索**、**商品详情**、**商家信息**与**商家名下商品**。
|
||||
|
||||
## 两个部署单元
|
||||
|
||||
同一个仓库出**两个服务**,分进程运行:
|
||||
|
||||
| | 抓取服务 `app.scraping` | 交易服务 `app.trading` |
|
||||
| --- | --- | --- |
|
||||
| 启动 | `python -m app.scraping.main`(:31107) | `python -m app.trading.main`(:31108) |
|
||||
| 账号 | 全程匿名 | 必须带登录态 cookie |
|
||||
| 状态 | 无状态,请求-响应 | 有状态:订单、页面证据、付款进度 |
|
||||
| 失败重试 | 幂等,重试无代价 | **不可逆**,重复提交即重复下单 |
|
||||
| 实例数 | 想开几个开几个 | **只能一个**(或按账号分片) |
|
||||
| 出口 IP | 被限速换掉即可 | 频繁漂移会触发风控 |
|
||||
|
||||
拆开的决定性理由是「实例数」那一行,而不是「要不要登录」:登录态 cookie 全局唯一、
|
||||
订单监控是常驻轮询,一旦与抓取同进程,抓取横向扩容就会把登录态和轮询任务复制 N 份,
|
||||
让同一个账号被多个进程并发操作。
|
||||
|
||||
依赖方向固定为 `scraping → shared`、`trading → shared`,两侧互不 import
|
||||
(`tests/test_architecture.py` 会守着)。交易侧需要商品信息时,走抓取服务的 HTTP 接口——
|
||||
下单要用的 `purchase` 块本来就是抓取服务的对外契约。
|
||||
|
||||
```
|
||||
app/
|
||||
shared/ 配置、错误码、日志、响应信封与鉴权(两侧共用,不认识两侧)
|
||||
scraping/ 站点常量 / 会话 / 解析器 / 抓取路由(本 README 的绝大部分)
|
||||
trading/ 登录态、(在建)加购、下单、付款与订单监控
|
||||
```
|
||||
|
||||
## 抓取原理
|
||||
|
||||
两站的页面形态与防护完全不同,因此各走一条独立链路。
|
||||
@@ -63,15 +91,18 @@ PC UA 在搜索页、详情页、店铺页上都能拿到完整模板。因此
|
||||
|
||||
## 接口
|
||||
|
||||
抓取服务(:31107):
|
||||
|
||||
| 接口 | 站点 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET /health` | — | 健康检查,含各通道状态 |
|
||||
| `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/categories` | ラクマ | 分类树 |
|
||||
| `POST /api/rakuma/item_detail` | ラクマ | 商品详情 |
|
||||
| `POST /api/rakuma/shop_detail` | ラクマ | 卖家详情 |
|
||||
| `POST /api/rakuma/shop_items` | ラクマ | 卖家名下商品 |
|
||||
@@ -80,7 +111,16 @@ PC UA 在搜索页、详情页、店铺页上都能拿到完整模板。因此
|
||||
(乐天有 `genre_id` / 成色 / SuperDEAL,ラクマ 有 `category_id` / `brand_id` / 匿名配送 / 鉴定服务),
|
||||
合并会让大半字段对另一站无效。
|
||||
|
||||
启动后可访问 `http://127.0.0.1:31107/docs` 查看完整 OpenAPI 文档。
|
||||
交易服务(:31108):
|
||||
|
||||
| 接口 | 说明 |
|
||||
| --- | --- |
|
||||
| `GET /health` | 健康检查,含两站登录态(只读缓存,不打站点) |
|
||||
| `POST /api/auth/status` | 查询登录态,默认真实探测一次 |
|
||||
| `POST /api/auth/reload` | 人工重新登录后免重启换上新 cookie |
|
||||
|
||||
两个服务共用同一个 Bearer Token,错误码表也是同一份。
|
||||
启动后分别在 `http://127.0.0.1:31107/docs` 与 `:31108/docs` 查看 OpenAPI 文档。
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -94,14 +134,26 @@ uv sync --extra dev --extra browser
|
||||
|
||||
## 启动
|
||||
|
||||
默认监听 `0.0.0.0:31107`。启动前建议先按 `.env.example` 配置 `.env`。
|
||||
启动前建议先按 `.env.example` 配置 `.env`,两个服务共用这一份。
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python.exe -m app.main
|
||||
# 或
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 31107
|
||||
# 抓取服务,默认 0.0.0.0:31107;可多开实例
|
||||
.venv/Scripts/python.exe -m app.scraping.main
|
||||
|
||||
# 交易服务,默认 0.0.0.0:31108;只能起一个实例
|
||||
.venv/Scripts/python.exe -m app.trading.main
|
||||
```
|
||||
|
||||
只需要抓取时不必起交易服务。交易服务启动前要先人工登录一次:
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python.exe scripts/login.py --site all
|
||||
```
|
||||
|
||||
浏览器窗口打开后手动完成登录(账号密码只在浏览器与站点之间传递,脚本不读取),
|
||||
cookie 落在 `.auth/`(已 gitignore,内含可直接冒充账号的凭据,不要提交或外传)。
|
||||
后续重新登录后调 `POST /api/auth/reload` 换上新 cookie,不必重启服务。
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
@@ -357,6 +409,31 @@ uvicorn app.main:app --host 0.0.0.0 --port 31107
|
||||
每页固定 40 条。`total_count` 取自页面埋点里的精确值——页面上可见的「約1,190,000件」
|
||||
是四舍五入后的展示值,不要拿它做分页计算;翻页以 `has_more` 为准。
|
||||
|
||||
### 分类(ラクマ)
|
||||
|
||||
`POST /api/rakuma/categories`
|
||||
|
||||
```json
|
||||
{ "category_id": "10007", "include_descendants": true }
|
||||
```
|
||||
|
||||
不传 `category_id` 返回 14 个顶层分类;传入后返回该分类的名称、`full_name` 路径名、
|
||||
`ancestors` 祖先链与 `children` 直接子分类。分类共三层(14 顶层 / 169 二级 / 1503 三级),
|
||||
拿到的 `category_id` 可直接用于 `/api/rakuma/search`。
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `category_id` | string | 目标分类;不传取顶层列表。无效 ID 返回 404 |
|
||||
| `include_descendants` | bool | `children` 里带完整子树而非只有直接子级,默认 `false` |
|
||||
|
||||
站点的分类一览页一次就把整棵树写进页面(与 URL 上的 `category_id` 无关),
|
||||
因此不论查哪一层、要不要子树,服务端都只打一次请求——不像乐天 `/api/genres` 需要逐层下钻。
|
||||
响应里的 `total_count` 是站点分类树的节点总数(当前 1686),可用于确认取到的是全量树。
|
||||
|
||||
> **本接口不返回商品数。** 站点的分类数据里没有这一项,只有 `/category/{id}` 列表页的埋点上有,
|
||||
> 要逐个分类多打一次请求,成本与收益不匹配。需要某分类的商品数时,用该 `category_id`
|
||||
> 调一次 `/api/rakuma/search` 取 `total_count`。
|
||||
|
||||
### 商品详情(ラクマ)
|
||||
|
||||
`POST /api/rakuma/item_detail`
|
||||
@@ -407,7 +484,7 @@ uvicorn app.main:app --host 0.0.0.0 --port 31107
|
||||
| --- | --- | --- |
|
||||
| 商品标识 | `shop_code` + `item_code` 两段 | `item_id` 单个 hash |
|
||||
| 商家标识 | `shop_code`(如 `edion`)/ `shop_id`(数值) | `shop_id`(hash) |
|
||||
| 分类 | `genre_id` + `/api/genres` 分类树 | `category_id`(无分类树接口) |
|
||||
| 分类 | `genre_id` + `/api/genres` 分类树(逐层下钻,带商品数) | `category_id` + `/api/rakuma/categories` 分类树(一次取全,无商品数) |
|
||||
| 品牌 | — | `brand_id` |
|
||||
| 成色 | 3 档(`new`/`used`/`rental`) | 6 档卖家申告 |
|
||||
| 规格 | `sku.axis` + `sku.variants` | 无(单件商品,仅一个 `size` 字段) |
|
||||
@@ -419,7 +496,9 @@ uvicorn app.main:app --host 0.0.0.0 --port 31107
|
||||
## 加购与下单(仅乐天市场)
|
||||
|
||||
详情响应里的 `purchase` 块给出构造「加入购物车」请求所需的标识。
|
||||
**本服务只提供数据,不执行加购**——加购需要已登录的乐天账号会话,由上游采购流程持有。
|
||||
**抓取服务只提供数据,不执行加购**——加购需要账号登录态,属于交易服务
|
||||
(`app/trading/`,尚在建设中)。这个 `purchase` 块正是两个服务之间的契约:
|
||||
交易服务调抓取服务的 `/api/item_detail` 取它,而不是直接 import 解析器。
|
||||
|
||||
```jsonc
|
||||
"purchase": {
|
||||
@@ -470,16 +549,24 @@ uvicorn app.main:app --host 0.0.0.0 --port 31107
|
||||
| 4001 | 页面解析失败(ラクマ 传了站点不认的筛选取值时也归此类) | 400 |
|
||||
| 4002 | 商品页跳转至未登记的乐天子站 | 400 |
|
||||
| 4004 | 商品/店铺不存在或已下架 | 404 |
|
||||
| 5001 | 账号未登录或登录态失效(交易服务) | 401 |
|
||||
| 5002 | 加购失败(交易服务) | 400 |
|
||||
| 5003 | 下单失败(交易服务) | 400 |
|
||||
| 5004 | 下单安全闸门未通过:未显式确认或金额超上限(交易服务) | 400 |
|
||||
|
||||
错误码在两站间通用。ラクマ 链路不会出现 `3002`(无反爬拦截行为)与 `4002`(无子站跳转)。
|
||||
错误码在两站、两个服务之间通用。ラクマ 链路不会出现 `3002`(无反爬拦截行为)
|
||||
与 `4002`(无子站跳转);`5xxx` 只会来自交易服务——抓取服务全程匿名,不会有登录态问题。
|
||||
`5001` 与 `5004` 都标记为不可重试:前者要人工重新登录,后者要调用方改入参。
|
||||
|
||||
## 常用环境变量
|
||||
|
||||
完整列表见 [.env.example](.env.example)。配置项前缀统一为 `RAKUTEN_`,两站共用同一套抓取参数
|
||||
(并发数、超时、重试次数);`SESSION_TTL` 与浏览器兜底只对乐天链路生效。
|
||||
完整列表见 [.env.example](.env.example)。配置项前缀统一为 `RAKUTEN_`,两个服务共用同一份
|
||||
配置文件、各读各的那部分;两站共用同一套抓取参数(并发数、超时、重试次数),
|
||||
`SESSION_TTL` 与浏览器兜底只对乐天链路生效。
|
||||
|
||||
- 服务:`RAKUTEN_APP_HOST`、`RAKUTEN_APP_PORT`(默认 `31107`)、`RAKUTEN_APP_ENV`
|
||||
- 鉴权:`RAKUTEN_BEARER_TOKEN`
|
||||
- 抓取服务:`RAKUTEN_APP_HOST`、`RAKUTEN_APP_PORT`(默认 `31107`)、`RAKUTEN_APP_ENV`
|
||||
- 交易服务:`RAKUTEN_TRADING_HOST`、`RAKUTEN_TRADING_PORT`(默认 `31108`)、`RAKUTEN_AUTH_STATE_DIR`(默认 `.auth`)、`RAKUTEN_ORDER_MAX_TOTAL_YEN`(默认 `30000`)
|
||||
- 鉴权:`RAKUTEN_BEARER_TOKEN`(两个服务共用)
|
||||
- 抓取:`RAKUTEN_MAX_SITE_CONCURRENCY`(默认 `8`,两站各自独立计数)、`RAKUTEN_HTTP_MAX_ATTEMPTS`(默认 `3`)、`RAKUTEN_SESSION_TTL_SECONDS`(默认 `1800`,仅乐天)
|
||||
- 浏览器兜底(仅乐天):`RAKUTEN_BROWSER_FALLBACK_ENABLED`、`RAKUTEN_BROWSER_HEADLESS`、`RAKUTEN_BROWSER_CHANNEL`
|
||||
- 代理(需日本 IP 时):`RAKUTEN_PROXY_SERVER`、`RAKUTEN_PROXY_USERNAME`、`RAKUTEN_PROXY_PASSWORD`
|
||||
|
||||
Reference in New Issue
Block a user