Исправлено: - engine: _calculate_score() при ошибке загрузки (score не 100 для мёртвых сайтов) - privacy_link: замена page.goto() на aiohttp (гонка данных) - form_checkbox: 3-уровневый поиск label + dark-pattern detection - server_location: синхронный urllib → aiohttp - api/server: перепутанные request/response модели в batch - main.py: csv --checks флаг - engine: фазовая модель (cookie_audit отдельно, остальные параллельно) - engine: retry-логика (3 попытки, exponential backoff) - PDF: emoji заменены на текст (WeasyPrint) Добавлено: - consent_doc: проверка отдельного согласия на обработку ПД - operator_info: проверка ИНН/ОГРН/DPO (читает главную + политику) - cookie_audit: 4-фазный аудит (load→detect→reject→compare) - llm_policy: опциональный LLM-анализ 16 разделов (--deep) - external_scripts: YAML-словари trackers.yaml (critical/warning/russian) - logging (output.py), .gitignore, pyproject.toml - smoke-тесты (16/16)
47 lines
1.4 KiB
Python
47 lines
1.4 KiB
Python
"""
|
|
Вывод результатов: консоль + файл.
|
|
Прозрачная замена print() — всё что идёт в out(), дублируется в лог-файл.
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
|
|
class Output:
|
|
"""Дуплексный вывод: консоль + лог-файл."""
|
|
|
|
def __init__(self, log_dir: str | None = None):
|
|
if log_dir is None:
|
|
log_dir = Path(__file__).parent.parent / "logs"
|
|
Path(log_dir).mkdir(parents=True, exist_ok=True)
|
|
|
|
self._log_path = Path(log_dir) / f"scan_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
|
self._file = open(self._log_path, "w", encoding="utf-8")
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
"""out('текст') — вывести на консоль и в лог."""
|
|
sep = kwargs.get("sep", " ")
|
|
end = kwargs.get("end", "\n")
|
|
text = sep.join(str(a) for a in args) + end
|
|
|
|
# Консоль
|
|
sys.stdout.write(text)
|
|
sys.stdout.flush()
|
|
|
|
# Файл (без emoji для читаемости)
|
|
clean = text.encode("ascii", errors="replace").decode("ascii")
|
|
self._file.write(clean)
|
|
self._file.flush()
|
|
|
|
def close(self):
|
|
self._file.close()
|
|
|
|
@property
|
|
def log_path(self) -> Path:
|
|
return self._log_path
|
|
|
|
|
|
# Глобальный экземпляр
|
|
out = Output()
|