把需要账号登录态的链路从抓取服务里拆出成独立进程。分界线不是「要不要登录」, 而是抓取无状态、幂等、可多开实例,而交易的写操作不可逆、登录态全局唯一、 订单监控是常驻轮询——同进程时抓取一扩容就会复制出 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>
116 lines
4.5 KiB
Python
116 lines
4.5 KiB
Python
"""登录态相关的站点常量
|
|
|
|
与抓取用的 `scraping/core/site.py` / `rakuma_site.py` 分开:那两个模块描述的是
|
|
「匿名抓取怎么拿到页面」,这里描述的是「怎么判断这套 cookie 还登录着」以及登录
|
|
入口在哪。
|
|
|
|
登录态探针的选取原则:挑一个**未登录时行为明确可辨**的页面,而不是靠首页上有没有
|
|
用户名这类会随改版漂移的文案。两站各自的信号(均为实测):
|
|
|
|
- 乐天:手机版购物车页始终返回 200,未登录时正文含「現在ログインしていません」,
|
|
登录后该串消失。购物车页同时是加购结果的校验页,一页两用。
|
|
- ラクマ:`/mypage` 未登录时 302 到 `/users/sign_in`,登录后停在 `/mypage`。
|
|
用落地 URL 判断比翻文案稳。
|
|
|
|
每站的这组事实收在 `PROFILES` 里,`scripts/login.py`(起浏览器人工登录)与
|
|
`AuthSession`(在 httpx 里探测)共用同一份,避免两处各写一遍判据后悄悄漂移。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Final
|
|
|
|
from app.shared import headers
|
|
|
|
# ---- 乐天市场 ----
|
|
# 手机版购物车。加购走的是 sp.basket 集群,校验也用手机版,保持同一套指纹。
|
|
RAKUTEN_CART_URL: Final = "https://sp.cart.step.rakuten.co.jp/cart"
|
|
|
|
# 登录入口。人工登录时打开这个地址,站点会在登录成功后跳回 my.rakuten.co.jp。
|
|
RAKUTEN_LOGIN_URL: Final = "https://www.rakuten.co.jp/myrakuten/"
|
|
|
|
# 购物车页上表示「当前会话未登录」的文案。出现即判定登录态失效。
|
|
RAKUTEN_LOGGED_OUT_MARKER: Final = "現在ログインしていません"
|
|
|
|
# ---- ラクマ ----
|
|
RAKUMA_MYPAGE_URL: Final = "https://fril.jp/mypage"
|
|
RAKUMA_LOGIN_URL: Final = "https://fril.jp/users/sign_in"
|
|
|
|
# 未登录时会被重定向到的登录页路径特征
|
|
RAKUMA_SIGN_IN_PATH: Final = "/users/sign_in"
|
|
|
|
# ---- 登录态请求指纹 ----
|
|
# 这两个 UA 必须与 scripts/login.py 起浏览器时用的一致:cookie 是在那个 UA 下拿到的,
|
|
# 服务端再拿它发请求时换了 UA,可能触发站点的设备校验让登录态提前失效。
|
|
#
|
|
# 值与抓取侧当前相同,但**刻意不复用**抓取侧的常量:抓取 UA 是为绕反爬服务的,
|
|
# 随时可能为了成功率被调整,那种调整不该波及已落盘的账号 cookie。
|
|
RAKUTEN_USER_AGENT: Final = (
|
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 "
|
|
"(KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
|
|
)
|
|
RAKUMA_USER_AGENT: Final = (
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SiteAuthProfile:
|
|
"""一个站点的登录态配置:登录入口、探针、指纹与落盘文件名"""
|
|
|
|
name: str
|
|
label: str
|
|
login_url: str
|
|
probe_url: str # 判断登录态是否仍有效的页面
|
|
user_agent: str
|
|
mobile: bool
|
|
state_filename: str # storage_state 落盘文件名(放在 settings.auth_state_dir 下)
|
|
|
|
def headers(self) -> dict[str, str]:
|
|
"""该站登录态请求用的完整导航请求头"""
|
|
return headers.navigation_headers(self.user_agent, mobile=self.mobile)
|
|
|
|
|
|
PROFILES: Final[dict[str, SiteAuthProfile]] = {
|
|
"rakuten": SiteAuthProfile(
|
|
name="rakuten",
|
|
label="楽天市場",
|
|
login_url=RAKUTEN_LOGIN_URL,
|
|
probe_url=RAKUTEN_CART_URL,
|
|
user_agent=RAKUTEN_USER_AGENT,
|
|
mobile=True,
|
|
state_filename="rakuten_state.json",
|
|
),
|
|
"rakuma": SiteAuthProfile(
|
|
name="rakuma",
|
|
label="ラクマ",
|
|
login_url=RAKUMA_LOGIN_URL,
|
|
probe_url=RAKUMA_MYPAGE_URL,
|
|
user_agent=RAKUMA_USER_AGENT,
|
|
mobile=False,
|
|
state_filename="rakuma_state.json",
|
|
),
|
|
}
|
|
|
|
SITES: Final = tuple(PROFILES)
|
|
|
|
|
|
def profile(site: str) -> SiteAuthProfile:
|
|
"""取某站的登录态配置,未知站点直接报错"""
|
|
try:
|
|
return PROFILES[site]
|
|
except KeyError:
|
|
raise ValueError(f"未知站点:{site}") from None
|
|
|
|
|
|
def is_logged_in(site: str, *, final_url: str, body: str) -> bool:
|
|
"""按该站判据,从探针页的落地 URL 与正文判断是否仍登录着
|
|
|
|
两处共用:`AuthSession` 传 httpx 响应的 URL 与文本,`scripts/login.py` 传
|
|
浏览器页面的 URL 与内容。判据只写一遍,两条链路不会各判各的。
|
|
"""
|
|
if site == "rakuten":
|
|
return RAKUTEN_LOGGED_OUT_MARKER not in body
|
|
return RAKUMA_SIGN_IN_PATH not in final_url
|