"""登录态相关的站点常量 与抓取用的 `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