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: Константы --- # --- Шаг 0: Константы ---
POSITIVE_KEYWORDS = ['зал', 'ауд', 'каб', 'корп', 'кор.'] POSITIVE_KEYWORDS = ['зал', 'ауд', 'каб', 'корп', 'кор.', "ЛК"]
NEGATIVE_KEYWORDS = ['доц', 'проф', 'асс', 'лек', 'пр'] NEGATIVE_KEYWORDS = ['доц', 'проф', 'асс', 'лек', 'пр']
# Транслитерация для унификации # Транслитерация для унификации
@@ -148,3 +148,51 @@ def is_room_number(s: str) -> bool:
return False 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 json
import re
import time
import traceback
from urllib.parse import urljoin
import pandas as pd
import xlwt
import xlrd import xlrd
import requests
from requests.structures import CaseInsensitiveDict
from bs4 import BeautifulSoup
import aigenerated
import parser import parser
import utils import utils
import json import json
# Общее правило проекта, сначала в координатах идёт ROW а потом COL, нумерация с нуля # Общее правило проекта, сначала в координатах идёт 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) session = requests.Session()
print("The number of worksheets is {0}".format(book.nsheets)) session.headers = CaseInsensitiveDict(
print("Worksheet name(s): {0}".format(book.sheet_names())) {
sh = book.sheet_by_index(0) "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) result = {}
prs.parse() 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) try:
print("Saved to groups.json") 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 from coord import Coord, Merged
import utils import utils
LOGGING = True
def pprint(*args, **kwargs):
if LOGGING:
print(*args, **kwargs)
class Parser: class Parser:
@@ -14,27 +19,32 @@ class Parser:
self.groups = {} self.groups = {}
self.teachers = set() self.teachers = set()
self.places = 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): def parse(self):
monday = utils.find(self.sh, "ПОНЕДЕЛЬНИК") monday = utils.find(self.sh, "ПОНЕДЕЛЬНИК")
if monday is None:
print(" -- Failed parse! -- ")
print("ПОНЕДЕЛЬНИК НЕ НАЙДЕН!")
return
head_rx = monday[0] - 1 # выше первого понидельника head_rx = monday[0] - 1 # выше первого понидельника
if head_rx < 0: if head_rx < 0:
raise Exception("head_rx < 0: Программа пыталась найти 'ПОНЕДЕЛЬНИК', но по всей видимости не нашла.") raise Exception("head_rx < 0: Программа пыталась найти 'ПОНЕДЕЛЬНИК', но по всей видимости не нашла.")
head = self.sh.row(head_rx) # get all ROW (months, groups) 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 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(): for group in self.groups.values():
print("\nSTART OF PROCESS GROUP\n") pprint("\nSTART OF PROCESS GROUP\n")
self.process_group(group, monday) 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): def parse_potokoviy(self, merged: Merged):
speaker = None speaker = None
@@ -48,16 +58,16 @@ class Parser:
# location # location
location = merged.high.shift(down=1).cell(self.sh).value 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): def process_group(self, group, monday):
""" """
Обработать группы, выполняется для каждой группы, после того как они распарены (parse_groups) Обработать группы, выполняется для каждой группы, после того как они распарены (parse_groups)
group = {'name': 'ИВТ-260', 'position': [5, 6], 'position_human': 'G6:J6'} 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'] group_name = group['name']
print(group_name) pprint(group_name)
row = group['position'][0] + 1 # counter for while, +1 for shift down; также номер строки в таблице (вроде с нуля) row = group['position'][0] + 1 # counter for while, +1 for shift down; также номер строки в таблице (вроде с нуля)
weeknum = 1 # номер недели, щёлкнет +1 при каком-то условии. weeknum = 1 # номер недели, щёлкнет +1 при каком-то условии.
while row < self.sh.nrows: # maybe условие чтобы не уйти ниже чем есть строк while row < self.sh.nrows: # maybe условие чтобы не уйти ниже чем есть строк
@@ -71,7 +81,7 @@ class Parser:
cv = merged_cell.value cv = merged_cell.value
# В конце (12 пара:>) название группы, можно использовать как якорь # В конце (12 пара:>) название группы, можно использовать как якорь
if utils.unspace(cv) == group_name: if utils.unspace(cv) == group_name:
print("Lesson == group name; ending group loop.") pprint("Lesson == group name; ending group loop.")
break break
weekday_mr = utils.get_merged_coord(self.sh, weekday_pos) weekday_mr = utils.get_merged_coord(self.sh, weekday_pos)
@@ -83,7 +93,7 @@ class Parser:
if weekday == "": if weekday == "":
if weeknum == 1: if weeknum == 1:
weeknum += 1 weeknum += 1
print("------") pprint("------")
skip = 1 skip = 1
row += 1 row += 1
else: else:
@@ -104,17 +114,20 @@ class Parser:
dispname = "<no lesson>" dispname = "<no lesson>"
if not is_empty_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: if is_wide_maybe_potokoviy:
ret = self.parse_potokoviy(merged) ret = self.parse_potokoviy(merged)
parsed_location = ret['loc'] parsed_location = ret['loc']
parsed_leader = ret['leader'] parsed_leader = ret['leader']
parsed_discipline_name = ret['name'] parsed_discipline_name = ret['name']
else: parsed_uncotigorized = list(utils.parse_all_dirt(self.sh, merged.low, merged.width(), next))
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
else:
if (is_solid): if (is_solid):
parsed_discipline_name = cv parsed_discipline_name = cv
@@ -128,7 +141,7 @@ class Parser:
if parsed_leader: dispname += f" [{parsed_leader}]" if parsed_leader: dispname += f" [{parsed_leader}]"
if parsed_location: dispname += f" [{parsed_location}]" if parsed_location: dispname += f" [{parsed_location}]"
dispname = dispname.replace("\n", "\\n") 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() prepods = set()
@@ -187,14 +200,14 @@ def parse_groups(sh, head, monday, head_rx):
i = 0 i = 0
while i < len(head): while i < len(head):
x = head[i] 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)) merged = utils.get_merged_coord(sh, Coord(head_rx, i))
if i > monday[1] + 1: if i > monday[1] + 1:
if merged is None or x.value == "": if merged is None or x.value == "":
break break
if merged.width() != 4: 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 break
name = utils.unspace(x.value) 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): def border_top(sh, cell):
return border(sh, cell).top_line_style 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): def parse_all_dirt(sh, min_pos, right, down):
RET = set() RET = set()