This commit is contained in:
2026-03-28 14:00:19 +03:00
parent fc739d591d
commit d5cf92858e
5 changed files with 6468 additions and 4119 deletions

2
.env
View File

@@ -1,3 +1,3 @@
GEMINI_API_KEY=AIzaSyDTFKJscB72NY7R-zeyZcmO0iTnJRoMusw GEMINI_API_KEY=AIzaSyDTFKJscB72NY7R-zeyZcmO0iTnJRoMusw
PROXY_URL=socks5://127.0.0.1:2080 PROXY_URL=socks5://127.0.0.1:2081
POLL_INTERVAL=300 POLL_INTERVAL=300

View File

@@ -15323,5 +15323,250 @@
"subject": "МОДЕЛИРОВАНИЕ СИСТЕМ", "subject": "МОДЕЛИРОВАНИЕ СИСТЕМ",
"location": "В-403", "location": "В-403",
"teacher": "Казьмин И.В." "teacher": "Казьмин И.В."
},
"6724fe932660d6b1537c0ffced2d6867b1e4b001": {
"subject": "ФИЗИКА",
"location": "Т-108",
"teacher": "доц. Михайлов В.К."
},
"ec2a25844383316f6052fba255a0e75d572ddf4c": {
"subject": "ПРАКТИКА 4 часа",
"location": "Не указана",
"teacher": "Бессаరెడ్డి С.С."
},
"72930243a141264fc55fdafdec7da368b109c2c4": {
"subject": "ПРАКТИКА 4 часа",
"location": "Не указана",
"teacher": "Евтушенко О.А."
},
"6a933a2d817aded941ae229701e6aac03faea617": {
"subject": "ПРАКТИКА 4 часа",
"location": "425.0",
"teacher": "Пром Н.А."
},
"3230ca70034e0547a8bf20dafee5d903f13a2c70": {
"subject": "ПРАКТИКА 4 часа",
"location": "Не указана",
"teacher": "Пром Н.А."
},
"b29ea992e9eb0d09cba62f2aa18b1c06c2cb51c1": {
"subject": "ЛЕКЦИЯ 4 группа 4 часа ",
"location": "В 801",
"teacher": "доц. Казанова Н.В."
},
"d47cb3ec778ff93124e5ce8ef62ed7fc92543469": {
"subject": "СЕМИНАР 4 группа 4 часа",
"location": "В 801",
"teacher": "Казанова Н.В."
},
"c1e177f8707494c7767653f1310a69d09270a2bb": {
"subject": "ПРАКТИКА 4 часа",
"location": "407.0",
"teacher": "Тихаева В.В."
},
"3f3f5628af3a858920999d22b0aaa12285040a0d": {
"subject": "ПРАКТИКА 4 часа",
"location": "Не указана",
"teacher": "Топоркова О.В."
},
"f8ad08b013aa931fd7821e721ee590f22c45209c": {
"subject": "ПРАКТИКА 4 часа",
"location": "425.0",
"teacher": "Ионкина Е.Ю."
},
"755242026a4f8c386974dd77c5aa9103ebf28598": {
"subject": "ПРАКТИКА 4 часа",
"location": "426.0",
"teacher": "Лихачева Т.С."
},
"57f2e5f3643fcc0d159731e7bd3e530864f9fb5a": {
"subject": "ПРАКТИКА 4 часа",
"location": "425.0",
"teacher": "Лихачева Т.С."
},
"3f2ef2c3240697f19d1de0d7028e7b87cdfd74bc": {
"subject": "ПРАКТИКА 4 часа",
"location": "Не указана",
"teacher": "Лихачева Т.С."
},
"c1b53091d551421de5142b47ffb764bf5ea914f1": {
"subject": "ПРАКТИКА 4 часа",
"location": "124, 436а",
"teacher": "Тисленкова И.А."
},
"eb82443458f1ed6b4c0f2e87f1fd909fe5902980": {
"subject": "ПРАКТИКА 4 часа",
"location": "408а, 407",
"teacher": "Тисленкова И.А."
},
"e61b773d2d9cc5430395b9d040df920dc504500b": {
"subject": "СЕМИНАР 6 группа 4 часа",
"location": "Не указана",
"teacher": "Леонтьева Е.Ю."
},
"134b2b88176cb1ca78163818c892fada0c74f86b": {
"subject": "ЛЕКЦИЯ 6 группа 4 часа",
"location": "Не указана",
"teacher": "проф.Леонтьева Е.Ю."
},
"45820f55f14f5383c719a4f93ede4aa1c73bbce1": {
"subject": "ЛЕКЦИЯ 1 - 3 группы 4 часа",
"location": "В 801",
"teacher": "проф. Виноградова Н.Л."
},
"5b09400f3c608563d393dcd54cba0f0ec3abe5c4": {
"subject": "СЕМИНАР 3 группа 4 часа",
"location": "В 505",
"teacher": "Виноградова Н.Л."
},
"e441900f2ba1f093c52f24871721c4426bd8041b": {
"subject": "СЕМИНАР 1 группа 4 часа",
"location": "124.0",
"teacher": "Виноградова Н.Л."
},
"872c9eb679b28e94f1c3dc9b8fa8ed706d2915d5": {
"subject": "СЕМИНАР 2 группа 4 часа",
"location": "В 505",
"teacher": "Виноградова Н.Л."
},
"96af3e79170d8ec7418563ef670745c222822c03": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "400.0",
"teacher": "Не указан"
},
"36eef4b36538a6df1a0b4ad68bcf4a4a8948cab5": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "121.0",
"teacher": "Не указан"
},
"1342dc48f6ce46f429855be042b26955abe083c9": {
"subject": "ПАТЕНТОВЕДЕНИЕ ",
"location": "Б 205а",
"teacher": "доц. Курсин О.А."
},
"1059c33321e35284cacca3a896b8d30807d4c0bd": {
"subject": "ПАТЕНТОВЕДЕНИЕ ",
"location": "Б 205а",
"teacher": "Курсин О.А."
},
"6f9de7cf339ed97369128d1853fc5bc6cc22d561": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "406.0",
"teacher": "Не указан"
},
"8b6019ca7b6b00d9b2cd611cad95832e68db3c47": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "А 603",
"teacher": "Не указан"
},
"956ee4f6b1e52a89c8c2cb2d11418f494728647d": {
"subject": "Не указан",
"location": "Б 102",
"teacher": "Поступаева С.Г."
},
"0240be3346523f64f9af51608e89a1a664b08f01": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "416.0",
"teacher": "Не указан"
},
"17c47690a180e68d5941a014127cc367907eaef1": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "400.0",
"teacher": "Не указан"
},
"6e8a9686c53da2660392904f55d7c4d5071cf0b8": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "406.0",
"teacher": "Не указан"
},
"a9dc0f696cac983080fd428ab2008d7a1d50e2f7": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "406.0",
"teacher": "Не указан"
},
"b8cf0a65bc5a0f358ea25730af9daa61c1275b2e": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "314.0",
"teacher": "Не указан"
},
"72d0556e2a0081e5f2869669ba9a8800ad1657f6": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "416.0",
"teacher": "Не указан"
},
"2a8f52817b684c6159a7582b36e0afd52873d619": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "406.0",
"teacher": "Не указан"
},
"1451ec00d807a838ad2a4563e02c00abe3f05486": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "В 802",
"teacher": "Не указан"
},
"60ac332b3910fa75358b9b072fe28225044c7dec": {
"subject": "ПРОИЗВ.УЧЕТ И ОТЧ.",
"location": "Б 607",
"teacher": "Золотарева А.Г."
},
"d190f5b45133cf18376ff3ae0f532314fb4970e5": {
"subject": "ЧИСЛ.\nМЕТОДЫ",
"location": "ОБЩ.517",
"teacher": "ст.преп. Аршинов А.В."
},
"509ccaffa0922ca72cc319d33f5a21b98e65f371": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "В 1102",
"teacher": "Не указан"
},
"6170261d2943446fb3ec8de95091a7f1eef53022": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "В 1002",
"teacher": "Не указан"
},
"9dc221ea333d4e68a09a97b3e76e89a7ea52d706": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "В 1102",
"teacher": "Не указан"
},
"bd439495d85950c35b15e0de7e71f2a91c191887": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "Б 609",
"teacher": "Не указан"
},
"79bc8090eb50dd3c49466a7d248ff4e262b93ab4": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "312.0",
"teacher": "Не указан"
},
"09bb560cdc7707574e41dade8c67d8e32cec1327": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "А 603",
"teacher": "Не указан"
},
"6c07886b56c082033e7c71fd86139ad33bd054fe": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "314.0",
"teacher": "Не указан"
},
"539fd4d3fac18bf571680e16133cea1514ea8639": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "А 610",
"teacher": "Не указан"
},
"b509510277d78bf7472d54fea1d59169a35c224c": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "В 1102",
"teacher": "Не указан"
},
"6886c6b4a8efabca13d3c3c14538e0a04d97d5b3": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "Б 609",
"teacher": "Не указан"
},
"9b2b77bcbf8e02b8dc67dcf7e36810318f090737": {
"subject": "ИНФОРМ. КУЛЬТУРА",
"location": "А 605",
"teacher": "Не указан"
} }
} }

File diff suppressed because it is too large Load Diff

241
main.py
View File

@@ -1,31 +1,52 @@
import json import json
import os import os
import hashlib import hashlib
import threading
import pika
import requests import requests
import time import time
import traceback import traceback
from datetime import datetime from datetime import datetime
from fastapi import FastAPI
from fastapi.responses import FileResponse
import uvicorn
# --- Загрузка переменных окружения --- # --- Загрузка переменных окружения ---
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
# ----------------------------------- # -----------------------------------
# ================= КОНФИГУРАЦИЯ ================= # ================= КОНФИГУРАЦИЯ =================
URL_PARSER_ROOT = "https://fazziclay.com/api/v1/vstu_schedule_parser_v2/parser.json" # ИЗМЕНЕНИЕ 1: Отказываемся от списка файлов. Грузим монолит.
BASE_URL_FILES = "https://fazziclay.com/api/v1/vstu_schedule_parser_v2/parsed/" URL_RESULT_V2 = "https://fazziclay.com/api/v1/vstu_schedule_parser_v2/result_v2.json"
FILE_RESULT = "data/result.json" FILE_RESULT = "data/result.json"
FILE_CACHE = "data/raw_cache.json" FILE_CACHE = "data/raw_cache.json"
GEMINI_MODEL="gemini-flash-latest" GEMINI_MODEL="gemini-flash-lite-latest"
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", 600)) POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", 600))
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
PROXY_URL = os.environ.get("PROXY_URL") 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))
# ================================================= # =================================================
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): def log(msg):
"""Логирование с принудительным сбросом буфера (важно для Docker)."""
print(f"[{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}] {msg}", flush=True) print(f"[{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}] {msg}", flush=True)
def get_raw_hash(raw_list): def get_raw_hash(raw_list):
@@ -44,7 +65,6 @@ def save_json(filename, data):
json.dump(data, f, ensure_ascii=False, indent=2) json.dump(data, f, ensure_ascii=False, indent=2)
def merge_week_keys_metadata(sheets): def merge_week_keys_metadata(sheets):
"""Объединяет week_keys_metadata со всех листов, исключая дубликаты дней."""
merged = {} merged = {}
for sheet_data in sheets.values(): for sheet_data in sheets.values():
wkm = sheet_data.get("week_keys_metadata", {}) wkm = sheet_data.get("week_keys_metadata", {})
@@ -54,7 +74,6 @@ def merge_week_keys_metadata(sheets):
for month, days in months.items(): for month, days in months.items():
if month not in merged[day]: if month not in merged[day]:
merged[day][month] = [] merged[day][month] = []
# Добавляем даты, сохраняя порядок и избегая дублей
for d in days: for d in days:
if d not in merged[day][month]: if d not in merged[day][month]:
merged[day][month].append(d) merged[day][month].append(d)
@@ -65,11 +84,7 @@ def ask_gemini(unknown_raws):
log("[!] GEMINI_API_KEY не задан! ИИ-парсинг пропущен.") log("[!] GEMINI_API_KEY не задан! ИИ-парсинг пропущен.")
return {} return {}
proxies = {} proxies = {"http": PROXY_URL, "https": PROXY_URL} if PROXY_URL else {}
if PROXY_URL:
log("Using proxy")
proxies = {"http": PROXY_URL, "https": PROXY_URL}
results = {} results = {}
items = list(unknown_raws.items()) items = list(unknown_raws.items())
batch_size = 40 batch_size = 40
@@ -92,18 +107,13 @@ def ask_gemini(unknown_raws):
url = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent?key={GEMINI_API_KEY}" url = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent?key={GEMINI_API_KEY}"
payload = { payload = {
"contents": [{ "contents": [{"role": "user", "parts": [{"text": prompt}]}],
"role": "user", "generationConfig": {"responseMimeType": "application/json"}
"parts": [{"text": prompt}]
}],
"generationConfig": {
"responseMimeType": "application/json" # ИСПРАВЛЕНИЕ: строго camelCase для REST API
}
} }
try: try:
resp = requests.post(url, json=payload, proxies=proxies, timeout=60) resp = requests.post(url, json=payload, proxies=proxies, timeout=60)
resp.raise_for_status() # Выкинет ошибку для 400, 403, 404 и т.д. resp.raise_for_status()
data = resp.json() data = resp.json()
text_response = data['candidates'][0]['content']['parts'][0]['text'] text_response = data['candidates'][0]['content']['parts'][0]['text']
@@ -113,94 +123,136 @@ def ask_gemini(unknown_raws):
log(f"[+] Батч успешно обработан ИИ ({GEMINI_MODEL}).") log(f"[+] Батч успешно обработан ИИ ({GEMINI_MODEL}).")
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
# ИСПРАВЛЕНИЕ: Читаем текст ответа от Google (там написана причина)
error_body = e.response.text if e.response is not None else "Нет тела ответа" error_body = e.response.text if e.response is not None else "Нет тела ответа"
log(f"[!] HTTP Ошибка API Gemini ({e.response.status_code}): {error_body}")
if e.response.status_code == 404:
log(f"[!] Ошибка 404: Модель '{GEMINI_MODEL}' не найдена. Обновите GEMINI_MODEL в .env!")
elif e.response.status_code == 400:
log(f"[!] Ошибка 400 (Bad Request): Неверный формат запроса. Ответ Google:\n{error_body}")
else:
log(f"[!] HTTP Ошибка API Gemini: {e}\nОтвет: {error_body}")
except Exception as e: except Exception as e:
log(f"[!] Ошибка обращения к API Gemini: {e}") log(f"[!] Ошибка обращения к API Gemini: {e}")
return results return results
def job_iteration():
log("--- Начало итерации обновления расписания ---")
cache = load_json(FILE_CACHE, {})
def fetch_and_build(cache): # ШАГ 1: Единоразовая загрузка монолита
log("[*] Загрузка списка файлов парсера V2...") log(f"[*] Скачивание all-in-one файла {URL_RESULT_V2} ...")
parser_data = requests.get(URL_PARSER_ROOT, timeout=30).json() try:
v2_data = requests.get(URL_RESULT_V2, timeout=60).json()
except Exception as e:
log(f"[!!!] Ошибка скачивания базового файла расписаний: {e}")
return
final_groups = {} items_to_process = v2_data.get("all_files", [])
excels_list =[] if not items_to_process:
log("[!] Ключ 'all_files' пуст или отсутствует в result_v2.json")
return
# ШАГ 2: Предварительный обход для сбора неизвестных ячеек (ОРИГИНАЛЬНАЯ ЛОГИКА ОБХОДА)
unknown_raws = {} unknown_raws = {}
for file_info in parser_data.get("all_files",[]): for file_data in items_to_process:
file_url = f"{BASE_URL_FILES}{requests.utils.quote(file_info['json_represent'])}" sheets = file_data.get("sheets", {})
log(f"[*] Обработка файла: {file_info['json_represent']}") for sheet_data in sheets.values():
for group_data in sheet_data.get("groups", {}).values():
slots = group_data.get("slots", {})
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
try: events = pair_data if isinstance(pair_data, list) else [pair_data]
faculty_data = requests.get(file_url, timeout=30).json() for event in events:
except Exception as e: if not isinstance(event, dict): continue
log(f"[!] Ошибка скачивания {file_url}: {e}")
continue
file_ds_hash = faculty_data.get("sha1hash", faculty_data.get("excel", {}).get("sha1hash", "UNKNOWN")) raw_list = event.get("raw", [])
sheets = faculty_data.get("sheets", {}) if not raw_list: continue
r_hash = get_raw_hash(raw_list)
# --- СБОРКА ОБЪЕКТА EXCEL (Обновлено) --- 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)
if ai_data:
cache.update(ai_data)
save_json(FILE_CACHE, cache)
log(f"[+] Кэш обновлен, добавлено {len(ai_data)} записей.")
# ШАГ 4: Сборка финального графа
final_groups = {}
excels_list = []
log("[*] Сборка структуры слияния...")
for file_data in items_to_process:
excel_info = file_data.get("excel", {})
sheets = file_data.get("sheets", {})
file_ds_hash = excel_info.get("sha1hash", "UNKNOWN")
# Сборка метаданных экселя
excel_meta = { excel_meta = {
"data_source_hash": file_ds_hash, "data_source_hash": file_ds_hash,
"week_keys_metadata": merge_week_keys_metadata(sheets) "week_keys_metadata": merge_week_keys_metadata(sheets)
} }
# Переносим остальные корневые ключи из оригинального json эксельки (за исключением уже обработанных) for k, v in excel_info.items():
for k, v in faculty_data.items(): if k not in ["sheets", "data_source_hash", "week_keys_metadata"] and not isinstance(v, (dict, list)):
if k not in ["sheets", "data_source_hash", "week_keys_metadata"]:
excel_meta[k] = v excel_meta[k] = v
excels_list.append(excel_meta) excels_list.append(excel_meta)
# ----------------------------------------
# Обработка групп (ОРИГИНАЛЬНАЯ ЛОГИКА ОБХОДА)
for sheet_data in sheets.values(): for sheet_data in sheets.values():
groups = sheet_data.get("groups", {}) groups = sheet_data.get("groups", {})
for group_id, group_data in groups.items(): for group_id, group_data in groups.items():
if group_data["name"] not in final_groups: group_name = group_data["name"]
final_groups[group_data["name"]] = {
"name": group_data["name"], if group_name not in final_groups:
"facultet": faculty_data.get('excel', {}).get('facultet', 'Неизвестно'), final_groups[group_name] = {
"name": group_name,
"facultet": excel_info.get('facultet', 'Неизвестно'),
"position": group_data.get("position"), "position": group_data.get("position"),
"position_human": group_data.get("position_human"), "position_human": group_data.get("position_human"),
"slots": {}, "slots": {},
"data_source_hash": file_ds_hash "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}"
slots = group_data.get("slots", {}) slots = group_data.get("slots", {})
for slot_key, pair_value in slots.items(): for slot_key, pair_value in slots.items():
if not isinstance(pair_value, dict): continue if not isinstance(pair_value, dict): continue
if slot_key not in final_groups[group_data["name"]]["slots"]: if slot_key not in final_groups[group_name]["slots"]:
final_groups[group_data["name"]]["slots"][slot_key] = {} final_groups[group_name]["slots"][slot_key] = {}
target_slot = final_groups[group_name]["slots"][slot_key]
for pair_key, pair_data in pair_value.items(): for pair_key, pair_data in pair_value.items():
if not (isinstance(pair_key, str) and '-' in pair_key): continue if not (isinstance(pair_key, str) and '-' in pair_key): continue
events = pair_data if isinstance(pair_data, list) else [pair_data] events = pair_data if isinstance(pair_data, list) else [pair_data]
for i, event in enumerate(events): for event in events:
current_pair_id = pair_key if i == 0 else f"{pair_key}_{i}"
if not isinstance(event, dict): continue 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", []) raw_list = event.get("raw", [])
r_hash = get_raw_hash(raw_list) r_hash = get_raw_hash(raw_list)
if r_hash in cache: res = cache.get(r_hash, {})
res = cache[r_hash]
locs = [l.strip() for l in res.get("location", "").split(",")] if res.get("location") and res.get("location") not in ["Не указана", "Не указан"] else [] 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 [] leads = [l.strip() for l in res.get("teacher", "").split(",")] if res.get("teacher") and res.get("teacher") not in ["Не указана", "Не указан"] else []
final_groups[group_data["name"]]["slots"][slot_key][current_pair_id] = { target_slot[current_pair_id] = {
"discipline_name": res.get("subject", "Не указан"), "discipline_name": res.get("subject", "Не указан"),
"locations": locs, "locations": locs,
"leads": leads, "leads": leads,
@@ -212,33 +264,8 @@ def fetch_and_build(cache):
"excel_range": event.get("excel_range"), "excel_range": event.get("excel_range"),
"excel_pos": event.get("excel_pos") "excel_pos": event.get("excel_pos")
} }
else:
unknown_raws[r_hash] = raw_list
return final_groups, excels_list, unknown_raws # Формируем финальный результат ТОЧНО в первоначальном виде
def job_iteration():
log("--- Начало итерации обновления расписания ---")
cache = load_json(FILE_CACHE, {})
# ПЕРВЫЙ ПРОХОД: собираем данные и ищем неизвестные строки
final_groups, excels_list, unknown_raws = fetch_and_build(cache)
# Если есть неизвестные строки — отправляем в ИИ
if unknown_raws:
log(f"[*] Найдено {len(unknown_raws)} новых уникальных записей. Обращаемся к Gemini API...")
ai_data = ask_gemini(unknown_raws)
if ai_data:
cache.update(ai_data)
save_json(FILE_CACHE, cache)
log(f"[+] Кэш обновлен, добавлено {len(ai_data)} записей.")
# ВТОРОЙ ПРОХОД: пересобираем расписание уже с новыми кэшированными данными ИИ
log("[*] Пересборка структуры с учетом новых данных от ИИ...")
final_groups, excels_list, _ = fetch_and_build(cache)
# Формируем финальный результат
output = { output = {
"version": 1, "version": 1,
"notice": "ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Данные, доступ к API и т.д. предоставляется КАК-ЕСТЬ (AS-IS) без каких либо, явно или не явно подразумеваемых гарантий.\n\nПарсер написал: Миронов Станислав\n\nИсточник данных: https://www.vstu.ru/student/raspisaniya/zanyatiy/index.php", "notice": "ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Данные, доступ к API и т.д. предоставляется КАК-ЕСТЬ (AS-IS) без каких либо, явно или не явно подразумеваемых гарантий.\n\nПарсер написал: Миронов Станислав\n\nИсточник данных: https://www.vstu.ru/student/raspisaniya/zanyatiy/index.php",
@@ -267,25 +294,49 @@ def job_iteration():
} }
save_json(FILE_RESULT, output) save_json(FILE_RESULT, output)
log(f"[+] Итерация завершена. Файл {FILE_RESULT} успешно обновлен.") log(f"[+] Сборка графа завершена. Уникальных групп: {len(final_groups)}. Файл {FILE_RESULT} обновлен.")
# ... Логика RabbitMQ остается без изменений ...
if not (RABBITMQ_USER and RABBITMQ_PASS):
log("[!] Данные RabbitMQ не установлены. Пропуск отправки.")
return
try:
credentials = pika.PlainCredentials(RABBITMQ_USER, RABBITMQ_PASS)
connection = pika.BlockingConnection(pika.ConnectionParameters(
host=RABBITMQ_HOST, credentials=credentials, heartbeat=600, blocked_connection_timeout=300
))
channel = connection.channel()
channel.exchange_declare(exchange=EXCHANGE_NAME, exchange_type='fanout')
with open(FILE_RESULT, 'r', encoding='utf-8') as fp:
channel.basic_publish(
exchange=EXCHANGE_NAME,
routing_key='',
properties=pika.BasicProperties(content_encoding='utf-8', content_type='application/json', delivery_mode=2),
body=fp.read()
)
log(f" [x] Данные опубликованы в RabbitMQ: {EXCHANGE_NAME}")
connection.close()
except Exception as mq_err:
log(f"[!!!] Ошибка RabbitMQ: {mq_err}")
def main(): def parser_loop():
log("=== Система Совместимости V1-V2 (Служба) запущена ===") log("=== Система Совместимости V1-V2 (Оптимизированная) запущена ===")
if PROXY_URL: os.makedirs(os.path.dirname(FILE_RESULT), exist_ok=True)
log(f"[*] Настроен прокси-сервер: {PROXY_URL}") if not GEMINI_API_KEY: log("[!] GEMINI_API_KEY не задан!")
if not GEMINI_API_KEY:
log("[!] ВНИМАНИЕ: GEMINI_API_KEY не задан. Интеллектуальный парсинг работать не будет!")
while True: while True:
try: try:
job_iteration() job_iteration()
except Exception as e: except Exception as e:
log(f"[!!!] Критическая ошибка на верхнем уровне: {e}") log(f"[!!!] Критическая ошибка: {e}")
traceback.print_exc() traceback.print_exc()
log(f"[*] Ожидание {POLL_INTERVAL} секунд перед следующей итерацией...\n") log(f"[*] Ожидание {POLL_INTERVAL} сек...\n")
time.sleep(POLL_INTERVAL) time.sleep(POLL_INTERVAL)
if __name__ == "__main__": if __name__ == "__main__":
main() threading.Thread(target=parser_loop, daemon=True).start()
uvicorn.run(app, host="0.0.0.0", port=HTTP_PORT)

View File

@@ -1,2 +1,5 @@
requests[socks]==2.31.0 requests[socks]==2.31.0
python-dotenv==1.0.1 python-dotenv==1.0.1
pika==1.3.2
fastapi==0.109.0
uvicorn==0.27.0