add cookie session persistence
This commit is contained in:
5
.env
5
.env
@@ -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.
|
||||||
|
|||||||
@@ -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
1
.gitignore
vendored
@@ -9,6 +9,7 @@ venv/
|
|||||||
*.pyd
|
*.pyd
|
||||||
*.log
|
*.log
|
||||||
*.db
|
*.db
|
||||||
|
storage_state.json
|
||||||
.vscode/
|
.vscode/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
dist/
|
dist/
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user