commit
This commit is contained in:
2
.env
2
.env
@@ -1,3 +1,3 @@
|
||||
GEMINI_API_KEY=AIzaSyDTFKJscB72NY7R-zeyZcmO0iTnJRoMusw
|
||||
PROXY_URL=socks5://127.0.0.1:2080
|
||||
PROXY_URL=socks5://127.0.0.1:2081
|
||||
POLL_INTERVAL=300
|
||||
@@ -15323,5 +15323,250 @@
|
||||
"subject": "МОДЕЛИРОВАНИЕ СИСТЕМ",
|
||||
"location": "В-403",
|
||||
"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
277
main.py
277
main.py
@@ -1,31 +1,52 @@
|
||||
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
|
||||
import uvicorn
|
||||
|
||||
# --- Загрузка переменных окружения ---
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
# -----------------------------------
|
||||
|
||||
# ================= КОНФИГУРАЦИЯ =================
|
||||
URL_PARSER_ROOT = "https://fazziclay.com/api/v1/vstu_schedule_parser_v2/parser.json"
|
||||
BASE_URL_FILES = "https://fazziclay.com/api/v1/vstu_schedule_parser_v2/parsed/"
|
||||
# ИЗМЕНЕНИЕ 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-latest"
|
||||
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))
|
||||
# =================================================
|
||||
|
||||
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):
|
||||
"""Логирование с принудительным сбросом буфера (важно для Docker)."""
|
||||
print(f"[{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}] {msg}", flush=True)
|
||||
|
||||
def get_raw_hash(raw_list):
|
||||
@@ -44,7 +65,6 @@ def save_json(filename, data):
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def merge_week_keys_metadata(sheets):
|
||||
"""Объединяет week_keys_metadata со всех листов, исключая дубликаты дней."""
|
||||
merged = {}
|
||||
for sheet_data in sheets.values():
|
||||
wkm = sheet_data.get("week_keys_metadata", {})
|
||||
@@ -53,8 +73,7 @@ def merge_week_keys_metadata(sheets):
|
||||
merged[day] = {}
|
||||
for month, days in months.items():
|
||||
if month not in merged[day]:
|
||||
merged[day][month] =[]
|
||||
# Добавляем даты, сохраняя порядок и избегая дублей
|
||||
merged[day][month] = []
|
||||
for d in days:
|
||||
if d not in merged[day][month]:
|
||||
merged[day][month].append(d)
|
||||
@@ -65,11 +84,7 @@ def ask_gemini(unknown_raws):
|
||||
log("[!] GEMINI_API_KEY не задан! ИИ-парсинг пропущен.")
|
||||
return {}
|
||||
|
||||
proxies = {}
|
||||
if PROXY_URL:
|
||||
log("Using proxy")
|
||||
proxies = {"http": PROXY_URL, "https": PROXY_URL}
|
||||
|
||||
proxies = {"http": PROXY_URL, "https": PROXY_URL} if PROXY_URL else {}
|
||||
results = {}
|
||||
items = list(unknown_raws.items())
|
||||
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}"
|
||||
payload = {
|
||||
"contents": [{
|
||||
"role": "user",
|
||||
"parts": [{"text": prompt}]
|
||||
}],
|
||||
"generationConfig": {
|
||||
"responseMimeType": "application/json" # ИСПРАВЛЕНИЕ: строго camelCase для REST API
|
||||
}
|
||||
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
|
||||
"generationConfig": {"responseMimeType": "application/json"}
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(url, json=payload, proxies=proxies, timeout=60)
|
||||
resp.raise_for_status() # Выкинет ошибку для 400, 403, 404 и т.д.
|
||||
resp.raise_for_status()
|
||||
|
||||
data = resp.json()
|
||||
text_response = data['candidates'][0]['content']['parts'][0]['text']
|
||||
@@ -113,132 +123,149 @@ def ask_gemini(unknown_raws):
|
||||
log(f"[+] Батч успешно обработан ИИ ({GEMINI_MODEL}).")
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
# ИСПРАВЛЕНИЕ: Читаем текст ответа от Google (там написана причина)
|
||||
error_body = e.response.text if e.response is not None else "Нет тела ответа"
|
||||
|
||||
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}")
|
||||
log(f"[!] HTTP Ошибка API Gemini ({e.response.status_code}): {error_body}")
|
||||
except Exception as e:
|
||||
log(f"[!] Ошибка обращения к API Gemini: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def job_iteration():
|
||||
log("--- Начало итерации обновления расписания ---")
|
||||
cache = load_json(FILE_CACHE, {})
|
||||
|
||||
# ШАГ 1: Единоразовая загрузка монолита
|
||||
log(f"[*] Скачивание all-in-one файла {URL_RESULT_V2} ...")
|
||||
try:
|
||||
v2_data = requests.get(URL_RESULT_V2, timeout=60).json()
|
||||
except Exception as e:
|
||||
log(f"[!!!] Ошибка скачивания базового файла расписаний: {e}")
|
||||
return
|
||||
|
||||
def fetch_and_build(cache):
|
||||
log("[*] Загрузка списка файлов парсера V2...")
|
||||
parser_data = requests.get(URL_PARSER_ROOT, timeout=30).json()
|
||||
items_to_process = v2_data.get("all_files", [])
|
||||
if not items_to_process:
|
||||
log("[!] Ключ 'all_files' пуст или отсутствует в result_v2.json")
|
||||
return
|
||||
|
||||
final_groups = {}
|
||||
excels_list =[]
|
||||
# ШАГ 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", {})
|
||||
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
|
||||
|
||||
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 file_info in parser_data.get("all_files",[]):
|
||||
file_url = f"{BASE_URL_FILES}{requests.utils.quote(file_info['json_represent'])}"
|
||||
log(f"[*] Обработка файла: {file_info['json_represent']}")
|
||||
# ШАГ 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", {})
|
||||
|
||||
try:
|
||||
faculty_data = requests.get(file_url, timeout=30).json()
|
||||
except Exception as e:
|
||||
log(f"[!] Ошибка скачивания {file_url}: {e}")
|
||||
continue
|
||||
file_ds_hash = excel_info.get("sha1hash", "UNKNOWN")
|
||||
|
||||
file_ds_hash = faculty_data.get("sha1hash", faculty_data.get("excel", {}).get("sha1hash", "UNKNOWN"))
|
||||
sheets = faculty_data.get("sheets", {})
|
||||
|
||||
# --- СБОРКА ОБЪЕКТА EXCEL (Обновлено) ---
|
||||
# Сборка метаданных экселя
|
||||
excel_meta = {
|
||||
"data_source_hash": file_ds_hash,
|
||||
"week_keys_metadata": merge_week_keys_metadata(sheets)
|
||||
}
|
||||
# Переносим остальные корневые ключи из оригинального json эксельки (за исключением уже обработанных)
|
||||
for k, v in faculty_data.items():
|
||||
if k not in ["sheets", "data_source_hash", "week_keys_metadata"]:
|
||||
for k, v in excel_info.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 group_id, group_data in groups.items():
|
||||
if group_data["name"] not in final_groups:
|
||||
final_groups[group_data["name"]] = {
|
||||
"name": group_data["name"],
|
||||
"facultet": faculty_data.get('excel', {}).get('facultet', 'Неизвестно'),
|
||||
group_name = group_data["name"]
|
||||
|
||||
if group_name not in final_groups:
|
||||
final_groups[group_name] = {
|
||||
"name": group_name,
|
||||
"facultet": excel_info.get('facultet', 'Неизвестно'),
|
||||
"position": group_data.get("position"),
|
||||
"position_human": group_data.get("position_human"),
|
||||
"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", {})
|
||||
for slot_key, pair_value in slots.items():
|
||||
if not isinstance(pair_value, dict): continue
|
||||
|
||||
if slot_key not in final_groups[group_data["name"]]["slots"]:
|
||||
final_groups[group_data["name"]]["slots"][slot_key] = {}
|
||||
if slot_key not in final_groups[group_name]["slots"]:
|
||||
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():
|
||||
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):
|
||||
current_pair_id = pair_key if i == 0 else f"{pair_key}_{i}"
|
||||
for event in events:
|
||||
if not isinstance(event, dict): continue
|
||||
|
||||
raw_list = event.get("raw",[])
|
||||
# Логика склейки дубликатов (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)
|
||||
|
||||
if r_hash in cache:
|
||||
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 []
|
||||
leads =[l.strip() for l in res.get("teacher", "").split(",")] if res.get("teacher") and res.get("teacher") not in["Не указана", "Не указан"] else []
|
||||
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_data["name"]]["slots"][slot_key][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")
|
||||
}
|
||||
else:
|
||||
unknown_raws[r_hash] = raw_list
|
||||
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")
|
||||
}
|
||||
|
||||
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 = {
|
||||
"version": 1,
|
||||
"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)
|
||||
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():
|
||||
log("=== Система Совместимости V1-V2 (Служба) запущена ===")
|
||||
if PROXY_URL:
|
||||
log(f"[*] Настроен прокси-сервер: {PROXY_URL}")
|
||||
if not GEMINI_API_KEY:
|
||||
log("[!] ВНИМАНИЕ: GEMINI_API_KEY не задан. Интеллектуальный парсинг работать не будет!")
|
||||
def parser_loop():
|
||||
log("=== Система Совместимости V1-V2 (Оптимизированная) запущена ===")
|
||||
os.makedirs(os.path.dirname(FILE_RESULT), exist_ok=True)
|
||||
if not GEMINI_API_KEY: log("[!] GEMINI_API_KEY не задан!")
|
||||
|
||||
while True:
|
||||
try:
|
||||
job_iteration()
|
||||
except Exception as e:
|
||||
log(f"[!!!] Критическая ошибка на верхнем уровне: {e}")
|
||||
log(f"[!!!] Критическая ошибка: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
log(f"[*] Ожидание {POLL_INTERVAL} секунд перед следующей итерацией...\n")
|
||||
log(f"[*] Ожидание {POLL_INTERVAL} сек...\n")
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
threading.Thread(target=parser_loop, daemon=True).start()
|
||||
uvicorn.run(app, host="0.0.0.0", port=HTTP_PORT)
|
||||
@@ -1,2 +1,5 @@
|
||||
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
|
||||
Reference in New Issue
Block a user