import requests from bs4 import BeautifulSoup import re import json import os import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def parse_program_page(url, program_id): """Парсинг страницы программы""" try: logger.info(f'Парсинг страницы {program_id}: {url}') response = requests.get(url, timeout=10) response.raise_for_status() soup = BeautifulSoup(response.content, 'html.parser') # Ищем заголовок title = soup.find('h1') title_text = title.get_text().strip() if title else f'Программа {program_id}' # Ищем описание description = soup.find('div', class_='description') or soup.find('p') desc_text = description.get_text().strip() if description else f'Описание программы {program_id}' # Ищем ссылки на PDF pdf_links = [] for link in soup.find_all('a', href=True): href = link['href'] if '.pdf' in href.lower() or 'curriculum' in href.lower() or 'plan' in href.lower(): if href.startswith('/'): href = 'https://abit.itmo.ru' + href elif not href.startswith('http'): href = 'https://abit.itmo.ru/' + href pdf_links.append(href) logger.info(f'Найдено {len(pdf_links)} PDF ссылок для {program_id}') return { 'title': title_text, 'description': desc_text, 'pdf_links': pdf_links, 'source_url': url } except Exception as e: logger.error(f'Ошибка парсинга страницы {program_id}: {e}') return { 'title': f'Программа {program_id}', 'description': f'Описание программы {program_id}', 'pdf_links': [], 'source_url': url } def parse_pdf(url, program_id): """Парсинг PDF файла с учебным планом""" try: logger.info(f'Попытка парсинга PDF: {url}') # Пока используем заглушку, так как PDF парсинг сложен # В реальной реализации здесь был бы код для извлечения таблиц из PDF # Возвращаем пустой список, чтобы не ломать приложение return [] except Exception as e: logger.error(f'Ошибка парсинга PDF {url}: {e}') return [] def normalize_course(course_data, program_id): """Нормализация данных курса""" # Создаем short_desc из названия если нет if 'short_desc' not in course_data: course_data['short_desc'] = course_data.get('name', '')[:200] # Генерируем теги на основе названия и описания text = f"{course_data.get('name', '')} {course_data.get('short_desc', '')}".lower() tags = [] if any(word in text for word in ['машинное обучение', 'ml', 'machine learning']): tags.append('ml') if any(word in text for word in ['глубокое обучение', 'dl', 'neural', 'нейрон']): tags.append('dl') if any(word in text for word in ['nlp', 'язык', 'текст', 'natural language']): tags.append('nlp') if any(word in text for word in ['зрение', 'vision', 'image', 'изображение']): tags.append('cv') if any(word in text for word in ['продукт', 'product', 'менеджмент', 'management']): tags.append('product') if any(word in text for word in ['бизнес', 'business', 'аналитика', 'analytics']): tags.append('business') if any(word in text for word in ['исследование', 'research', 'наука']): tags.append('research') if any(word in text for word in ['данные', 'data', 'статистика']): tags.append('data') if any(word in text for word in ['системы', 'systems', 'архитектура']): tags.append('systems') if any(word in text for word in ['python', 'программирование']): tags.append('python') if any(word in text for word in ['математика', 'math', 'статистика', 'оптимизация']): tags.append('math') course_data['tags'] = tags course_data['program_id'] = program_id return course_data def get_fallback_courses(): """Fallback курсы на случай недоступности парсинга""" return [ # Программа ИИ { 'id': 'ai_1_1', 'program_id': 'ai', 'semester': 1, 'name': 'Машинное обучение', 'credits': 6, 'hours': 108, 'type': 'required', 'short_desc': 'Основы машинного обучения, алгоритмы классификации и регрессии', 'tags': ['ml', 'math', 'stats', 'python'], 'source_url': 'https://abit.itmo.ru/program/master/ai' }, { 'id': 'ai_1_2', 'program_id': 'ai', 'semester': 1, 'name': 'Глубокое обучение', 'credits': 4, 'hours': 72, 'type': 'required', 'short_desc': 'Нейронные сети, CNN, RNN, трансформеры', 'tags': ['dl', 'ml', 'neural', 'python'], 'source_url': 'https://abit.itmo.ru/program/master/ai' }, { 'id': 'ai_2_1', 'program_id': 'ai', 'semester': 2, 'name': 'Обработка естественного языка', 'credits': 5, 'hours': 90, 'type': 'required', 'short_desc': 'Методы обработки текста, токенизация, эмбеддинги', 'tags': ['nlp', 'dl', 'text', 'transformers'], 'source_url': 'https://abit.itmo.ru/program/master/ai' }, { 'id': 'ai_2_2', 'program_id': 'ai', 'semester': 2, 'name': 'Компьютерное зрение', 'credits': 4, 'hours': 72, 'type': 'required', 'short_desc': 'Обработка изображений, CNN, детекция объектов', 'tags': ['cv', 'dl', 'image', 'cnn'], 'source_url': 'https://abit.itmo.ru/program/master/ai' }, { 'id': 'ai_3_1', 'program_id': 'ai', 'semester': 3, 'name': 'Продвинутые методы машинного обучения', 'credits': 5, 'hours': 90, 'type': 'required', 'short_desc': 'Продвинутые алгоритмы ML, ансамбли, оптимизация', 'tags': ['ml', 'advanced', 'algorithms'], 'source_url': 'https://abit.itmo.ru/program/master/ai' }, { 'id': 'ai_4_1', 'program_id': 'ai', 'semester': 4, 'name': 'Магистерская диссертация', 'credits': 12, 'hours': 216, 'type': 'required', 'short_desc': 'Научно-исследовательская работа, защита диссертации', 'tags': ['research', 'thesis', 'project'], 'source_url': 'https://abit.itmo.ru/program/master/ai' }, # Программа AI Product { 'id': 'ai_product_1_1', 'program_id': 'ai_product', 'semester': 1, 'name': 'Продуктовая аналитика', 'credits': 6, 'hours': 108, 'type': 'required', 'short_desc': 'Анализ продуктовых метрик, A/B тестирование', 'tags': ['product', 'business', 'data', 'analytics'], 'source_url': 'https://abit.itmo.ru/program/master/ai_product' }, { 'id': 'ai_product_1_2', 'program_id': 'ai_product', 'semester': 1, 'name': 'Управление проектами', 'credits': 4, 'hours': 72, 'type': 'required', 'short_desc': 'Методологии управления проектами, Agile, Scrum', 'tags': ['pm', 'business', 'management', 'agile'], 'source_url': 'https://abit.itmo.ru/program/master/ai_product' }, { 'id': 'ai_product_2_1', 'program_id': 'ai_product', 'semester': 2, 'name': 'UX/UI для ИИ продуктов', 'credits': 4, 'hours': 72, 'type': 'required', 'short_desc': 'Дизайн интерфейсов для ИИ, UX исследования', 'tags': ['ux', 'ui', 'design', 'ai'], 'source_url': 'https://abit.itmo.ru/program/master/ai_product' }, { 'id': 'ai_product_2_2', 'program_id': 'ai_product', 'semester': 2, 'name': 'Этика ИИ', 'credits': 3, 'hours': 54, 'type': 'required', 'short_desc': 'Этические принципы ИИ, справедливость, прозрачность', 'tags': ['ethics', 'ai', 'responsible', 'fairness'], 'source_url': 'https://abit.itmo.ru/program/master/ai_product' }, { 'id': 'ai_product_3_1', 'program_id': 'ai_product', 'semester': 3, 'name': 'Управление ИИ продуктами', 'credits': 6, 'hours': 108, 'type': 'required', 'short_desc': 'Стратегическое управление ИИ продуктами, команды', 'tags': ['product', 'management', 'ai', 'leadership'], 'source_url': 'https://abit.itmo.ru/program/master/ai_product' }, { 'id': 'ai_product_4_1', 'program_id': 'ai_product', 'semester': 4, 'name': 'Дипломный проект', 'credits': 12, 'hours': 216, 'type': 'required', 'short_desc': 'Разработка ИИ продукта, защита проекта', 'tags': ['project', 'thesis', 'product'], 'source_url': 'https://abit.itmo.ru/program/master/ai_product' } ] def parse_all(): """Основная функция парсинга всех данных""" try: logger.info('Начинаем парсинг всех данных') # Создаем директории если нет os.makedirs('data/processed', exist_ok=True) # Парсим страницы программ programs = { 'ai': 'https://abit.itmo.ru/program/master/ai', 'ai_product': 'https://abit.itmo.ru/program/master/ai_product' } all_courses = [] for program_id, url in programs.items(): # Парсим страницу программы program_info = parse_program_page(url, program_id) # Пытаемся парсить PDF файлы for pdf_url in program_info['pdf_links']: pdf_courses = parse_pdf(pdf_url, program_id) for course in pdf_courses: normalized_course = normalize_course(course, program_id) all_courses.append(normalized_course) # Если парсинг не дал результатов, используем fallback if not all_courses: logger.warning('Парсинг не дал результатов, используем fallback курсы') all_courses = get_fallback_courses() # Сохраняем в JSON courses_file = 'data/processed/courses.json' with open(courses_file, 'w', encoding='utf-8') as f: json.dump(all_courses, f, ensure_ascii=False, indent=2) logger.info(f'Сохранено {len(all_courses)} курсов в {courses_file}') return True except Exception as e: logger.error(f'Ошибка парсинга: {e}') # Сохраняем fallback курсы try: os.makedirs('data/processed', exist_ok=True) with open('data/processed/courses.json', 'w', encoding='utf-8') as f: json.dump(get_fallback_courses(), f, ensure_ascii=False, indent=2) logger.info('Сохранены fallback курсы') return True except Exception as e2: logger.error(f'Ошибка сохранения fallback курсов: {e2}') return False