add cookie session persistence

This commit is contained in:
qananasikq
2026-04-10 19:55:38 +03:00
parent b1aeb966ac
commit 05b83b5518
5 changed files with 71 additions and 1 deletions

5
.env
View File

@@ -40,6 +40,11 @@ IAAI_RETRY_DELAY_SECONDS=2.5
IAAI_RETRY_BACKOFF_MULTIPLIER=2.0
IAAI_RETRY_JITTER_SECONDS=0.25
# Session persistence (cookies)
IAAI_STORAGE_STATE_PATH=storage_state.json
IAAI_SESSION_MAX_AGE_DAYS=30
IAAI_SESSION_SAVE_ON_EXIT=true
# Proxy (HTTP/HTTPS preferred for Playwright)
# Chromium does not support SOCKS5 proxy authentication directly.
# Use residential or mobile USA proxy.

View File

@@ -40,6 +40,12 @@ IAAI_RETRY_DELAY_SECONDS=2.5
IAAI_RETRY_BACKOFF_MULTIPLIER=2.0
IAAI_RETRY_JITTER_SECONDS=0.25
# Session persistence (cookies + localStorage)
# Keeps browser session alive between runs (up to 30 days).
IAAI_STORAGE_STATE_PATH=storage_state.json
IAAI_SESSION_MAX_AGE_DAYS=30
IAAI_SESSION_SAVE_ON_EXIT=true
# Proxy (HTTP/HTTPS preferred for Playwright)
# Chromium does not support SOCKS5 proxy authentication directly.
# Use residential or mobile USA proxy.

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ venv/
*.pyd
*.log
*.db
storage_state.json
.vscode/
*.egg-info/
dist/

View File

@@ -167,6 +167,19 @@ class ProxyConfig:
return result
# --- Конфиг сессии браузера (куки, storage_state) ---
@dataclass(slots=True)
class SessionConfig:
storage_state_path: str | None = _env_optional_str("IAAI_STORAGE_STATE_PATH")
max_age_days: int = _env_int("IAAI_SESSION_MAX_AGE_DAYS", 30)
save_on_exit: bool = _env_bool("IAAI_SESSION_SAVE_ON_EXIT", True)
@property
def enabled(self) -> bool:
return bool(self.storage_state_path)
# --- Главный объект настроек: собирает все блоки конфигурации ---
@dataclass(slots=True)
@@ -195,6 +208,7 @@ class Settings:
redis: RedisConfig = field(default_factory=RedisConfig)
celery: CeleryConfig = field(default_factory=CeleryConfig)
proxy: ProxyConfig = field(default_factory=ProxyConfig)
session: SessionConfig = field(default_factory=SessionConfig)
# Глобальный синглтон — используется по умолчанию во всех модулях.
settings = Settings()

View File

@@ -1,8 +1,12 @@
import json
import logging
import os
import re
import signal
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from playwright.sync_api import Error as PlaywrightError
from playwright.sync_api import BrowserContext, Page, sync_playwright
@@ -86,6 +90,7 @@ class IAAIScraper:
def close(self) -> None:
if self.context is not None:
try:
self._save_storage_state()
self.context.close()
except PlaywrightError:
pass
@@ -106,12 +111,51 @@ class IAAIScraper:
finally:
self.playwright = None
def _load_storage_state(self) -> str | None:
"""Load saved browser session (cookies + localStorage) if valid."""
session_cfg = self.settings.session
if not session_cfg.enabled:
return None
state_path = session_cfg.storage_state_path
if not state_path or not os.path.isfile(state_path):
logger.info("No saved session found at %s", state_path)
return None
try:
mtime = os.path.getmtime(state_path)
age_days = (time.time() - mtime) / 86400
if age_days > session_cfg.max_age_days:
logger.info("Session expired (%.1f days old, max %d). Starting fresh.", age_days, session_cfg.max_age_days)
os.remove(state_path)
return None
logger.info("Reusing saved session from %s (%.1f days old)", state_path, age_days)
return state_path
except Exception as exc:
logger.warning("Failed to load session state: %s", exc)
return None
def _save_storage_state(self) -> None:
"""Save browser session (cookies + localStorage) to disk."""
session_cfg = self.settings.session
if not session_cfg.enabled or not session_cfg.save_on_exit:
return
if not self.context:
return
state_path = session_cfg.storage_state_path
try:
Path(state_path).parent.mkdir(parents=True, exist_ok=True)
self.context.storage_state(path=state_path)
logger.info("Session state saved to %s", state_path)
except Exception as exc:
logger.warning("Failed to save session state: %s", exc)
def _new_context(self, storage_state: str | None = None) -> BrowserContext:
if self.browser is None:
self.__enter__()
if self.context:
self._save_storage_state()
self.context.close()
self.context = self.browser_factory.create_context(self.browser, storage_state=storage_state)
effective_state = storage_state or self._load_storage_state()
self.context = self.browser_factory.create_context(self.browser, storage_state=effective_state)
return self.context
def init_db(self):