changes
This commit is contained in:
430
main.py
430
main.py
@@ -1,70 +1,22 @@
|
||||
import json
|
||||
import os
|
||||
import hashlib
|
||||
import threading
|
||||
import pika
|
||||
import requests
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import FileResponse
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3 import Retry
|
||||
|
||||
import uvicorn
|
||||
|
||||
# --- Загрузка переменных окружения ---
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
# -----------------------------------
|
||||
|
||||
# ================= КОНФИГУРАЦИЯ =================
|
||||
# ИЗМЕНЕНИЕ 1: Отказываемся от списка файлов. Грузим монолит.
|
||||
URL_RESULT_V2 = "https://fazziclay.com/api/v1/vstu_schedule_parser_v2/result_v2.json"
|
||||
|
||||
FILE_RESULT = "data/result.json"
|
||||
FILE_CACHE = "data/raw_cache.json"
|
||||
|
||||
GEMINI_MODEL="gemini-flash-lite-latest"
|
||||
|
||||
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", 600))
|
||||
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
||||
PROXY_URL = os.environ.get("PROXY_URL")
|
||||
|
||||
RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST", "172.20.0.10")
|
||||
RABBITMQ_USER = os.environ.get("RABBITMQ_USER")
|
||||
RABBITMQ_PASS = os.environ.get("RABBITMQ_PASS")
|
||||
EXCHANGE_NAME = os.environ.get("EXCHANGE_NAME", "vstu_schedule_parser.schedule_parsed")
|
||||
|
||||
HTTP_PORT = int(os.environ.get("HTTP_PORT", 8080))
|
||||
# =================================================
|
||||
import consts
|
||||
import httpserver
|
||||
from utils import *
|
||||
from consts import *
|
||||
|
||||
app = FastAPI(title="VSTU Compatibility API")
|
||||
|
||||
@app.get("/result.json")
|
||||
async def get_result():
|
||||
if os.path.exists(FILE_RESULT):
|
||||
return FileResponse(FILE_RESULT, media_type="application/json")
|
||||
return {"error": "Файл еще не сгенерирован. Попробуйте позже."}, 404
|
||||
|
||||
def log(msg):
|
||||
print(f"[{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}] {msg}", flush=True)
|
||||
|
||||
def get_raw_hash(raw_list):
|
||||
normalized = "|".join(sorted([str(i).strip() for i in raw_list]))
|
||||
return hashlib.sha1(normalized.encode('utf-8')).hexdigest()
|
||||
|
||||
def load_json(filename, default):
|
||||
if os.path.exists(filename):
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
try: return json.load(f)
|
||||
except: return default
|
||||
return default
|
||||
|
||||
def save_json(filename, data):
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
app.include_router(httpserver.router)
|
||||
|
||||
def merge_week_keys_metadata(sheets):
|
||||
merged = {}
|
||||
@@ -81,7 +33,64 @@ def merge_week_keys_metadata(sheets):
|
||||
merged[day][month].append(d)
|
||||
return merged
|
||||
|
||||
def ask_gemini(unknown_raws):
|
||||
def get_preferer_facultet(facultets_data: dict, excel_url: str, skip_for=None, ):
|
||||
if skip_for is None:
|
||||
skip_for = []
|
||||
|
||||
for _key, _value in facultets_data.items():
|
||||
if _key.startswith("_"):
|
||||
continue
|
||||
if _key in skip_for:
|
||||
continue
|
||||
|
||||
short_names = _value.get("short_names", None)
|
||||
if short_names is None:
|
||||
continue
|
||||
|
||||
for name in short_names:
|
||||
if name.lower() in excel_url.lower():
|
||||
return _key
|
||||
|
||||
def get_abbrev_for_facultet(facultets_data: dict, facultet_id: str, fallback_not_found="?", fallback_error="?", fallback_no_short_name="?"):
|
||||
if (facultet_id == 'mag'):
|
||||
return "МАГ"
|
||||
if (facultet_id == 'asp'):
|
||||
return "АСП"
|
||||
|
||||
for _key, _value in facultets_data.items():
|
||||
if _key != facultet_id:
|
||||
continue
|
||||
|
||||
short_names = _value.get("short_names", None)
|
||||
if short_names is None:
|
||||
return fallback_no_short_name
|
||||
|
||||
try:
|
||||
return short_names[0]
|
||||
except Exception as e:
|
||||
traceback.print_exception(e)
|
||||
return fallback_error
|
||||
return fallback_not_found
|
||||
|
||||
def get_slot_key_for_event(event: dict):
|
||||
raw = sorted(event.get("raw", []))
|
||||
pairs = event.get("pairs", [])
|
||||
times = event.get("times", [])
|
||||
weekday = event.get("weekday", None)
|
||||
weeknum = event.get("weeknum", None)
|
||||
|
||||
w = ""
|
||||
if weeknum is not None:
|
||||
w = "_WN" + str(weeknum)
|
||||
|
||||
if weekday is not None:
|
||||
w = "_WD" + str(weekday)
|
||||
|
||||
r = ('_'.join(pairs)) + ("_".join(times))
|
||||
if len(r) > 0: r = "_" + r
|
||||
return get_raw_hash(raw) + w + r
|
||||
|
||||
def ask_gemini(unknown_raws, cache):
|
||||
if not GEMINI_API_KEY:
|
||||
log("[!] GEMINI_API_KEY не задан! ИИ-парсинг пропущен.")
|
||||
return {}
|
||||
@@ -98,10 +107,26 @@ def ask_gemini(unknown_raws):
|
||||
prompt = f"""
|
||||
Ты парсер академического расписания. Я даю тебе JSON, где ключи - хэши, а значения - массив сырых строк расписания.
|
||||
Твоя задача вернуть JSON, где ключи - те же хэши, а значения - объекты с ключами:
|
||||
- "subject": строка, название предмета (или "Не указан")
|
||||
- "subject": строка, название предмета (или "Не указан") // Нормализуй subject ВЕРХ. РЕГИСТРОМ. и "С К Л Е И В А Й В О Т Т А К И Е" => "СКЛЕИВАЙ ВОТ ТАКИЕ" казусы
|
||||
- "location": строка, аудитории через запятую (или "Не указана")
|
||||
- "teacher": строка, ФИО преподавателя(ей) через запятую (или "Не указан")
|
||||
|
||||
Типы локаций
|
||||
ГУК - ГЛАВНЫЙ УЧЕБНЫЙ КОРПУС, если не указан корпус, пример: 400, значит речь идёт о гуке.
|
||||
А - корпус А, пример: А-400б, А-404
|
||||
Б - корпус Б, пример: Б-600
|
||||
В - корпус В (высотка), пример: В-1203,
|
||||
ЛК - корпус ЛК, пример: ЛК-100
|
||||
Т - корпус на ВГТЗ (на тракторном), пример: Т-200
|
||||
Корпус-Кабинет
|
||||
Корпус всегда на кириллице, всегда пиши дифис. Если место СИЛЬНО отличается, например спорт. зал, то оставь как есть строку.
|
||||
|
||||
В входных данных забывают дефис, иногда пишут криво например: "А-400,404" - очевидно ты должен выдать две локации в корпусе А, составители просто ленивые
|
||||
Если корпус ГУК, то префикса писать не надо, просто 100, 200, 400 и т.д.
|
||||
Если в raw есть КИРОВСКИЙ то сделай префикс КИР-
|
||||
Если ВГТЗ то Т-
|
||||
Если Красноармейский то префикс КРАСН-
|
||||
|
||||
Верни ТОЛЬКО валидный JSON без форматирования markdown.
|
||||
Входные данные:
|
||||
{json.dumps(batch, ensure_ascii=False)}
|
||||
@@ -113,105 +138,96 @@ def ask_gemini(unknown_raws):
|
||||
"generationConfig": {"responseMimeType": "application/json"}
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(url, json=payload, proxies=proxies, timeout=60)
|
||||
resp.raise_for_status()
|
||||
|
||||
data = resp.json()
|
||||
text_response = data['candidates'][0]['content']['parts'][0]['text']
|
||||
|
||||
parsed_batch = json.loads(text_response)
|
||||
results.update(parsed_batch)
|
||||
log(f"[+] Батч успешно обработан ИИ ({GEMINI_MODEL}).")
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
error_body = e.response.text if e.response is not None else "Нет тела ответа"
|
||||
log(f"[!] HTTP Ошибка API Gemini ({e.response.status_code}): {error_body}")
|
||||
except Exception as e:
|
||||
log(f"[!] Ошибка обращения к API Gemini: {e}")
|
||||
while True: # Цикл ожидания при лимитах
|
||||
try:
|
||||
resp = requests.post(url, json=payload, proxies=proxies, timeout=60*3)
|
||||
|
||||
if resp.status_code == 429:
|
||||
log("[!] Лимит запросов Gemini (429). Ждем 60 секунд...")
|
||||
time.sleep(60)
|
||||
continue
|
||||
|
||||
resp.raise_for_status()
|
||||
parsed_res = json.loads(resp.json()['candidates'][0]['content']['parts'][0]['text'])
|
||||
|
||||
# Сохраняем немедленно
|
||||
cache.update(parsed_res)
|
||||
save_json(FILE_CACHE, cache)
|
||||
log(f"[+] Батч успешно сохранен в {FILE_CACHE}")
|
||||
break # Успех, выходим из цикла ожидания
|
||||
|
||||
except Exception as e:
|
||||
log(f"[!!!] Ошибка батча: {e}")
|
||||
break # Фатальная ошибка батча, идем к следующему
|
||||
|
||||
return results
|
||||
|
||||
def fetch_json_robust(url, timeout=120):
|
||||
"""
|
||||
Устойчивый HTTP-клиент с маскировкой под браузер и механизмом Retry.
|
||||
Адаптирован для обхода базовых проверок Cloudflare.
|
||||
"""
|
||||
session = requests.Session()
|
||||
|
||||
# Маскировка под стандартный браузер
|
||||
session.headers.update({
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip, deflate", # Оптимизация получения 2.4 МБ
|
||||
"Connection": "keep-alive"
|
||||
})
|
||||
|
||||
# Настройка стратегии повторных попыток
|
||||
# 3 попытки, задержки: 2с, 4с, 8с. Отработка ошибок таймаутов Cloudflare (522, 524)
|
||||
retry_strategy = Retry(
|
||||
total=3,
|
||||
backoff_factor=2,
|
||||
status_forcelist=[429, 500, 502, 503, 504, 522, 524],
|
||||
allowed_methods=["GET"]
|
||||
)
|
||||
|
||||
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
|
||||
proxies = {"http": PROXY_URL, "https": PROXY_URL} if PROXY_URL else None
|
||||
|
||||
response = session.get(url, timeout=timeout, proxies=proxies)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def job_iteration():
|
||||
log("--- Начало итерации обновления расписания ---")
|
||||
cache = load_json(FILE_CACHE, {})
|
||||
|
||||
log(f"[*] Скачивание {URL_FACULTETS} ...")
|
||||
try:
|
||||
facultets_data = fetch_json_robust(URL_FACULTETS, timeout=120)
|
||||
except Exception as e:
|
||||
log(f"[!!!] Ошибка скачивания facultets.json: {e}")
|
||||
return "error"
|
||||
|
||||
# ШАГ 1: Единоразовая загрузка монолита
|
||||
log(f"[*] Скачивание all-in-one файла {URL_RESULT_V2} ...")
|
||||
try:
|
||||
v2_data = fetch_json_robust(URL_RESULT_V2, timeout=120)
|
||||
except Exception as e:
|
||||
log(f"[!!!] Ошибка скачивания базового файла расписаний: {e}")
|
||||
return
|
||||
return "error"
|
||||
|
||||
items_to_process = v2_data.get("all_files", [])
|
||||
if not items_to_process:
|
||||
all_files = v2_data.get("all_files", [])
|
||||
if not all_files:
|
||||
log("[!] Ключ 'all_files' пуст или отсутствует в result_v2.json")
|
||||
return
|
||||
|
||||
# ШАГ 2: Предварительный обход для сбора неизвестных ячеек (ОРИГИНАЛЬНАЯ ЛОГИКА ОБХОДА)
|
||||
unknown_raws = {}
|
||||
|
||||
for file_data in items_to_process:
|
||||
sheets = file_data.get("sheets", {})
|
||||
for sheet_data in sheets.values():
|
||||
for group_data in sheet_data.get("groups", {}).values():
|
||||
slots = group_data.get("slots", {})
|
||||
def process_unknown_raw_events(pair_data):
|
||||
events = pair_data if isinstance(pair_data, list) else [pair_data]
|
||||
for event in events:
|
||||
if not isinstance(event, dict): continue
|
||||
|
||||
raw_list = event.get("raw", [])
|
||||
if not raw_list: continue
|
||||
r_hash = get_raw_hash(raw_list)
|
||||
|
||||
if r_hash not in cache and r_hash not in unknown_raws:
|
||||
unknown_raws[r_hash] = raw_list
|
||||
|
||||
for excel_file_dict in all_files:
|
||||
sheets = excel_file_dict.get("sheets", {})
|
||||
for sheet_dict in sheets.values():
|
||||
groups = sheet_dict.get("groups", {})
|
||||
|
||||
for group_dict in groups.values():
|
||||
slots = group_dict.get("slots", {})
|
||||
|
||||
# slot_key = ПОНЕДЕЛЬНИК etc...
|
||||
for slot_key, pair_value in slots.items():
|
||||
if not isinstance(pair_value, dict): continue
|
||||
for pair_key, pair_data in pair_value.items():
|
||||
if not (isinstance(pair_key, str) and '-' in pair_key): continue
|
||||
|
||||
events = pair_data if isinstance(pair_data, list) else [pair_data]
|
||||
for event in events:
|
||||
if not isinstance(event, dict): continue
|
||||
if isinstance(pair_value, dict):
|
||||
is_event_object = pair_value.get("object", None) == 'event'
|
||||
if is_event_object:
|
||||
process_unknown_raw_events(pair_value)
|
||||
else:
|
||||
for pair_key, pair_data in pair_value.items():
|
||||
if not (isinstance(pair_key, str) and '-' in pair_key): continue
|
||||
process_unknown_raw_events(pair_data)
|
||||
|
||||
raw_list = event.get("raw", [])
|
||||
if not raw_list: continue
|
||||
r_hash = get_raw_hash(raw_list)
|
||||
|
||||
if r_hash not in cache and r_hash not in unknown_raws:
|
||||
unknown_raws[r_hash] = raw_list
|
||||
|
||||
# ШАГ 3: Вызов ИИ (Только для новых строк)
|
||||
if unknown_raws:
|
||||
log(f"[*] Найдено {len(unknown_raws)} новых уникальных записей. Обращаемся к Gemini API...")
|
||||
ai_data = ask_gemini(unknown_raws)
|
||||
ai_data = ask_gemini(unknown_raws, cache)
|
||||
|
||||
if ai_data:
|
||||
cache.update(ai_data)
|
||||
save_json(FILE_CACHE, cache)
|
||||
@@ -220,87 +236,115 @@ def job_iteration():
|
||||
# ШАГ 4: Сборка финального графа
|
||||
final_groups = {}
|
||||
excels_list = []
|
||||
|
||||
def process_event_for_group(group_key, event):
|
||||
if (isinstance(event, list)):
|
||||
for x in event:
|
||||
process_event_for_group(group_key, x)
|
||||
return
|
||||
|
||||
is_event_object = False
|
||||
try:
|
||||
is_event_object = event.get("object", None) == 'event'
|
||||
except Exception as e:
|
||||
traceback.print_exception(e)
|
||||
print(event)
|
||||
|
||||
if not is_event_object:
|
||||
log("[WTF] process_event_for_group is not event object :<")
|
||||
return
|
||||
|
||||
raw_list = event.get("raw", [])
|
||||
slot_key = get_slot_key_for_event(event)
|
||||
|
||||
if slot_key not in final_groups[group_key]["slots"]:
|
||||
final_groups[group_key]["slots"][slot_key] = {}
|
||||
r_hash = get_raw_hash(raw_list)
|
||||
|
||||
res = cache.get(r_hash, {})
|
||||
locs = [l.strip() for l in res.get("location", "").split(",")] if res.get("location") and res.get("location") not in ["Не указана", "Не указан"] else []
|
||||
leads = [l.strip() for l in res.get("teacher", "").split(",")] if res.get("teacher") and res.get("teacher") not in ["Не указана", "Не указан"] else []
|
||||
|
||||
final_groups[group_key]["slots"][slot_key] = {
|
||||
"discipline_name": res.get("subject", "Не указан"),
|
||||
"locations": locs,
|
||||
"leads": leads,
|
||||
"is_solid": event.get("is_solid", True),
|
||||
"is_flow": event.get("is_flow", False),
|
||||
"raw": raw_list,
|
||||
"weekday": event.get("weekday"),
|
||||
"weeknum": event.get("weeknum"),
|
||||
"excel_range": event.get("excel_range"),
|
||||
"excel_pos": event.get("excel_pos"),
|
||||
"raw_hash": r_hash
|
||||
}
|
||||
|
||||
|
||||
log("[*] Сборка структуры слияния...")
|
||||
for file_data in items_to_process:
|
||||
excel_info = file_data.get("excel", {})
|
||||
sheets = file_data.get("sheets", {})
|
||||
for excel_file_dict in all_files:
|
||||
excel = excel_file_dict.get("excel", {})
|
||||
excel_url = excel['url']
|
||||
excel_facultet = excel['facultet']
|
||||
preferer_facultet = get_preferer_facultet(facultets_data, excel_url, skip_for=['asp', 'mag'])
|
||||
group_prefix = get_abbrev_for_facultet(facultets_data, excel_facultet) + "/"
|
||||
if preferer_facultet != excel_facultet and preferer_facultet is not None:
|
||||
group_prefix = get_abbrev_for_facultet(facultets_data, preferer_facultet) + "/" + group_prefix
|
||||
|
||||
file_ds_hash = excel_info.get("sha1hash", "UNKNOWN")
|
||||
file_ds_hash = excel.get("sha1hash", "UNKNOWN")
|
||||
|
||||
sheets = excel_file_dict.get("sheets", {})
|
||||
|
||||
# Сборка метаданных экселя
|
||||
excel_meta = {
|
||||
"data_source_hash": file_ds_hash,
|
||||
"prefix_groups": group_prefix,
|
||||
"facultet": excel_facultet,
|
||||
"preferer_facultet": preferer_facultet,
|
||||
"url": excel_url,
|
||||
"week_keys_metadata": merge_week_keys_metadata(sheets)
|
||||
}
|
||||
for k, v in excel_info.items():
|
||||
for k, v in excel.items():
|
||||
if k not in ["sheets", "data_source_hash", "week_keys_metadata"] and not isinstance(v, (dict, list)):
|
||||
excel_meta[k] = v
|
||||
excels_list.append(excel_meta)
|
||||
|
||||
# Обработка групп (ОРИГИНАЛЬНАЯ ЛОГИКА ОБХОДА)
|
||||
for sheet_data in sheets.values():
|
||||
groups = sheet_data.get("groups", {})
|
||||
for sheet_dict in sheets.values():
|
||||
groups = sheet_dict.get("groups", {})
|
||||
|
||||
for group_id, group_data in groups.items():
|
||||
group_name = group_data["name"]
|
||||
for group_id, group_dict in groups.items():
|
||||
group_name = group_dict["name"]
|
||||
group_key = (group_prefix + group_name.replace(" ", "")).upper()
|
||||
|
||||
if group_name not in final_groups:
|
||||
final_groups[group_name] = {
|
||||
if group_key not in final_groups:
|
||||
final_groups[group_key] = {
|
||||
"name": group_name,
|
||||
"facultet": excel_info.get('facultet', 'Неизвестно'),
|
||||
"position": group_data.get("position"),
|
||||
"position_human": group_data.get("position_human"),
|
||||
"facultet": excel_facultet,
|
||||
"preferer_facultet": preferer_facultet,
|
||||
"position": group_dict.get("position"),
|
||||
"position_human": group_dict.get("position_human"),
|
||||
"slots": {},
|
||||
"data_source_hash": file_ds_hash
|
||||
}
|
||||
else:
|
||||
if file_ds_hash not in final_groups[group_name]["data_source_hash"]:
|
||||
final_groups[group_name]["data_source_hash"] += f",{file_ds_hash}"
|
||||
if file_ds_hash not in final_groups[group_key]["data_source_hash"]:
|
||||
final_groups[group_key]["data_source_hash"] += f",{file_ds_hash}"
|
||||
|
||||
slots = group_data.get("slots", {})
|
||||
slots = group_dict.get("slots", {})
|
||||
|
||||
# slot_key as ПОНЕДЕЛЬНИК_1 etc
|
||||
for slot_key, pair_value in slots.items():
|
||||
if not isinstance(pair_value, dict): continue
|
||||
|
||||
if slot_key not in final_groups[group_name]["slots"]:
|
||||
final_groups[group_name]["slots"][slot_key] = {}
|
||||
if isinstance(pair_value, dict):
|
||||
is_event_object = pair_value.get("object", None) == 'event'
|
||||
if is_event_object:
|
||||
process_event_for_group(group_key, pair_value)
|
||||
else:
|
||||
for key, value in pair_value.items():
|
||||
process_event_for_group(group_key, value)
|
||||
|
||||
target_slot = final_groups[group_name]["slots"][slot_key]
|
||||
|
||||
for pair_key, pair_data in pair_value.items():
|
||||
if not (isinstance(pair_key, str) and '-' in pair_key): continue
|
||||
|
||||
events = pair_data if isinstance(pair_data, list) else [pair_data]
|
||||
|
||||
for event in events:
|
||||
if not isinstance(event, dict): continue
|
||||
|
||||
# Логика склейки дубликатов (1-2, 1-2_1)
|
||||
current_pair_id = pair_key
|
||||
counter = 1
|
||||
while current_pair_id in target_slot:
|
||||
current_pair_id = f"{pair_key}_{counter}"
|
||||
counter += 1
|
||||
|
||||
raw_list = event.get("raw", [])
|
||||
r_hash = get_raw_hash(raw_list)
|
||||
|
||||
res = cache.get(r_hash, {})
|
||||
locs = [l.strip() for l in res.get("location", "").split(",")] if res.get("location") and res.get("location") not in ["Не указана", "Не указан"] else []
|
||||
leads = [l.strip() for l in res.get("teacher", "").split(",")] if res.get("teacher") and res.get("teacher") not in ["Не указана", "Не указан"] else []
|
||||
|
||||
target_slot[current_pair_id] = {
|
||||
"discipline_name": res.get("subject", "Не указан"),
|
||||
"locations": locs,
|
||||
"leads": leads,
|
||||
"is_solid": event.get("is_solid", True),
|
||||
"is_flow": event.get("is_flow", False),
|
||||
"raw": raw_list,
|
||||
"weekday": event.get("weekday"),
|
||||
"weeknum": event.get("weeknum"),
|
||||
"excel_range": event.get("excel_range"),
|
||||
"excel_pos": event.get("excel_pos")
|
||||
}
|
||||
elif isinstance(pair_value, list):
|
||||
for xxx in pair_value:
|
||||
process_event_for_group(group_key, xxx)
|
||||
|
||||
# Формируем финальный результат ТОЧНО в первоначальном виде
|
||||
output = {
|
||||
@@ -330,7 +374,7 @@ def job_iteration():
|
||||
"excels": excels_list
|
||||
}
|
||||
|
||||
save_json(FILE_RESULT, output)
|
||||
save_json(FILE_RESULT, output, sort_keys=True)
|
||||
log(f"[+] Сборка графа завершена. Уникальных групп: {len(final_groups)}. Файл {FILE_RESULT} обновлен.")
|
||||
|
||||
# ... Логика RabbitMQ остается без изменений ...
|
||||
@@ -365,14 +409,22 @@ def parser_loop():
|
||||
if not GEMINI_API_KEY: log("[!] GEMINI_API_KEY не задан!")
|
||||
|
||||
while True:
|
||||
devider = 1
|
||||
try:
|
||||
job_iteration()
|
||||
r = job_iteration()
|
||||
if r == "error":
|
||||
devider = 10
|
||||
print("returned error; devider now = 10")
|
||||
|
||||
except Exception as e:
|
||||
log(f"[!!!] Критическая ошибка: {e}")
|
||||
traceback.print_exc()
|
||||
devider = 5
|
||||
log(f"devider now = 5")
|
||||
|
||||
log(f"[*] Ожидание {POLL_INTERVAL} сек...\n")
|
||||
time.sleep(POLL_INTERVAL)
|
||||
delay = round(POLL_INTERVAL / devider)
|
||||
log(f"[*] Ожидание {delay} сек...\n")
|
||||
time.sleep(delay)
|
||||
|
||||
if __name__ == "__main__":
|
||||
threading.Thread(target=parser_loop, daemon=True).start()
|
||||
|
||||
Reference in New Issue
Block a user