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_BACKOFF_MULTIPLIER=2.0
IAAI_RETRY_JITTER_SECONDS=0.25 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) # Proxy (HTTP/HTTPS preferred for Playwright)
# Chromium does not support SOCKS5 proxy authentication directly. # Chromium does not support SOCKS5 proxy authentication directly.
# Use residential or mobile USA proxy. # 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_BACKOFF_MULTIPLIER=2.0
IAAI_RETRY_JITTER_SECONDS=0.25 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) # Proxy (HTTP/HTTPS preferred for Playwright)
# Chromium does not support SOCKS5 proxy authentication directly. # Chromium does not support SOCKS5 proxy authentication directly.
# Use residential or mobile USA proxy. # Use residential or mobile USA proxy.

1
.gitignore vendored
View File

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

View File

@@ -167,6 +167,19 @@ class ProxyConfig:
return result 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) @dataclass(slots=True)
@@ -195,6 +208,7 @@ class Settings:
redis: RedisConfig = field(default_factory=RedisConfig) redis: RedisConfig = field(default_factory=RedisConfig)
celery: CeleryConfig = field(default_factory=CeleryConfig) celery: CeleryConfig = field(default_factory=CeleryConfig)
proxy: ProxyConfig = field(default_factory=ProxyConfig) proxy: ProxyConfig = field(default_factory=ProxyConfig)
session: SessionConfig = field(default_factory=SessionConfig)
# Глобальный синглтон — используется по умолчанию во всех модулях. # Глобальный синглтон — используется по умолчанию во всех модулях.
settings = Settings() settings = Settings()

View File

@@ -1,8 +1,12 @@
import json
import logging import logging
import os
import re import re
import signal import signal
import time import time
import uuid import uuid
from datetime import datetime, timezone
from pathlib import Path
from playwright.sync_api import Error as PlaywrightError from playwright.sync_api import Error as PlaywrightError
from playwright.sync_api import BrowserContext, Page, sync_playwright from playwright.sync_api import BrowserContext, Page, sync_playwright
@@ -86,6 +90,7 @@ class IAAIScraper:
def close(self) -> None: def close(self) -> None:
if self.context is not None: if self.context is not None:
try: try:
self._save_storage_state()
self.context.close() self.context.close()
except PlaywrightError: except PlaywrightError:
pass pass
@@ -106,12 +111,51 @@ class IAAIScraper:
finally: finally:
self.playwright = None 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: def _new_context(self, storage_state: str | None = None) -> BrowserContext:
if self.browser is None: if self.browser is None:
self.__enter__() self.__enter__()
if self.context: if self.context:
self._save_storage_state()
self.context.close() 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 return self.context
def init_db(self): def init_db(self):