拆分抓取与交易服务
把需要账号登录态的链路从抓取服务里拆出成独立进程。分界线不是「要不要登录」, 而是抓取无状态、幂等、可多开实例,而交易的写操作不可逆、登录态全局唯一、 订单监控是常驻轮询——同进程时抓取一扩容就会复制出 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:
+69
-7
@@ -7,19 +7,20 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.errors import InvalidRequestError, ScrapeParseError
|
||||
from app.models.scrape import (
|
||||
from app.shared.errors import InvalidRequestError, ItemNotFoundError, ScrapeParseError
|
||||
from app.scraping.models.scrape import (
|
||||
RakumaAuthenticity,
|
||||
RakumaCondition,
|
||||
RakumaSearchRequest,
|
||||
RakumaSortOption,
|
||||
RakumaTransaction,
|
||||
)
|
||||
from app.parsers.rakuma.base import parse_int, parse_total_count
|
||||
from app.parsers.rakuma.item import parse_item_detail
|
||||
from app.parsers.rakuma.search import parse_search
|
||||
from app.parsers.rakuma.shop import parse_shop_detail, parse_shop_items
|
||||
from app.utils.rakuma_urls import (
|
||||
from app.scraping.parsers.rakuma.base import parse_int, parse_total_count
|
||||
from app.scraping.parsers.rakuma.category import parse_categories
|
||||
from app.scraping.parsers.rakuma.item import parse_item_detail
|
||||
from app.scraping.parsers.rakuma.search import parse_search
|
||||
from app.scraping.parsers.rakuma.shop import parse_shop_detail, parse_shop_items
|
||||
from app.scraping.utils.rakuma_urls import (
|
||||
build_item_url,
|
||||
build_search_url,
|
||||
build_shop_url,
|
||||
@@ -269,6 +270,67 @@ def test_parse_shop_pages_reject_non_shop_page():
|
||||
parse_shop_items("<html><body>x</body></html>", shop_id="S", request_url="u", page=1)
|
||||
|
||||
|
||||
# ---- 分类树解析 ----
|
||||
|
||||
def test_parse_categories_returns_top_level_by_default():
|
||||
data = parse_categories(
|
||||
fixture("rakuma_category.html"), category_id=None, include_descendants=False
|
||||
)
|
||||
assert data.category_id == ""
|
||||
assert len(data.children) == 14 # 站点顶层分类固定 14 个
|
||||
top = data.children[0]
|
||||
assert top.category_id == "10001"
|
||||
assert top.name == "レディース"
|
||||
assert top.parent_id == "0"
|
||||
assert top.is_leaf is False
|
||||
assert top.url == "https://fril.jp/category/10001"
|
||||
assert top.children == [] # 未要求子树时不展开
|
||||
|
||||
|
||||
def test_parse_categories_reads_ancestors_and_children():
|
||||
data = parse_categories(
|
||||
fixture("rakuma_category.html"), category_id="786", include_descendants=False
|
||||
)
|
||||
assert data.name == "ゲームソフト/ゲーム機本体"
|
||||
assert [node.category_id for node in data.ancestors] == ["10007"]
|
||||
assert data.full_name == "エンタメ/ホビー / ゲームソフト/ゲーム機本体"
|
||||
assert [node.category_id for node in data.children] == ["787", "788", "789"]
|
||||
assert all(node.is_leaf for node in data.children)
|
||||
assert data.is_leaf is False
|
||||
|
||||
|
||||
def test_parse_categories_marks_leaf_without_children():
|
||||
data = parse_categories(
|
||||
fixture("rakuma_category.html"), category_id="788", include_descendants=False
|
||||
)
|
||||
assert data.is_leaf is True
|
||||
assert data.children == []
|
||||
assert data.full_name == "エンタメ/ホビー / ゲームソフト/ゲーム機本体 / 家庭用ゲームソフト"
|
||||
|
||||
|
||||
def test_parse_categories_can_expand_full_subtree():
|
||||
"""整棵树本来就在一次响应里,展开子树不需要多打请求"""
|
||||
data = parse_categories(
|
||||
fixture("rakuma_category.html"), category_id="10007", include_descendants=True
|
||||
)
|
||||
branch = data.children[0]
|
||||
assert branch.category_id == "786"
|
||||
assert [node.category_id for node in branch.children] == ["787", "788", "789"]
|
||||
|
||||
|
||||
def test_parse_categories_rejects_unknown_category():
|
||||
"""无效分类要报 404,而不是当成「没有子分类」返回空结果"""
|
||||
with pytest.raises(ItemNotFoundError):
|
||||
parse_categories(
|
||||
fixture("rakuma_category.html"), category_id="99999999", include_descendants=False
|
||||
)
|
||||
|
||||
|
||||
def test_parse_categories_rejects_page_without_tree():
|
||||
with pytest.raises(ScrapeParseError):
|
||||
parse_categories("<html><body>home</body></html>", category_id=None, include_descendants=False)
|
||||
|
||||
|
||||
# ---- 取值工具 ----
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Reference in New Issue
Block a user