work copy

This commit is contained in:
2025-09-11 14:16:38 +03:00
parent b62640e39b
commit 414907a929
8 changed files with 88146 additions and 664 deletions

View File

@@ -90,7 +90,7 @@ def extract_last_name(name_str: str) -> str or None:
# --- Шаг 0: Константы ---
POSITIVE_KEYWORDS = ['зал', 'ауд', 'каб', 'корп', 'кор.']
POSITIVE_KEYWORDS = ['зал', 'ауд', 'каб', 'корп', 'кор.', "ЛК"]
NEGATIVE_KEYWORDS = ['доц', 'проф', 'асс', 'лек', 'пр']
# Транслитерация для унификации
@@ -106,7 +106,7 @@ def is_room_number(s: str) -> bool:
if ',' in s:
return False # 2. Проверка на запятые (даты)
# 3. Проверка на "очевидный мусор"
first_word = s.strip().lower().split()[0]
if first_word in NEGATIVE_KEYWORDS:
@@ -148,3 +148,51 @@ def is_room_number(s: str) -> bool:
return False
import requests
from urllib.parse import urlsplit, urlunsplit, quote
def download_file_from_url(url, output_filename):
"""
Скачивает файл по URL со спецсимволами и пробелами, сохраняя его под указанным именем.
Args:
url (str): Исходный URL, который может содержать пробелы и кириллицу.
output_filename (str): Имя файла для сохранения (например, 'calc.xls').
"""
try:
# --- Шаг 1: Правильное кодирование URL ---
# Разбираем URL на части: ('https', 'www.vstu.ru', '/path/to file.xls', '', '')
parts = urlsplit(url)
# Кодируем только путь, оставляя слэши '/' безопасными
# Это превратит ' ' в '%20', 'В' в '%D0%92' и т.д.
encoded_path = quote(parts.path, safe='/-_')
# Собираем URL обратно из частей с уже закодированным путем
encoded_url = urlunsplit((parts.scheme, parts.netloc, encoded_path, parts.query, parts.fragment))
# --- Шаг 2: Скачивание файла ---
response = requests.get(encoded_url, stream=True)
# Проверяем, успешен ли запрос (код 200 OK)
# Если сервер вернет ошибку (404, 500 и т.д.), здесь возникнет исключение
response.raise_for_status()
# --- Шаг 3: Сохранение файла ---
# Открываем файл для записи в бинарном режиме ('wb')
# Использование 'with' гарантирует, что файл будет закрыт автоматически
with open(output_filename, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"✅ Файл успешно скачан и сохранен как '{output_filename}'")
except requests.exceptions.RequestException as e:
print(f"❌ Ошибка скачивания: {e}")
except Exception as e:
print(f"❌ Произошла непредвиденная ошибка: {e}")

28
checkenv.py Normal file
View File

@@ -0,0 +1,28 @@
import sys
import os
print("--- ДИАГНОСТИКА ОКРУЖЕНИЯ ---")
print(f"Исполняемый файл Python: {sys.executable}")
print("-" * 20)
try:
import xlwt
print("✅ Библиотека 'xlwt' успешно импортирована.")
print(f" Версия: {xlwt.__VERSION__}")
# Попробуем найти, где она лежит
print(f" Расположение: {os.path.dirname(xlwt.__file__)}")
except ImportError:
print("❌ ОШИБКА: Библиотека 'xlwt' НЕ НАЙДЕНА в этом окружении.")
try:
import pandas
print("✅ Библиотека 'pandas' успешно импортирована.")
print(f" Версия: {pandas.__version__}")
# Попробуем найти, где она лежит
print(f" Расположение: {os.path.dirname(pandas.__file__)}")
except ImportError:
print("❌ ОШИБКА: Библиотека 'pandas' НЕ НАЙДЕНА в этом окружении.")
print("-" * 20)

Binary file not shown.

File diff suppressed because it is too large Load Diff

141
main.py
View File

@@ -1,21 +1,146 @@
import json
import re
import time
import traceback
from urllib.parse import urljoin
import pandas as pd
import xlwt
import xlrd
import requests
from requests.structures import CaseInsensitiveDict
from bs4 import BeautifulSoup
import aigenerated
import parser
import utils
import json
# Общее правило проекта, сначала в координатах идёт ROW а потом COL, нумерация с нуля
FACULTETS = [
"asp", "mag", "fastiv", "fat", "ftkm", "ftpp", "feu", "fevt", "htf", "vkf", "mmf", "fpik"
]
BASE_URL = "https://www.vstu.ru/"
RASP_PREFIX = "https://www.vstu.ru/student/raspisaniya/zanyatiy/index.php?dep="
book = xlrd.open_workbook("ОН_ФЭВТ_2 курс.xls", formatting_info=True)
print("The number of worksheets is {0}".format(book.nsheets))
print("Worksheet name(s): {0}".format(book.sheet_names()))
sh = book.sheet_by_index(0)
session = requests.Session()
session.headers = CaseInsensitiveDict(
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
"Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"Referer": "http://dump.vstu.ru/",
"Upgrade-Insecure-Requests": "1",
"Priority": "u=0, i",
"Pragma": "no-cache",
"Cache-Control": "no-cach",
}
)
EXCEL_LINKS = {}
filestime = str(round(time.time()))
for facultet in FACULTETS:
url = RASP_PREFIX + facultet
r = session.get(url)
print(f"GET {url}")
soup = BeautifulSoup(r.text, 'html.parser')
excel_pattern = re.compile(r'\.xlsx?$')
# Ищем все теги <a>, у которых атрибут href соответствует нашему паттерну
excel_tags = soup.find_all('a', href=excel_pattern)
excel_links = [tag.get('href') for tag in excel_tags]
# Предположим, вы уже получили excel_links из одного из методов выше
# excel_links = ['../../../upload/raspisanie/z/ОН_ХТФ_1 курс.xlsx', ...]
absolute_links = [urljoin(BASE_URL, relative_link) for relative_link in excel_links]
if facultet not in EXCEL_LINKS.keys():
EXCEL_LINKS[facultet] = set()
for excel_url in absolute_links:
EXCEL_LINKS[facultet].add(excel_url)
print(f"+url {excel_url}")
prs = parser.Parser(sh)
prs.parse()
result = {}
faileds = []
counter = 0
for facultet in FACULTETS:
counter += 1000
print(f"\n\n-- Факультет '{facultet}' --")
facultet_urls = EXCEL_LINKS[facultet]
for excel_url in facultet_urls:
counter += 1
print(f"\n\n-- Ссылка --")
print(f"{excel_url}")
xlsx = excel_url.endswith(".xlsx")
json.dump(prs.groups, open('groups.json', 'w'), indent=2, ensure_ascii=False)
print("Saved to groups.json")
try:
filename = "excels/" + facultet + filestime + f"[C{counter}]" + ".xls"
# Download a file
if not xlsx:
aigenerated.download_file_from_url(excel_url, filename)
else:
aigenerated.download_file_from_url(excel_url, filename+"x")
excel_file = pd.ExcelFile(filename + "x")
# Создаем "писателя" для формата .xls с помощью движка xlwt
# Использование 'with' гарантирует, что файл будет корректно сохранен и закрыт
with pd.ExcelWriter(filename, engine='xlwt') as writer:
print("Начинаю конвертацию...")
# Проходим по каждому листу в исходном файле
for sheet_name in excel_file.sheet_names:
print(f" - Обрабатываю лист: {sheet_name}")
# Читаем лист в DataFrame
df = excel_file.parse(sheet_name)
# Записываем этот DataFrame в новый .xls файл с тем же именем листа
# index=False чтобы не добавлять лишнюю колонку с индексами pandas
df.to_excel(writer, sheet_name=sheet_name, index=False)
print(f"✅ Успешно! Файл конвертирован как: {filename}")
book = xlrd.open_workbook(filename, formatting_info=True)
print("The number of worksheets is {0}".format(book.nsheets))
print("Worksheet name(s): {0}".format(book.sheet_names()))
sh = book.sheet_by_index(0)
parser.LOGGING = False
prs = parser.Parser(sh)
prs.parse()
for group_name in prs.groups.keys():
if group_name in result.keys():
print(f" -- WTF -- Doubled groups -- name: {group_name}")
continue
gr = result[group_name] = prs.groups[group_name]
gr['facultet'] = facultet
gr['data_source'] = excel_url.split("/")[-1] + " SHEET: " + str(sh.name)
gr['parser_debug'] = {
"C_COUNTER": counter,
"timestime": filestime,
"excel_url": excel_url,
"sheet": sh.name,
"filename": filename
}
except Exception as e:
print(f"Error while {excel_url}")
print(e)
traceback.print_exc()
faileds.append({
"ex": e,
"fac": facultet,
"url": excel_url
})
json.dump(result, open('result.json', 'w'), indent=2, ensure_ascii=False)
print("Faileds:")
print(faileds)
print("Saved to result.json")

View File

@@ -6,6 +6,11 @@ import aigenerated
from coord import Coord, Merged
import utils
LOGGING = True
def pprint(*args, **kwargs):
if LOGGING:
print(*args, **kwargs)
class Parser:
@@ -14,27 +19,32 @@ class Parser:
self.groups = {}
self.teachers = set()
self.places = set()
print("Parser created for '{0}': size: {1}x{2}".format(self.sh.name, self.sh.nrows, self.sh.ncols))
pprint("Parser created for '{0}': size: {1}x{2}".format(self.sh.name, self.sh.nrows, self.sh.ncols))
def parse(self):
monday = utils.find(self.sh, "ПОНЕДЕЛЬНИК")
if monday is None:
print(" -- Failed parse! -- ")
print("ПОНЕДЕЛЬНИК НЕ НАЙДЕН!")
return
head_rx = monday[0] - 1 # выше первого понидельника
if head_rx < 0:
raise Exception("head_rx < 0: Программа пыталась найти 'ПОНЕДЕЛЬНИК', но по всей видимости не нашла.")
head = self.sh.row(head_rx) # get all ROW (months, groups)
print(f"head={head}")
pprint(f"head={head}")
self.groups = parse_groups(self.sh, head, monday, head_rx) # parse groups to self.groups
print(f'self.groups={json.dumps(self.groups, indent=2, ensure_ascii=False)}')
pprint(f'self.groups={json.dumps(self.groups, indent=2, ensure_ascii=False)}')
print("\n\n\n")
pprint("\n\n\n")
for group in self.groups.values():
print("\nSTART OF PROCESS GROUP\n")
pprint("\nSTART OF PROCESS GROUP\n")
self.process_group(group, monday)
print("\nEND OF PROCESS GROUP\n")
pprint("\nEND OF PROCESS GROUP\n")
print(self.teachers)
pprint(self.teachers)
def parse_potokoviy(self, merged: Merged):
speaker = None
@@ -48,16 +58,16 @@ class Parser:
# location
location = merged.high.shift(down=1).cell(self.sh).value
return {"loc": location, "leader": speaker, "name": merged.cell(self.sh).value}
return {"loc": str(location), "leader": str(speaker), "name": str(merged.cell(self.sh).value)}
def process_group(self, group, monday):
"""
Обработать группы, выполняется для каждой группы, после того как они распарены (parse_groups)
group = {'name': 'ИВТ-260', 'position': [5, 6], 'position_human': 'G6:J6'}
"""
print(f"process_group group={group}")
pprint(f"process_group group={group}")
group_name = group['name']
print(group_name)
pprint(group_name)
row = group['position'][0] + 1 # counter for while, +1 for shift down; также номер строки в таблице (вроде с нуля)
weeknum = 1 # номер недели, щёлкнет +1 при каком-то условии.
while row < self.sh.nrows: # maybe условие чтобы не уйти ниже чем есть строк
@@ -71,7 +81,7 @@ class Parser:
cv = merged_cell.value
# В конце (12 пара:>) название группы, можно использовать как якорь
if utils.unspace(cv) == group_name:
print("Lesson == group name; ending group loop.")
pprint("Lesson == group name; ending group loop.")
break
weekday_mr = utils.get_merged_coord(self.sh, weekday_pos)
@@ -83,7 +93,7 @@ class Parser:
if weekday == "":
if weeknum == 1:
weeknum += 1
print("------")
pprint("------")
skip = 1
row += 1
else:
@@ -104,17 +114,20 @@ class Parser:
dispname = "<no lesson>"
if not is_empty_lesson:
may_prepod = merged.low.shift(down=2)
if utils.has_no_bottom_border(self.sh, may_prepod):
next = 6
is_2pair = True
if is_wide_maybe_potokoviy:
ret = self.parse_potokoviy(merged)
parsed_location = ret['loc']
parsed_leader = ret['leader']
parsed_discipline_name = ret['name']
parsed_uncotigorized = list(utils.parse_all_dirt(self.sh, merged.low, merged.width(), next))
else:
may_prepod = merged.low.shift(down=2)
if utils.border_bottom(self.sh, may_prepod) == 0 and utils.border_top(self.sh, may_prepod.shift(down=1)) == 0:
next = 6
is_2pair = True
if (is_solid):
parsed_discipline_name = cv
@@ -128,7 +141,7 @@ class Parser:
if parsed_leader: dispname += f" [{parsed_leader}]"
if parsed_location: dispname += f" [{parsed_location}]"
dispname = dispname.replace("\n", "\\n")
print(f"[{group_name}] row={row}; {pos} {pos_right} {pair} {weekday}: {'[ПОТОКОВЫЙ] ' if is_wide_maybe_potokoviy else ''}{dispname} {parsed_uncotigorized}")
pprint(f"[{group_name}] row={row}; {pos} {pos_right} {pair} {weekday}: {'[ПОТОКОВЫЙ] ' if is_wide_maybe_potokoviy else ''}{dispname} {parsed_uncotigorized}")
# пытаемся из некотегорезированных данных выцепить место и лидера (препода)
prepods = set()
@@ -187,14 +200,14 @@ def parse_groups(sh, head, monday, head_rx):
i = 0
while i < len(head):
x = head[i]
print(f"while i={i} head[i]={x}")
pprint(f"while i={i} head[i]={x}")
merged = utils.get_merged_coord(sh, Coord(head_rx, i))
if i > monday[1] + 1:
if merged is None or x.value == "":
break
if merged.width() != 4:
print(f"WARNING: group header witdh !=4 (found: {merged.width()}); blocks !=4 not supported by parser.")
pprint(f"WARNING: group header witdh !=4 (found: {merged.width()}); blocks !=4 not supported by parser.")
break
name = utils.unspace(x.value)

87265
result.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,9 @@ def border_bottom(sh, cell):
def border_top(sh, cell):
return border(sh, cell).top_line_style
def has_no_bottom_border(sh, coord):
return border_bottom(sh, coord) == 0 and border_top(sh, coord.shift(down=1)) == 0
def parse_all_dirt(sh, min_pos, right, down):
RET = set()