Инженерный кейс: Превращение сырого PDF колледжа в PWA-экосистему с форумом и Push-уведомлениями
Инженерный кейс: Превращение сырого PDF колледжа в PWA-экосистему с форумом
В академической среде студенческий опыт (User Experience) часто разбивается о консервативную IT-инфраструктуру. В Высшем колледже информатики НГУ ключевой источник оперативных данных — расписание занятий — годами публиковался в виде неструктурированных, формируемых вручную многостраничных PDF-документов.
В данном материале представлен технический разбор разработки автономного сервиса: от преодоления фундаментальных проблем Data Extraction и Reverse Engineering до проектирования реактивного PWA-клиента, масштабируемого бэкенда на FastAPI и полноценного студенческого комьюнити-хаба.
1. Проблематика и архитектурный контекст
С точки зрения продуктового анализа исходная схема распространения данных страдала критическими UX-недостатками:
- Отсутствие машиночитаемого API: Официальный портал не предоставлял открытых эндпоинтов для интеграции.
- Высокий Friction при доступе: Студентам требовалось ежедневно загружать многостраничные файлы, вручную масштабировать сетку и выискивать номер группы среди сотен ячеек.
- Нулевая реактивность: При внесении экстренных правок в расписание (замена преподавателя, перенос аудитории) пользователи узнавали об этом постфактум.
Цель проекта: разработать автономный, высокопроизводительный Fullstack-сервис, автоматически преобразующий сырые данные в типизированные API-структуры с мгновенной доставкой до конечных мобильных и десктопных клиентов.
2. Инженерия данных: Реверс-инжиниринг и кастомный парсинг PDF
Парсинг табличных данных из PDF — одна из наиболее нетривиальных задач из-за отсутствия в стандарте PDF семантических понятий «строка», «колонка» или «таблица». Это всего лишь набор координат отрисовки векторов и текстовых глифов.
Архитектурный тупик legacy-решений
На начальном этапе исследовались существующие Open Source решения, включая проект бота VkiHub от разработчика DedMaxTech. Однако очередной семестровый апдейт верстки со стороны учебной части колледжа привел к полному рассинхрону структур данных:
- Нестабильные Bounding Boxes: Смещение базовых линий колонок приводило к контаминации ячеек соседних групп.
- Динамические Merged Cells: Объединение ячеек на уровне подгрупп, потоковых лекций и числителей/знаменателей ломало детерминированные матрицы.
- Гетерогенный формат сущностей: Произвольные написания ФИО (
Иванов И.И.,Иванов И. И,Иванов-Петров А.), вариативные обозначения корпусов (ГК,КПА, аудитории главного корпуса НГУ) и служебные маркеры («консультация», «дистанционно», «зачет»).
Полагаться на внешние решения без прямого контроля логики было невозможно: малейшее изменение исходного шаблона приводило к cascade failures всего пайплайна.
Проектирование собственного Core-парсера с нуля
В новой версии бэкенда (services/parser.py) внешние зависимости были полностью устранены в пользу собственного алгоритмического пайплайна:
[ PDF с портала ]
│
▼ (Camelot Lattice/Stream Mode)
[ Сырая табличная матрица ]
│
▼ (find_header_row & is_group Regex)
[ Нормализация заголовков групп ]
│
▼ (Heuristic Cell Parsing & Entity Extraction)
[ Типизированный JSON-Payload ]
│
▼ (Inverted Index Construction)
[ Индексы: Группы / Преподаватели / Аудитории ]
Ключевые инженерные решения, заложенные в ядро парсера:
- Динамический Header Search: Алгоритм
find_header_rowсканирует верхние строки матрицы с помощью строгих регулярных выражений (^[А-ЯA-Z]\d{4}[а-яa-z]{1,2}\d$), автоматически определяя динамическую границу между метаданными документа и сеткой расписания. - Эвристический анализ ячеек (
parse_cell): Комплексный парсер текстового блока производит послойную деструктуризацию:- Изоляция сущности преподавателя с нормализацией инициалов:
extract_teacher. - Извлечение номера аудитории и локации корпуса:
extract_room. - Классификация типа занятия (
isLecture,isPractice,isExam,isConsultation,isCanceled). - Очистка от дублирующихся токенов и шума (
remove_duplicate_words).
- Изоляция сущности преподавателя с нормализацией инициалов:
def parse_cell(self, text: str):
if not text or not text.strip():
return None
text = self.clean_text(text)
teacher = self.extract_teacher(text)
classroom = self.extract_room(text)
isLecture = 'лекци' in text.lower() or 'диста' in text.lower()
lesson_text = self.extract_subject(text)
lesson = self.normalize_lesson(self.remove_duplicate_words(lesson_text))
if not lesson:
return None
formatted_lesson = (lesson[0].upper() + lesson[1:]) if lesson.lower() == lesson else lesson
if classroom:
formatted_lesson = formatted_lesson.replace(classroom, '')
isIssusing = ((not teacher or not classroom) and (not isLecture or (isLecture and 'нет' in text.lower()))) or 'выставле' in text.lower()
return {
'line': self.remove_duplicate_words(text) if isIssusing else text,
'lesson': formatted_lesson,
'teacher': teacher,
'classroom': classroom,
'isLecture': isLecture,
'isPractice': 'практик' in text.lower(),
'isExam': 'экзам' in text.lower(),
'isIssusing': isIssusing,
'isConsultation': 'консул' in text.lower(),
'isCanceled': 'отмен' in text.lower()
}
- Построение инвертированных индексов (Inverted Index): На лету создаются вторичные проекции данных — расписание по преподавателям (
/schedule/teachers) и по аудиториям (/schedule/classrooms), что позволило за O(1) рассчитывать занятость любого кабинета в заданный временной слот.
3. Архитектура сервиса и стек технологий
Архитектура системы строилась по принципу модульного разделения ответственности и максимальной оптимизации Time-to-Interactive (TTI).
| Уровень системы | Стек и инструменты | Архитектурная роль |
|---|---|---|
| Backend Core | Python 3.12, FastAPI, Tortoise ORM | Асинхронная обработка запросов, фоновый воркер синхронизации, транзакционная целостность |
| Database | PostgreSQL | Хранение профилей, сессий, тредов форума, подписок на уведомления |
| Frontend App | React 19, TypeScript, Vite | Реактивный UI, Single Page Application, клиентский роутинг и кэширование |
| PWA & Offline | Service Workers, Web App Manifest | Фоновое кэширование ассетов, установка в роли нативного приложения, оффлайн-доступ |
| Styling & UI | Модульный SCSS, Custom Design System | Легковесная компонентная дизайн-система без оверхеда сторонних UI-библиотек |
| Push Gateway | Web Push Protocol, VAPID, pywebpush | Доставка пуш-уведомлений об апдейтах расписания на системном уровне ОС |
4. Продуктовое масштабирование: Рождение студенческого форума
Изначально сервис проектировался как утилитарный Schedule Viewer. Однако в ходе совместной работы с дизайнером проекта открылся очевидный потенциал: объединить ежедневный органический трафик студентов вокруг общего коммуникационного хаба.
Проектирование UGC-подсистемы (User Generated Content)
Форум был спроектирован с упором на минимальный time-to-value:
- Разделение контуров: Авторизация через сессионные токены, роли пользователей и инструменты модерации.
- Иерархическая структура: Тематические категории, посты с тегами, вложенные ветки комментариев и интерактивные реакции.
- Оптимизация мобильного UX: Адаптивные диалоги создания тем, оптимистичные UI-апдейты при отправке сообщений, плавные микровзаимодействия.
5. Ключевые возможности платформы
На сегодняшний день платформа представляет собой готовую экосистему для учащихся и преподавателей:
-
Мониторинг занятости аудиторий в Real-Time: Благодаря инвертированному индексу аудиторий студенты могут в один клик найти свободные кабинеты для самоподготовки или командной работы.
-
Event-Driven Push-уведомления: Фоновый планировщик (
services/request.py) производит периодический polling официального сайта. При обнаружении изменений для целевой группы или преподавателя воркер мгновенно рассылает Web Push уведомления подписанным пользователям. -
Offline-First PWA Архитектура: Приложение сохраняет работоспособность при нестабильной сотовой связи в аудиториях колледжа благодаря прогрессивному кэшированию Service Worker.
-
Интегрированный студенческий форум: Единая точка коммуникации для обмена конспектами, поиска сокомандников и обсуждения учебных вопросов.
6. Инфраструктурные вызовы и технические выводы
В процессе разработки и эксплуатации был решен ряд технических вызовов:
- Resilience официального источника: Периодические сетевые сбои сайта колледжа потребовали внедрения Fallback-кэширования последних валидных версий расписания.
- Постоянная эволюция входного формата: Отказ от сторонних библиотек в пользу собственного алгоритма нормализации ячеек снизил процент ошибок парсинга до статистически незначимых величин.
Итоги
Проект стал наглядным примером того, как глубокий технический подход, устранение сторонних зависимостей и фокус на реальных болях пользователей позволяют в связке разработчика и дизайнера создать высоконагруженный студенческий продукт для целого колледжа.