ITMO-QA / parser.py
vydrking's picture
Upload 18 files
da3b5f8 verified
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