vydrking commited on
Commit
53fe915
·
verified ·
1 Parent(s): 5071500

Upload 19 files

Browse files
README.md CHANGED
@@ -9,7 +9,7 @@ pinned: false
9
 
10
  # 🤖 ITMO Магистратура - Чат-бот
11
 
12
- Полноценный чат-бот для абитуриентов магистратур ITMO с LLM-генерацией ответов и персональными рекомендациями.
13
 
14
  ## 🚀 Быстрый деплой в HF Spaces
15
 
@@ -28,17 +28,30 @@ pinned: false
28
 
29
  ### 3. Автоматический запуск
30
  - HF Spaces автоматически соберет Docker образ
31
- - При первом запуске загрузятся модели и создадутся данные
32
  - Приложение будет доступно по URL вида: `https://huggingface.co/spaces/username/space-name`
33
 
34
  ## 🎯 Возможности
35
 
36
  - **LLM-генерация ответов**: cointegrated/rut5-base-multitask для естественных ответов
37
  - **RAG поиск**: SentenceTransformer + FAISS для точного поиска по курсам
38
- - **Персональные рекомендации**: на основе профиля студента с LLM-объяснениями
 
39
  - **Фильтр релевантности**: отвечает только на вопросы о ITMO
40
  - **Улучшенный UI**: навыки, интересы, ползунки для оценки уровня
41
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  ## ⚙️ Быстрые настройки
43
 
44
  ### Параметры производительности (CPU basic):
@@ -57,7 +70,7 @@ max_text_length = 220 # Максимум символов для эмб
57
  - **CPU**: 2 vCPU
58
  - **RAM**: до 16GB
59
  - **Диск**: 50GB ephemeral
60
- - **Время холодного старта**: до 3 минут (загрузка моделей)
61
 
62
  ## 🔧 Локальный запуск
63
 
@@ -89,12 +102,34 @@ python app.py
89
  - **Чат**: системные инструкции + контекст + история + вопрос
90
  - **Рекомендации**: профиль студента + доступные курсы + инструкции по выбору
91
 
 
 
 
 
 
 
92
  ### Оптимизации:
93
  - Ленивая загрузка моделей
94
  - Кэширование данных на диске
95
  - Fallback режим при ошибках
96
  - Компактные эмбеддинги (float32, ≤220 символов)
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  ## 🔍 Устранение неполадок
99
 
100
  ### Проблемы с памятью:
@@ -106,8 +141,14 @@ top_k = 4 # Уменьшить с 6
106
  ### Проблемы с холодным стартом:
107
  - Первый запуск может занять 2-3 минуты
108
  - Модели загружаются при первом обращении
 
109
  - Последующие запуски используют кэш
110
 
 
 
 
 
 
111
  ### Проблемы с Docker:
112
  - Убедитесь, что Dockerfile корректный
113
  - Проверьте логи сборки в HF Spaces
@@ -115,4 +156,4 @@ top_k = 4 # Уменьшить с 6
115
 
116
  ---
117
 
118
- **Примечание**: Бот работает с тестовыми данными для быстрого старта. Для реальных данных используйте кнопку "Обновить данные".
 
9
 
10
  # 🤖 ITMO Магистратура - Чат-бот
11
 
12
+ Полноценный чат-бот для абитуриентов магистратур ITMO с LLM-генерацией ответов, парсингом реальных данных и персональными рекомендациями.
13
 
14
  ## 🚀 Быстрый деплой в HF Spaces
15
 
 
28
 
29
  ### 3. Автоматический запуск
30
  - HF Spaces автоматически соберет Docker образ
31
+ - При первом запуске загрузятся модели и спарсятся данные с сайтов ITMO
32
  - Приложение будет доступно по URL вида: `https://huggingface.co/spaces/username/space-name`
33
 
34
  ## 🎯 Возможности
35
 
36
  - **LLM-генерация ответов**: cointegrated/rut5-base-multitask для естественных ответов
37
  - **RAG поиск**: SentenceTransformer + FAISS для точного поиска по курсам
38
+ - **Парсинг реальных данных**: автоматический сбор с сайтов ITMO
39
+ - **LLM-рекомендации**: персонализированные советы с объяснениями
40
  - **Фильтр релевантности**: отвечает только на вопросы о ITMO
41
  - **Улучшенный UI**: навыки, интересы, ползунки для оценки уровня
42
 
43
+ ## 📊 Источники данных
44
+
45
+ ### Автоматический парсинг:
46
+ - **https://abit.itmo.ru/program/master/ai** - программа "Искусственный интеллект"
47
+ - **https://abit.itmo.ru/program/master/ai_product** - программа "AI Product Management"
48
+
49
+ ### Извлекаемые данные:
50
+ - Заголовки и описания программ
51
+ - Ссылки на PDF учебные планы
52
+ - Парсинг PDF с извлечением курсов
53
+ - Нормализация и обогащение данных
54
+
55
  ## ⚙️ Быстрые настройки
56
 
57
  ### Параметры производительности (CPU basic):
 
70
  - **CPU**: 2 vCPU
71
  - **RAM**: до 16GB
72
  - **Диск**: 50GB ephemeral
73
+ - **Время холодного старта**: до 3 минут (загрузка моделей + парсинг)
74
 
75
  ## 🔧 Локальный запуск
76
 
 
102
  - **Чат**: системные инструкции + контекст + история + вопрос
103
  - **Рекомендации**: профиль студента + доступные курсы + инструкции по выбору
104
 
105
+ ### Парсинг данных:
106
+ - **HTML скрапинг**: BeautifulSoup для извлечения метаданных программ
107
+ - **PDF парсинг**: pdfplumber для извлечения курсов из учебных планов
108
+ - **Нормализация**: унификация форматов и обогащение тегами
109
+ - **Обновления**: проверка изменений на сайтах ITMO
110
+
111
  ### Оптимизации:
112
  - Ленивая загрузка моделей
113
  - Кэширование данных на диске
114
  - Fallback режим при ошибках
115
  - Компактные эмбеддинги (float32, ≤220 символов)
116
 
117
+ ## 🔄 Обновление данных
118
+
119
+ ### Автоматическое обновление:
120
+ - При первом запуске парсится актуальная информация с сайтов ITMO
121
+ - Проверка обновлений при каждом запуске
122
+ - Кнопка "Обновить данные" для принудительного обновления
123
+
124
+ ### Ручное обновление:
125
+ ```bash
126
+ # Принудительное обновление
127
+ python update_data.py --force
128
+
129
+ # Проверка обновлений
130
+ python update_data.py --check
131
+ ```
132
+
133
  ## 🔍 Устранение неполадок
134
 
135
  ### Проблемы с памятью:
 
141
  ### Проблемы с холодным стартом:
142
  - Первый запуск может занять 2-3 минуты
143
  - Модели загружаются при первом обращении
144
+ - Парсинг данных выполняется автоматически
145
  - Последующие запуски используют кэш
146
 
147
+ ### Проблемы с парсингом:
148
+ - При недоступности сайтов ITMO используются тестовые данные
149
+ - Проверьте интернет-соединение
150
+ - Логи показывают детали процесса парсинга
151
+
152
  ### Проблемы с Docker:
153
  - Убедитесь, что Dockerfile корректный
154
  - Проверьте логи сборки в HF Spaces
 
156
 
157
  ---
158
 
159
+ **Примечание**: Бот автоматически парсит актуальные данные с сайтов ITMO. При недоступности источников используются тестовые данные для демонстрации функциональности.
app.py CHANGED
@@ -21,9 +21,15 @@ def chat_with_bot(message, history):
21
 
22
  try:
23
  response, relevance_score = chatbot.chat(message, history)
 
 
 
 
 
24
  return history + [[message, response]], ''
25
  except Exception as e:
26
- error_msg = f'Произошла ошибка: {str(e)}'
 
27
  return history + [[message, error_msg]], ''
28
 
29
  def get_recommendations(programming_exp, math_level, interests, semester, skills):
@@ -43,9 +49,16 @@ def get_recommendations(programming_exp, math_level, interests, semester, skills
43
  'interests': all_interests,
44
  'semester': semester
45
  }
 
46
  recommendations = chatbot.recommend_courses(profile)
 
 
 
 
 
47
  return recommendations
48
  except Exception as e:
 
49
  return f'Ошибка при получении рекомендаций: {str(e)}'
50
 
51
  def update_data_ui():
 
21
 
22
  try:
23
  response, relevance_score = chatbot.chat(message, history)
24
+
25
+ # Проверяем, что ответ не пустой и не содержит технических деталей
26
+ if not response or response.startswith('[') or len(response.strip()) < 5:
27
+ response = 'К сожалению, не смог сгенерировать ответ. Попробуйте переформулировать вопрос.'
28
+
29
  return history + [[message, response]], ''
30
  except Exception as e:
31
+ print(f'Ошибка в чате: {e}')
32
+ error_msg = 'Произошла ошибка при обработке запроса. Попробуйте еще раз.'
33
  return history + [[message, error_msg]], ''
34
 
35
  def get_recommendations(programming_exp, math_level, interests, semester, skills):
 
49
  'interests': all_interests,
50
  'semester': semester
51
  }
52
+
53
  recommendations = chatbot.recommend_courses(profile)
54
+
55
+ # Проверяем качество ответа
56
+ if not recommendations or recommendations.startswith('[') or len(recommendations.strip()) < 10:
57
+ recommendations = 'К сожалению, не удалось сгенерировать рекомендации. Попробуйте изменить параметры профиля.'
58
+
59
  return recommendations
60
  except Exception as e:
61
+ print(f'Ошибка в рекомендациях: {e}')
62
  return f'Ошибка при получении рекомендаций: {str(e)}'
63
 
64
  def update_data_ui():
app_simple.py CHANGED
@@ -106,11 +106,15 @@ def chat_with_bot(message, history):
106
 
107
  return history + [[message, response]], ''
108
 
109
- def get_recommendations(programming_exp, math_level, interests, semester):
110
  if not semester:
111
  return 'Пожалуйста, укажите семестр для получения рекомендаций.'
112
 
113
- semester = int(semester)
 
 
 
 
114
  filtered_courses = [c for c in TEST_COURSES if c['semester'] == semester]
115
 
116
  if not filtered_courses:
@@ -119,22 +123,45 @@ def get_recommendations(programming_exp, math_level, interests, semester):
119
  # Простая логика рекомендаций
120
  recommendations = []
121
  for course in filtered_courses[:5]: # Топ-5 курсов
122
- why = 'Курс из учебного плана программы'
123
- if interests:
124
- matching_tags = [tag for tag in interests if tag in course.get('tags', [])]
125
- if matching_tags:
126
- why = f'Соответствует вашим интересам: {", ".join(matching_tags)}'
 
 
 
 
 
 
 
 
 
127
 
128
- recommendations.append({
129
- 'name': course['name'],
130
- 'semester': course['semester'],
131
- 'credits': course['credits'],
132
- 'why': why
133
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
- result = '🎯 Рекомендуемые курсы (из официальных учебных планов ITMO):\n\n'
136
  for i, rec in enumerate(recommendations, 1):
137
- result += f'{i}. {rec["name"]} ({rec["semester"]} семестр, {rec["credits"]} кредитов)\n'
138
  result += f' {rec["why"]}\n\n'
139
 
140
  return result
@@ -166,25 +193,35 @@ with gr.Blocks(title='ITMO Магистратура - Чат-бот', theme=gr.t
166
  with gr.Column(scale=1):
167
  gr.Markdown('### 👤 Профиль для рекомендаций')
168
 
169
- programming_exp = gr.Slider(
170
- minimum=0, maximum=5, value=2, step=1,
171
- label='Опыт программирования (0-5)',
172
- info='0 - нет опыта, 5 - эксперт'
173
- )
174
-
175
- math_level = gr.Slider(
176
- minimum=0, maximum=4, value=2, step=1,
177
- label='Уровень математики (0-4)',
178
- info='0 - базовый, 4 - продвинутый'
179
- )
 
180
 
 
181
  interests = gr.CheckboxGroup(
182
  choices=['ml', 'dl', 'nlp', 'cv', 'product', 'business', 'research', 'data', 'systems'],
183
  value=['ml'],
184
- label='Интересы',
185
  info='Выберите интересующие направления'
186
  )
187
 
 
 
 
 
 
 
 
 
188
  semester = gr.Dropdown(
189
  choices=['1', '2', '3', '4'],
190
  label='Целевой семестр',
@@ -194,13 +231,13 @@ with gr.Blocks(title='ITMO Магистратура - Чат-бот', theme=gr.t
194
  recommend_btn = gr.Button('🎯 Получить рекомендации', variant='primary')
195
  recommendations_output = gr.Textbox(
196
  label='Рекомендации',
197
- lines=10,
198
  interactive=False
199
  )
200
 
201
  recommend_btn.click(
202
  get_recommendations,
203
- inputs=[programming_exp, math_level, interests, semester],
204
  outputs=recommendations_output
205
  )
206
 
 
106
 
107
  return history + [[message, response]], ''
108
 
109
+ def get_recommendations(programming_exp, math_level, interests, semester, skills):
110
  if not semester:
111
  return 'Пожалуйста, укажите семестр для получения рекомендаций.'
112
 
113
+ try:
114
+ semester = int(semester)
115
+ except ValueError:
116
+ return 'Пожалуйста, выберите корректный семестр.'
117
+
118
  filtered_courses = [c for c in TEST_COURSES if c['semester'] == semester]
119
 
120
  if not filtered_courses:
 
123
  # Простая логика рекомендаций
124
  recommendations = []
125
  for course in filtered_courses[:5]: # Топ-5 курсов
126
+ score = 0
127
+ why_reasons = []
128
+
129
+ # Оценка по интересам
130
+ all_interests = interests + skills
131
+ matching_tags = [tag for tag in all_interests if tag in course.get('tags', [])]
132
+ if matching_tags:
133
+ score += 2
134
+ why_reasons.append(f'соответствует вашим интересам: {", ".join(matching_tags)}')
135
+
136
+ # Оценка по опыту программирования
137
+ if programming_exp >= 3 and any(tag in course.get('tags', []) for tag in ['ml', 'dl', 'systems']):
138
+ score += 1
139
+ why_reasons.append('подходит для вашего уровня программирования')
140
 
141
+ # Оценка по математике
142
+ if math_level >= 3 and any(tag in course.get('tags', []) for tag in ['math', 'stats', 'dl']):
143
+ score += 1
144
+ why_reasons.append('соответствует вашему уровню математики')
145
+
146
+ if score > 0:
147
+ recommendations.append({
148
+ 'name': course['name'],
149
+ 'credits': course['credits'],
150
+ 'why': '; '.join(why_reasons) if why_reasons else 'курс из учебного плана программы'
151
+ })
152
+
153
+ if not recommendations:
154
+ # Если нет подходящих, показываем все курсы
155
+ for course in filtered_courses[:3]:
156
+ recommendations.append({
157
+ 'name': course['name'],
158
+ 'credits': course['credits'],
159
+ 'why': 'курс из учебного плана программы'
160
+ })
161
 
162
+ result = f'🎯 Рекомендуемые курсы для {semester} семестра:\n\n'
163
  for i, rec in enumerate(recommendations, 1):
164
+ result += f'{i}. {rec["name"]} ({rec["credits"]} кредитов)\n'
165
  result += f' {rec["why"]}\n\n'
166
 
167
  return result
 
193
  with gr.Column(scale=1):
194
  gr.Markdown('### 👤 Профиль для рекомендаций')
195
 
196
+ with gr.Row():
197
+ programming_exp = gr.Slider(
198
+ minimum=0, maximum=5, value=2, step=1,
199
+ label='Опыт программирования (0-5)',
200
+ info='0 - нет опыта, 5 - эксперт'
201
+ )
202
+
203
+ math_level = gr.Slider(
204
+ minimum=0, maximum=4, value=2, step=1,
205
+ label='Уровень математ��ки (0-4)',
206
+ info='0 - базовый, 4 - продвинутый'
207
+ )
208
 
209
+ gr.Markdown('**Интересы:**')
210
  interests = gr.CheckboxGroup(
211
  choices=['ml', 'dl', 'nlp', 'cv', 'product', 'business', 'research', 'data', 'systems'],
212
  value=['ml'],
213
+ label='Области интересов',
214
  info='Выберите интересующие направления'
215
  )
216
 
217
+ gr.Markdown('**Навыки:**')
218
+ skills = gr.CheckboxGroup(
219
+ choices=['python', 'java', 'sql', 'git', 'docker', 'aws', 'tensorflow', 'pytorch', 'scikit-learn'],
220
+ value=['python'],
221
+ label='Технические навыки',
222
+ info='Выберите имеющиеся навыки'
223
+ )
224
+
225
  semester = gr.Dropdown(
226
  choices=['1', '2', '3', '4'],
227
  label='Целевой семестр',
 
231
  recommend_btn = gr.Button('🎯 Получить рекомендации', variant='primary')
232
  recommendations_output = gr.Textbox(
233
  label='Рекомендации',
234
+ lines=12,
235
  interactive=False
236
  )
237
 
238
  recommend_btn.click(
239
  get_recommendations,
240
+ inputs=[programming_exp, math_level, interests, semester, skills],
241
  outputs=recommendations_output
242
  )
243
 
chatbot.py CHANGED
@@ -57,6 +57,11 @@ class ITMOChatbot:
57
  if not semester:
58
  return 'Пожалуйста, укажите семестр для получения рекомендаций.'
59
 
 
 
 
 
 
60
  # Получение курсов для семестра
61
  courses = self.knowledge_base.get_courses_by_semester(semester)
62
 
@@ -70,11 +75,18 @@ class ITMOChatbot:
70
 
71
  def _get_context(self, message: str) -> List[Dict]:
72
  try:
73
- results = self.retriever.retrieve(message, k=6, threshold=0.35)
74
- return results
 
 
 
 
 
 
 
75
  except Exception as e:
76
  print(f'Ошибка получения контекста: {e}')
77
- return []
78
 
79
  def _generate_answer(self, message: str, context: List[Dict], history: List[List[str]]) -> str:
80
  if not self.generator:
@@ -87,7 +99,7 @@ class ITMOChatbot:
87
  # Генерация ответа
88
  response = self.generator(
89
  prompt,
90
- max_new_tokens=180,
91
  temperature=0.4,
92
  do_sample=True,
93
  pad_token_id=self.generator.tokenizer.eos_token_id
@@ -98,8 +110,14 @@ class ITMOChatbot:
98
  # Очистка ответа
99
  if answer.startswith('Ответ:'):
100
  answer = answer[6:].strip()
 
 
101
 
102
- return answer if answer else self._fallback_answer(context)
 
 
 
 
103
 
104
  except Exception as e:
105
  print(f'Ошибка генерации ответа: {e}')
@@ -116,7 +134,7 @@ class ITMOChatbot:
116
  # Генерация рекомендаций
117
  response = self.generator(
118
  prompt,
119
- max_new_tokens=300,
120
  temperature=0.5,
121
  do_sample=True,
122
  pad_token_id=self.generator.tokenizer.eos_token_id
@@ -128,7 +146,11 @@ class ITMOChatbot:
128
  if recommendations.startswith('Рекомендации:'):
129
  recommendations = recommendations[14:].strip()
130
 
131
- return recommendations if recommendations else self._fallback_recommendations(profile, courses)
 
 
 
 
132
 
133
  except Exception as e:
134
  print(f'Ошибка генерации рекомендаций: {e}')
@@ -136,7 +158,7 @@ class ITMOChatbot:
136
 
137
  def _build_prompt(self, message: str, context: List[Dict], history: List[List[str]]) -> str:
138
  # Системные инструкции
139
- system_prompt = '''Отвечай только по контекстуиже). Если недостаточно данных прямо скажи: 'в предоставленных данных об этом не сказано'. Отвечай кратко и по делу.'''
140
 
141
  # История диалога (последние 3 хода)
142
  history_text = ''
@@ -146,13 +168,15 @@ class ITMOChatbot:
146
  history_text += f'Пользователь: {user_msg}\nБот: {bot_msg}\n\n'
147
 
148
  # Контекст
149
- context_text = 'Контекст:\n'
150
  for i, item in enumerate(context, 1):
151
  context_text += f'{i}. {item["name"]} ({item["semester"]} семестр, {item["credits"]} кредитов)\n'
152
- context_text += f' {item["short_desc"]}\n\n'
 
 
153
 
154
  # Полный промпт
155
- full_prompt = f'{system_prompt}\n\n{history_text}{context_text}Вопрос: {message}\nОтвет:'
156
 
157
  return full_prompt
158
 
@@ -171,13 +195,14 @@ class ITMOChatbot:
171
  for i, course in enumerate(courses[:10], 1): # Топ-10 курсов
172
  tags = ', '.join(course.get('tags', []))
173
  courses_text += f'{i}. {course["name"]} ({course["credits"]} кредитов)\n'
174
- courses_text += f' Описание: {course["short_desc"]}\n'
 
175
  courses_text += f' Теги: {tags}\n\n'
176
 
177
  # Инструкции для рекомендаций
178
  instructions = '''Для такого студента с такими навыками какие из курсов подойдут?
179
  Выбери 3-5 наиболее подходящих курсов и объясни почему они подходят для этого профиля.
180
- Учитывай уровень сложности, интересы и опыт студента.'''
181
 
182
  full_prompt = f'{profile_text}\n\n{courses_text}\n{instructions}\n\nРекомендации:'
183
 
@@ -190,27 +215,55 @@ class ITMOChatbot:
190
  response = 'Найденная информация:\n\n'
191
  for i, item in enumerate(context, 1):
192
  response += f'{i}. {item["name"]} ({item["semester"]} семестр, {item["credits"]} кредитов)\n'
193
- response += f' {item["short_desc"]}\n\n'
 
 
194
 
195
  return response
196
 
197
  def _fallback_recommendations(self, profile: Dict, courses: List[Dict]) -> str:
198
  semester = profile.get('semester')
199
  interests = profile.get('interests', [])
 
 
200
 
201
  # Простая логика рекомендаций
202
  recommendations = []
203
  for course in courses[:5]:
 
 
 
 
204
  matching_tags = [tag for tag in interests if tag in course.get('tags', [])]
205
- why = 'Курс из учебного плана программы'
206
  if matching_tags:
207
- why = f'Соответствует вашим интересам: {", ".join(matching_tags)}'
 
 
 
 
 
 
208
 
209
- recommendations.append({
210
- 'name': course['name'],
211
- 'credits': course['credits'],
212
- 'why': why
213
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
  result = f'🎯 Рекомендуемые курсы для {semester} сем��стра:\n\n'
216
  for i, rec in enumerate(recommendations, 1):
 
57
  if not semester:
58
  return 'Пожалуйста, укажите семестр для получения рекомендаций.'
59
 
60
+ try:
61
+ semester = int(semester)
62
+ except ValueError:
63
+ return 'Пожалуйста, выберите корректный семестр.'
64
+
65
  # Получение курсов для семестра
66
  courses = self.knowledge_base.get_courses_by_semester(semester)
67
 
 
75
 
76
  def _get_context(self, message: str) -> List[Dict]:
77
  try:
78
+ # Сначала пробуем RAG поиск
79
+ if self.retriever.index:
80
+ results = self.retriever.retrieve(message, k=6, threshold=0.35)
81
+ if results:
82
+ return results
83
+
84
+ # Fallback на простой поиск
85
+ return self.knowledge_base.search_courses(message)
86
+
87
  except Exception as e:
88
  print(f'Ошибка получения контекста: {e}')
89
+ return self.knowledge_base.search_courses(message)
90
 
91
  def _generate_answer(self, message: str, context: List[Dict], history: List[List[str]]) -> str:
92
  if not self.generator:
 
99
  # Генерация ответа
100
  response = self.generator(
101
  prompt,
102
+ max_new_tokens=200,
103
  temperature=0.4,
104
  do_sample=True,
105
  pad_token_id=self.generator.tokenizer.eos_token_id
 
110
  # Очистка ответа
111
  if answer.startswith('Ответ:'):
112
  answer = answer[6:].strip()
113
+ elif answer.startswith('Бот:'):
114
+ answer = answer[4:].strip()
115
 
116
+ # Проверяем, что ответ не пустой и не содержит технических деталей
117
+ if answer and len(answer) > 10 and not answer.startswith('['):
118
+ return answer
119
+ else:
120
+ return self._fallback_answer(context)
121
 
122
  except Exception as e:
123
  print(f'Ошибка генерации ответа: {e}')
 
134
  # Генерация рекомендаций
135
  response = self.generator(
136
  prompt,
137
+ max_new_tokens=400,
138
  temperature=0.5,
139
  do_sample=True,
140
  pad_token_id=self.generator.tokenizer.eos_token_id
 
146
  if recommendations.startswith('Рекомендации:'):
147
  recommendations = recommendations[14:].strip()
148
 
149
+ # Проверяем качество ответа
150
+ if recommendations and len(recommendations) > 20 and not recommendations.startswith('['):
151
+ return recommendations
152
+ else:
153
+ return self._fallback_recommendations(profile, courses)
154
 
155
  except Exception as e:
156
  print(f'Ошибка генерации рекомендаций: {e}')
 
158
 
159
  def _build_prompt(self, message: str, context: List[Dict], history: List[List[str]]) -> str:
160
  # Системные инструкции
161
+ system_prompt = '''Ты - помощник для абитуриентов магистратур ITMO. Отвечай на вопросы о программах и курсах на основе предоставленного контекста. Отвечай кратко, дружелюбно и по делу. Если информации недостаточно, скажи об этом прямо.'''
162
 
163
  # История диалога (последние 3 хода)
164
  history_text = ''
 
168
  history_text += f'Пользователь: {user_msg}\nБот: {bot_msg}\n\n'
169
 
170
  # Контекст
171
+ context_text = 'Информация о курсах:\n'
172
  for i, item in enumerate(context, 1):
173
  context_text += f'{i}. {item["name"]} ({item["semester"]} семестр, {item["credits"]} кредитов)\n'
174
+ if item.get('short_desc'):
175
+ context_text += f' {item["short_desc"]}\n'
176
+ context_text += '\n'
177
 
178
  # Полный промпт
179
+ full_prompt = f'{system_prompt}\n\n{history_text}{context_text}Пользователь: {message}\nБот:'
180
 
181
  return full_prompt
182
 
 
195
  for i, course in enumerate(courses[:10], 1): # Топ-10 курсов
196
  tags = ', '.join(course.get('tags', []))
197
  courses_text += f'{i}. {course["name"]} ({course["credits"]} кредитов)\n'
198
+ if course.get('short_desc'):
199
+ courses_text += f' Описание: {course["short_desc"]}\n'
200
  courses_text += f' Теги: {tags}\n\n'
201
 
202
  # Инструкции для рекомендаций
203
  instructions = '''Для такого студента с такими навыками какие из курсов подойдут?
204
  Выбери 3-5 наиболее подходящих курсов и объясни почему они подходят для этого профиля.
205
+ Учитывай уровень сложности, интересы и опыт студента. Отвечай на русском языке.'''
206
 
207
  full_prompt = f'{profile_text}\n\n{courses_text}\n{instructions}\n\nРекомендации:'
208
 
 
215
  response = 'Найденная информация:\n\n'
216
  for i, item in enumerate(context, 1):
217
  response += f'{i}. {item["name"]} ({item["semester"]} семестр, {item["credits"]} кредитов)\n'
218
+ if item.get('short_desc'):
219
+ response += f' {item["short_desc"]}\n'
220
+ response += '\n'
221
 
222
  return response
223
 
224
  def _fallback_recommendations(self, profile: Dict, courses: List[Dict]) -> str:
225
  semester = profile.get('semester')
226
  interests = profile.get('interests', [])
227
+ programming_exp = profile.get('programming_experience', 2)
228
+ math_level = profile.get('math_level', 2)
229
 
230
  # Простая логика рекомендаций
231
  recommendations = []
232
  for course in courses[:5]:
233
+ score = 0
234
+ why_reasons = []
235
+
236
+ # Оценка по интересам
237
  matching_tags = [tag for tag in interests if tag in course.get('tags', [])]
 
238
  if matching_tags:
239
+ score += 2
240
+ why_reasons.append(f'соответствует вашим интересам: {", ".join(matching_tags)}')
241
+
242
+ # Оценка по опыту программирования
243
+ if programming_exp >= 3 and any(tag in course.get('tags', []) for tag in ['ml', 'dl', 'systems']):
244
+ score += 1
245
+ why_reasons.append('подходит для вашего уровня программирования')
246
 
247
+ # Оценка по математике
248
+ if math_level >= 3 and any(tag in course.get('tags', []) for tag in ['math', 'stats', 'dl']):
249
+ score += 1
250
+ why_reasons.append('соответствует вашему уровню математики')
251
+
252
+ if score > 0:
253
+ recommendations.append({
254
+ 'name': course['name'],
255
+ 'credits': course['credits'],
256
+ 'why': '; '.join(why_reasons) if why_reasons else 'курс из учебного плана программы'
257
+ })
258
+
259
+ if not recommendations:
260
+ # Если нет подходящих, показываем все курсы
261
+ for course in courses[:3]:
262
+ recommendations.append({
263
+ 'name': course['name'],
264
+ 'credits': course['credits'],
265
+ 'why': 'курс из учебного плана программы'
266
+ })
267
 
268
  result = f'🎯 Рекомендуемые курсы для {semester} сем��стра:\n\n'
269
  for i, rec in enumerate(recommendations, 1):
data/processed/programs.json CHANGED
@@ -2,29 +2,57 @@
2
  "ai": {
3
  "id": "ai",
4
  "title": "Искусственный интеллект",
5
- "description": "Магистерская программа по искусственному интеллекту в ITMO",
6
  "url": "https://abit.itmo.ru/program/master/ai",
7
  "pdf_links": [
8
  {
9
- "url": "https://abit.itmo.ru/program/master/ai/curriculum",
10
- "text": "учебный план",
11
- "filename": "ai_curriculum.pdf"
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
  ],
14
- "hash": "test_hash_ai"
 
15
  },
16
  "ai_product": {
17
  "id": "ai_product",
18
- "title": "AI Product",
19
- "description": "Магистерская программа по продуктовой разработке с ИИ",
20
  "url": "https://abit.itmo.ru/program/master/ai_product",
21
  "pdf_links": [
22
  {
23
- "url": "https://abit.itmo.ru/program/master/ai_product/curriculum",
24
- "text": "учебный план",
25
- "filename": "ai_product_curriculum.pdf"
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
  ],
28
- "hash": "test_hash_ai_product"
 
29
  }
30
- }
 
2
  "ai": {
3
  "id": "ai",
4
  "title": "Искусственный интеллект",
5
+ "description": "Основа обучения на программе – проектный подход. Магистранты работают над проектами ведущих компаний X5 Group, Ozon Банк, МТС, Sber AI, Норникель, Napoleon IT, Genotek, Raft, AIRI, DeepPavlov. Перенимают опыт у 20+ экспертов в ML, в том числе из Яндекса и Газпромбанка. Вы станете частью комьюнити ведущих специалистов в области AI и ML.Вы сможете составить персональную траекторию обучения из курсов и проектов и освоить одну или несколько ролей: ML Engineer, Data Engineer, AI Product Developer и...",
6
  "url": "https://abit.itmo.ru/program/master/ai",
7
  "pdf_links": [
8
  {
9
+ "url": "https://abit.itmo.ru/file_storage/file/exams/master/ai.pdf",
10
+ "filename": "document_ai.pdf",
11
+ "type": "document",
12
+ "text": "смотреть"
13
+ },
14
+ {
15
+ "url": "https://itmo.ru/file/pages/79/personal_data_policy.pdf",
16
+ "filename": "document_personal_data_policy.pdf",
17
+ "type": "document",
18
+ "text": "политика по обработке персональных данных"
19
+ },
20
+ {
21
+ "url": "https://itmo.ru/images/pages/79/Pravila_ispolzovanija_informacii.pdf",
22
+ "filename": "document_Pravila_ispolzovanija_informacii.pdf",
23
+ "type": "document",
24
+ "text": "правила использования информации в доменной зоне itmo.ru"
25
  }
26
  ],
27
+ "content_hash": "525fb9a55baee4c11803c49ab1814e1b59fe3ed76715b120888241528e2671d2",
28
+ "last_updated": ""
29
  },
30
  "ai_product": {
31
  "id": "ai_product",
32
+ "title": "Управление ИИ-продуктами/AI Product",
33
+ "description": "Программа дает глубокие технические знания в области разработки систем искусственного интеллекта и навыки продуктового менеджмента. Вы сможете создавать инновационные ИИ‑решения и выводить их на рынок. Широкий выбор предметов позволяет построить индивидуальную траекторию обучения и стать AI Product Manager, AI Project Manager или Product Data Analyst. Вас ждут реальные проекты для компаний уровня Альфа-Банк, очные воркшопы и онлайн-лекции. Для выпускной работы вы можете выбрать проект для компан...",
34
  "url": "https://abit.itmo.ru/program/master/ai_product",
35
  "pdf_links": [
36
  {
37
+ "url": "https://abit.itmo.ru/file_storage/file/exams/master/ai_product.pdf",
38
+ "filename": "document_ai_product.pdf",
39
+ "type": "document",
40
+ "text": "смотреть"
41
+ },
42
+ {
43
+ "url": "https://itmo.ru/file/pages/79/personal_data_policy.pdf",
44
+ "filename": "document_personal_data_policy.pdf",
45
+ "type": "document",
46
+ "text": "политика по обработке персональных данных"
47
+ },
48
+ {
49
+ "url": "https://itmo.ru/images/pages/79/Pravila_ispolzovanija_informacii.pdf",
50
+ "filename": "document_Pravila_ispolzovanija_informacii.pdf",
51
+ "type": "document",
52
+ "text": "правила использования информации в доменной зоне itmo.ru"
53
  }
54
  ],
55
+ "content_hash": "d1d3028e607032b00a5ca364ef85990a0258df0d2116fc7bdb6d5bd4106d07bb",
56
+ "last_updated": ""
57
  }
58
+ }
knowledge_base.py CHANGED
@@ -25,6 +25,9 @@ class KnowledgeBase:
25
  except FileNotFoundError:
26
  print('Файлы данных не найдены, создаем тестовые данные...')
27
  self._create_test_data()
 
 
 
28
 
29
  def _create_test_data(self):
30
  # Тестовые программы
@@ -191,7 +194,13 @@ class KnowledgeBase:
191
  message_lower = message.lower()
192
  return any(keyword in message_lower for keyword in itmo_keywords)
193
 
194
- def get_courses_by_semester(self, semester: int) -> List[Dict]:
 
 
 
 
 
 
195
  return [course for course in self.courses if course.get('semester') == semester]
196
 
197
  def get_course_by_id(self, course_id: str) -> Dict:
 
25
  except FileNotFoundError:
26
  print('Файлы данных не найдены, создаем тестовые данные...')
27
  self._create_test_data()
28
+ except Exception as e:
29
+ print(f'Ошибка загрузки данных: {e}, создаем тестовые данные...')
30
+ self._create_test_data()
31
 
32
  def _create_test_data(self):
33
  # Тестовые программы
 
194
  message_lower = message.lower()
195
  return any(keyword in message_lower for keyword in itmo_keywords)
196
 
197
+ def get_courses_by_semester(self, semester) -> List[Dict]:
198
+ """Получает курсы для указанного семестра"""
199
+ try:
200
+ semester = int(semester)
201
+ except (ValueError, TypeError):
202
+ semester = 1
203
+
204
  return [course for course in self.courses if course.get('semester') == semester]
205
 
206
  def get_course_by_id(self, course_id: str) -> Dict:
scraper/html_scraper.py CHANGED
@@ -1,16 +1,16 @@
1
  import requests
2
- import re
3
- from bs4 import BeautifulSoup
4
- from typing import List, Dict
5
  import hashlib
6
  import json
7
  import os
 
 
 
8
 
9
  class HTMLScraper:
10
  def __init__(self):
11
  self.session = requests.Session()
12
  self.session.headers.update({
13
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
14
  })
15
 
16
  self.program_urls = {
@@ -23,9 +23,11 @@ class HTMLScraper:
23
 
24
  for program_id, url in self.program_urls.items():
25
  try:
26
- print(f'Скрапинг программы {program_id}...')
27
  program_data = self._scrape_program_page(url, program_id)
28
- programs[program_id] = program_data
 
 
29
  except Exception as e:
30
  print(f'Ошибка при скрапинге {program_id}: {e}')
31
 
@@ -37,107 +39,221 @@ class HTMLScraper:
37
 
38
  soup = BeautifulSoup(response.content, 'html.parser')
39
 
 
40
  title = self._extract_title(soup)
 
 
41
  description = self._extract_description(soup)
 
 
42
  pdf_links = self._extract_pdf_links(soup, url)
43
 
44
- program_data = {
 
 
 
45
  'id': program_id,
46
  'title': title,
47
  'description': description,
48
  'url': url,
49
  'pdf_links': pdf_links,
50
- 'hash': self._calculate_hash(response.content)
 
51
  }
52
-
53
- return program_data
54
 
55
  def _extract_title(self, soup: BeautifulSoup) -> str:
56
- title_elem = soup.find('h1') or soup.find('title')
57
- if title_elem:
58
- return title_elem.get_text().strip()
59
- return ''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  def _extract_description(self, soup: BeautifulSoup) -> str:
 
62
  desc_selectors = [
63
  '.program-description',
64
  '.description',
65
- '.program-info',
66
- 'p',
67
- '.content'
 
68
  ]
69
 
70
  for selector in desc_selectors:
71
- elem = soup.select_one(selector)
72
- if elem:
73
- text = elem.get_text().strip()
74
- if len(text) > 50:
75
- return text[:500]
76
 
77
- return ''
 
 
 
 
 
 
 
78
 
79
  def _extract_pdf_links(self, soup: BeautifulSoup, base_url: str) -> List[Dict]:
80
  pdf_links = []
81
 
 
82
  for link in soup.find_all('a', href=True):
83
- href = link.get('href', '')
84
  text = link.get_text().strip().lower()
85
 
86
- if self._is_pdf_link(href, text):
87
- full_url = self._make_absolute_url(href, base_url)
 
 
 
 
 
 
 
 
 
 
 
 
88
  pdf_links.append({
89
  'url': full_url,
90
- 'text': text,
91
- 'filename': self._extract_filename(href)
 
92
  })
93
 
 
 
 
 
94
  return pdf_links
95
 
96
- def _is_pdf_link(self, href: str, text: str) -> bool:
97
- pdf_indicators = [
98
- 'учебный план', 'учебный план', 'curriculum', 'plan',
99
- 'pdf', '.pdf', 'программа', 'program'
100
- ]
101
 
102
- href_lower = href.lower()
103
- return any(indicator in href_lower or indicator in text for indicator in pdf_indicators)
 
 
 
 
 
 
104
 
105
  def _make_absolute_url(self, href: str, base_url: str) -> str:
106
- if href.startswith('http'):
107
- return href
108
  elif href.startswith('/'):
109
- base = '/'.join(base_url.split('/')[:3])
110
- return base + href
 
 
111
  else:
112
  return base_url.rstrip('/') + '/' + href.lstrip('/')
113
 
114
- def _extract_filename(self, href: str) -> str:
 
115
  filename = href.split('/')[-1]
116
  if not filename.endswith('.pdf'):
117
  filename += '.pdf'
118
- return filename
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- def _calculate_hash(self, content: bytes) -> str:
121
  return hashlib.sha256(content).hexdigest()
122
 
123
- def save_programs(self, programs: Dict, output_path: str = 'data/processed/programs.json'):
124
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
125
 
126
- with open(output_path, 'w', encoding='utf-8') as f:
127
  json.dump(programs, f, ensure_ascii=False, indent=2)
128
 
129
- print(f'Программы сохранены в {output_path}')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  def main():
132
  scraper = HTMLScraper()
133
  programs = scraper.scrape_programs()
134
  scraper.save_programs(programs)
135
 
 
136
  for program_id, program in programs.items():
137
- print(f'\n{program["title"]}:')
138
- print(f'PDF ссылок найдено: {len(program["pdf_links"])}')
139
- for link in program['pdf_links']:
140
- print(f' - {link["filename"]}: {link["url"]}')
141
 
142
  if __name__ == '__main__':
143
  main()
 
1
  import requests
 
 
 
2
  import hashlib
3
  import json
4
  import os
5
+ from typing import List, Dict
6
+ from bs4 import BeautifulSoup
7
+ import re
8
 
9
  class HTMLScraper:
10
  def __init__(self):
11
  self.session = requests.Session()
12
  self.session.headers.update({
13
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
14
  })
15
 
16
  self.program_urls = {
 
23
 
24
  for program_id, url in self.program_urls.items():
25
  try:
26
+ print(f'Скрапинг программы: {program_id}')
27
  program_data = self._scrape_program_page(url, program_id)
28
+ if program_data:
29
+ programs[program_id] = program_data
30
+ print(f'Успешно обработана программа: {program_data["title"]}')
31
  except Exception as e:
32
  print(f'Ошибка при скрапинге {program_id}: {e}')
33
 
 
39
 
40
  soup = BeautifulSoup(response.content, 'html.parser')
41
 
42
+ # Извлечение заголовка
43
  title = self._extract_title(soup)
44
+
45
+ # Извлечение описания
46
  description = self._extract_description(soup)
47
+
48
+ # Поиск ссылок на PDF учебные планы
49
  pdf_links = self._extract_pdf_links(soup, url)
50
 
51
+ # Создание хэша контента для отслеживания изменений
52
+ content_hash = self._calculate_content_hash(response.content)
53
+
54
+ return {
55
  'id': program_id,
56
  'title': title,
57
  'description': description,
58
  'url': url,
59
  'pdf_links': pdf_links,
60
+ 'content_hash': content_hash,
61
+ 'last_updated': response.headers.get('last-modified', '')
62
  }
 
 
63
 
64
  def _extract_title(self, soup: BeautifulSoup) -> str:
65
+ # Поиск заголовка программы
66
+ title_selectors = [
67
+ 'h1',
68
+ '.program-title',
69
+ '.title',
70
+ '[class*="title"]',
71
+ '[class*="header"]'
72
+ ]
73
+
74
+ for selector in title_selectors:
75
+ title_elem = soup.select_one(selector)
76
+ if title_elem and title_elem.get_text().strip():
77
+ title = title_elem.get_text().strip()
78
+ if len(title) > 5: # Минимальная длина заголовка
79
+ return title
80
+
81
+ # Fallback - поиск по ключевым словам
82
+ for elem in soup.find_all(['h1', 'h2', 'h3']):
83
+ text = elem.get_text().strip()
84
+ if any(keyword in text.lower() for keyword in ['искусственный интеллект', 'ai', 'продукт']):
85
+ return text
86
+
87
+ return f'Программа {program_id.upper()}'
88
 
89
  def _extract_description(self, soup: BeautifulSoup) -> str:
90
+ # Поиск описания программы
91
  desc_selectors = [
92
  '.program-description',
93
  '.description',
94
+ '.about',
95
+ '[class*="description"]',
96
+ '[class*="about"]',
97
+ 'p'
98
  ]
99
 
100
  for selector in desc_selectors:
101
+ desc_elem = soup.select_one(selector)
102
+ if desc_elem:
103
+ desc = desc_elem.get_text().strip()
104
+ if len(desc) > 50: # Минимальная длина описания
105
+ return desc[:500] + '...' if len(desc) > 500 else desc
106
 
107
+ # Fallback - поиск по ключевым словам
108
+ for elem in soup.find_all('p'):
109
+ text = elem.get_text().strip()
110
+ if any(keyword in text.lower() for keyword in ['магистратура', 'программа', 'обучение', 'подготовка']):
111
+ if len(text) > 30:
112
+ return text[:500] + '...' if len(text) > 500 else text
113
+
114
+ return 'Описание программы магистратуры ITMO'
115
 
116
  def _extract_pdf_links(self, soup: BeautifulSoup, base_url: str) -> List[Dict]:
117
  pdf_links = []
118
 
119
+ # Поиск всех ссылок на PDF
120
  for link in soup.find_all('a', href=True):
121
+ href = link['href']
122
  text = link.get_text().strip().lower()
123
 
124
+ # Проверка на PDF файлы
125
+ if href.endswith('.pdf') or 'pdf' in href:
126
+ # Определение типа документа по тексту ссылки
127
+ doc_type = self._determine_document_type(text)
128
+
129
+ # Получение полного URL
130
+ if href.startswith('http'):
131
+ full_url = href
132
+ else:
133
+ full_url = self._make_absolute_url(href, base_url)
134
+
135
+ # Генерация имени файла
136
+ filename = self._generate_filename(href, doc_type)
137
+
138
  pdf_links.append({
139
  'url': full_url,
140
+ 'filename': filename,
141
+ 'type': doc_type,
142
+ 'text': text
143
  })
144
 
145
+ # Поиск по ключевым словам в тексте
146
+ if not pdf_links:
147
+ pdf_links = self._search_pdf_by_keywords(soup, base_url)
148
+
149
  return pdf_links
150
 
151
+ def _determine_document_type(self, text: str) -> str:
152
+ text_lower = text.lower()
 
 
 
153
 
154
+ if any(word in text_lower for word in ['учебный план', 'curriculum', 'plan']):
155
+ return 'curriculum'
156
+ elif any(word in text_lower for word in ['программа', 'program']):
157
+ return 'program'
158
+ elif any(word in text_lower for word in ['описание', 'description']):
159
+ return 'description'
160
+ else:
161
+ return 'document'
162
 
163
  def _make_absolute_url(self, href: str, base_url: str) -> str:
164
+ if href.startswith('//'):
165
+ return 'https:' + href
166
  elif href.startswith('/'):
167
+ # Извлекаем домен из base_url
168
+ from urllib.parse import urlparse
169
+ parsed = urlparse(base_url)
170
+ return f"{parsed.scheme}://{parsed.netloc}{href}"
171
  else:
172
  return base_url.rstrip('/') + '/' + href.lstrip('/')
173
 
174
+ def _generate_filename(self, href: str, doc_type: str) -> str:
175
+ # Извлекаем имя файла из URL
176
  filename = href.split('/')[-1]
177
  if not filename.endswith('.pdf'):
178
  filename += '.pdf'
179
+
180
+ # Добавляем префикс типа документа
181
+ return f"{doc_type}_{filename}"
182
+
183
+ def _search_pdf_by_keywords(self, soup: BeautifulSoup, base_url: str) -> List[Dict]:
184
+ pdf_links = []
185
+
186
+ # Ключевые слова для поиска учебных планов
187
+ keywords = [
188
+ 'учебный план',
189
+ 'curriculum',
190
+ 'программа обучения',
191
+ 'образовательная программа'
192
+ ]
193
+
194
+ # Поиск по тексту страницы
195
+ page_text = soup.get_text().lower()
196
+
197
+ for keyword in keywords:
198
+ if keyword in page_text:
199
+ # Попытка найти ссылку рядом с ключевым словом
200
+ for elem in soup.find_all(['a', 'p', 'div']):
201
+ text = elem.get_text().lower()
202
+ if keyword in text:
203
+ # Ищем ссылки в этом элементе или рядом
204
+ links = elem.find_all('a', href=True)
205
+ for link in links:
206
+ href = link['href']
207
+ if href.endswith('.pdf') or 'pdf' in href:
208
+ full_url = self._make_absolute_url(href, base_url)
209
+ pdf_links.append({
210
+ 'url': full_url,
211
+ 'filename': f"curriculum_{href.split('/')[-1]}",
212
+ 'type': 'curriculum',
213
+ 'text': link.get_text().strip()
214
+ })
215
+
216
+ return pdf_links
217
 
218
+ def _calculate_content_hash(self, content: bytes) -> str:
219
  return hashlib.sha256(content).hexdigest()
220
 
221
+ def save_programs(self, programs: Dict):
222
+ os.makedirs('data/processed', exist_ok=True)
223
 
224
+ with open('data/processed/programs.json', 'w', encoding='utf-8') as f:
225
  json.dump(programs, f, ensure_ascii=False, indent=2)
226
 
227
+ print(f'Программы сохранены: {len(programs)} программ')
228
+
229
+ def check_updates(self, programs: Dict) -> Dict:
230
+ updates = {}
231
+
232
+ for program_id, program in programs.items():
233
+ try:
234
+ response = self.session.get(program['url'], timeout=30)
235
+ current_hash = self._calculate_content_hash(response.content)
236
+
237
+ if current_hash != program.get('content_hash'):
238
+ updates[program_id] = {
239
+ 'old_hash': program.get('content_hash'),
240
+ 'new_hash': current_hash,
241
+ 'last_modified': response.headers.get('last-modified', '')
242
+ }
243
+ print(f'Обнаружены изменения в программе: {program_id}')
244
+ except Exception as e:
245
+ print(f'Ошибка проверки обновлений для {program_id}: {e}')
246
+
247
+ return updates
248
 
249
  def main():
250
  scraper = HTMLScraper()
251
  programs = scraper.scrape_programs()
252
  scraper.save_programs(programs)
253
 
254
+ print(f'Обработано программ: {len(programs)}')
255
  for program_id, program in programs.items():
256
+ print(f'{program_id}: {program["title"]} - {len(program["pdf_links"])} PDF')
 
 
 
257
 
258
  if __name__ == '__main__':
259
  main()
scraper/normalize.py CHANGED
@@ -5,20 +5,31 @@ from typing import List, Dict
5
  class DataNormalizer:
6
  def __init__(self):
7
  self.tag_keywords = {
8
- 'ml': ['машинное обучение', 'machine learning', 'ml', 'алгоритм', 'модель'],
9
- 'dl': ['глубокое обучение', 'deep learning', 'нейронная сеть', 'cnn', 'rnn', 'transformer'],
10
- 'nlp': ['nlp', 'обработка естественного языка', 'natural language', 'текст', 'язык'],
11
- 'cv': ['компьютерное зрение', 'computer vision', 'cv', 'изображение', 'видео'],
12
- 'math': ['математика', 'математический', 'алгебра', 'геометрия', 'анализ'],
13
- 'stats': ['статистика', 'вероятность', 'статистический', 'probability'],
14
- 'product': ['продукт', 'product', 'разработка продукта', 'продуктовая'],
15
- 'business': ['бизнес', 'business', 'менеджмент', 'управление', 'экономика'],
16
- 'pm': ['project management', 'управление проектами', 'pm', 'проект'],
17
- 'systems': ['система', 'system', 'архитектура', 'инфраструктура'],
18
- 'data': ['данные', 'data', 'анализ данных', 'big data', 'база данных']
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
21
  def normalize_courses(self, courses: List[Dict]) -> List[Dict]:
 
22
  normalized_courses = []
23
  seen_hashes = set()
24
 
@@ -33,15 +44,22 @@ class DataNormalizer:
33
  return normalized_courses
34
 
35
  def _normalize_course(self, course: Dict) -> Dict:
 
36
  if not course.get('name'):
37
  return None
38
 
39
  normalized = course.copy()
40
 
 
41
  normalized['name'] = self._normalize_name(course['name'])
 
 
42
  normalized['short_desc'] = self._generate_short_desc(course)
 
 
43
  normalized['tags'] = self._generate_tags(course)
44
 
 
45
  normalized['semester'] = self._normalize_semester(course.get('semester', 1))
46
  normalized['credits'] = self._normalize_credits(course.get('credits', 0))
47
  normalized['hours'] = self._normalize_hours(course.get('hours', 0))
@@ -50,31 +68,51 @@ class DataNormalizer:
50
  return normalized
51
 
52
  def _normalize_name(self, name: str) -> str:
 
53
  if not name:
54
  return ''
55
 
56
  name = str(name).strip()
 
 
57
  name = re.sub(r'\s+', ' ', name)
58
- name = name.replace('"', '').replace('"', '')
 
 
 
 
59
 
60
  return name
61
 
62
- def _generate_short_desc(self, course: dict) -> str:
 
63
  name = course.get('name', '')
64
  desc = course.get('description', '')
65
 
 
66
  if desc:
67
  desc = str(desc).strip()
68
  if len(desc) > 220:
69
  desc = desc[:220] + '...'
70
  return desc
71
 
 
72
  if name and len(name) > 50:
73
  return name[:220]
74
 
75
- return 'Курс из учебного плана программы'
 
 
 
 
 
 
 
 
 
76
 
77
  def _generate_tags(self, course: Dict) -> List[str]:
 
78
  text = f"{course.get('name', '')} {course.get('short_desc', '')}".lower()
79
  tags = []
80
 
@@ -82,9 +120,19 @@ class DataNormalizer:
82
  if any(keyword in text for keyword in keywords):
83
  tags.append(tag)
84
 
85
- return tags
 
 
 
 
 
 
 
 
 
86
 
87
  def _normalize_semester(self, semester) -> int:
 
88
  try:
89
  semester = int(semester)
90
  if 1 <= semester <= 4:
@@ -95,6 +143,7 @@ class DataNormalizer:
95
  return 1
96
 
97
  def _normalize_credits(self, credits) -> int:
 
98
  try:
99
  credits = int(credits)
100
  if credits >= 0:
@@ -105,6 +154,7 @@ class DataNormalizer:
105
  return 0
106
 
107
  def _normalize_hours(self, hours) -> int:
 
108
  try:
109
  hours = int(hours)
110
  if hours >= 0:
@@ -115,23 +165,26 @@ class DataNormalizer:
115
  return 0
116
 
117
  def _normalize_type(self, course_type: str) -> str:
 
118
  if not course_type:
119
  return 'required'
120
 
121
  type_lower = str(course_type).lower()
122
 
123
- if any(word in type_lower for word in ['обязательная', 'required', 'обяз']):
124
  return 'required'
125
- elif any(word in type_lower for word in ['по выбору', 'elective', 'выбор']):
126
  return 'elective'
127
 
128
  return 'required'
129
 
130
  def _calculate_course_hash(self, course: Dict) -> str:
 
131
  text = f"{course.get('name', '')}{course.get('program_id', '')}{course.get('semester', '')}"
132
  return hashlib.md5(text.encode()).hexdigest()
133
 
134
  def merge_courses(self, courses_list: List[List[Dict]]) -> List[Dict]:
 
135
  all_courses = []
136
  for courses in courses_list:
137
  all_courses.extend(courses)
@@ -139,6 +192,7 @@ class DataNormalizer:
139
  return self.normalize_courses(all_courses)
140
 
141
  def validate_course(self, course: Dict) -> bool:
 
142
  required_fields = ['name', 'program_id', 'semester']
143
 
144
  for field in required_fields:
@@ -151,6 +205,7 @@ class DataNormalizer:
151
  return True
152
 
153
  def get_statistics(self, courses: List[Dict]) -> Dict:
 
154
  stats = {
155
  'total_courses': len(courses),
156
  'by_program': {},
@@ -173,10 +228,94 @@ class DataNormalizer:
173
  stats['by_tags'][tag] = stats['by_tags'].get(tag, 0) + 1
174
 
175
  return stats
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
  def main():
178
  normalizer = DataNormalizer()
179
 
 
180
  test_courses = [
181
  {
182
  'id': 'test_1',
@@ -197,10 +336,14 @@ def main():
197
  ]
198
 
199
  normalized = normalizer.normalize_courses(test_courses)
200
- stats = normalizer.get_statistics(normalized)
 
201
 
202
  print(f'Нормализовано курсов: {len(normalized)}')
203
  print(f'Статистика: {stats}')
 
 
 
204
 
205
  if __name__ == '__main__':
206
  main()
 
5
  class DataNormalizer:
6
  def __init__(self):
7
  self.tag_keywords = {
8
+ 'ml': ['машинное обучение', 'machine learning', 'ml', 'алгоритм', 'модель', 'классификация', 'регрессия'],
9
+ 'dl': ['глубокое обучение', 'deep learning', 'нейронная сеть', 'cnn', 'rnn', 'transformer', 'нейросеть'],
10
+ 'nlp': ['nlp', 'обработка естественного языка', 'natural language', 'текст', 'язык', 'токенизация'],
11
+ 'cv': ['компьютерное зрение', 'computer vision', 'cv', 'изображение', 'видео', 'детекция', 'сегментация'],
12
+ 'math': ['математика', 'математический', 'алгебра', 'геометрия', 'анализ', 'линейная алгебра', 'статистика'],
13
+ 'stats': ['статистика', 'вероятность', 'статистический', 'probability', 'теория вероятностей'],
14
+ 'product': ['продукт', 'product', 'разработка продукта', 'продуктовая', 'аналитика'],
15
+ 'business': ['бизнес', 'business', 'менеджмент', 'управление', 'экономика', 'маркетинг'],
16
+ 'pm': ['project management', 'управление проектами', 'pm', 'проект', 'agile', 'scrum'],
17
+ 'systems': ['система', 'system', 'архитектура', 'инфраструктура', 'разработка'],
18
+ 'data': ['данные', 'data', 'анализ данных', 'big data', 'база данных', 'sql', 'nosql'],
19
+ 'research': ['исследование', 'research', 'наука', 'научный', 'диссертация', 'магистерская'],
20
+ 'python': ['python', 'питон', 'программирование'],
21
+ 'java': ['java', 'джава', 'программирование'],
22
+ 'sql': ['sql', 'база данных', 'database'],
23
+ 'git': ['git', 'версионирование', 'контроль версий'],
24
+ 'docker': ['docker', 'контейнеризация', 'контейнер'],
25
+ 'aws': ['aws', 'amazon', 'облако', 'cloud'],
26
+ 'tensorflow': ['tensorflow', 'tf', 'фреймворк'],
27
+ 'pytorch': ['pytorch', 'torch', 'фреймворк'],
28
+ 'scikit-learn': ['scikit-learn', 'sklearn', 'библиотека']
29
  }
30
 
31
  def normalize_courses(self, courses: List[Dict]) -> List[Dict]:
32
+ """Нормализует список курсов"""
33
  normalized_courses = []
34
  seen_hashes = set()
35
 
 
44
  return normalized_courses
45
 
46
  def _normalize_course(self, course: Dict) -> Dict:
47
+ """Нормализует отдельный курс"""
48
  if not course.get('name'):
49
  return None
50
 
51
  normalized = course.copy()
52
 
53
+ # Нормализация названия
54
  normalized['name'] = self._normalize_name(course['name'])
55
+
56
+ # Генерация короткого описания
57
  normalized['short_desc'] = self._generate_short_desc(course)
58
+
59
+ # Генерация тегов
60
  normalized['tags'] = self._generate_tags(course)
61
 
62
+ # Нормализация числовых полей
63
  normalized['semester'] = self._normalize_semester(course.get('semester', 1))
64
  normalized['credits'] = self._normalize_credits(course.get('credits', 0))
65
  normalized['hours'] = self._normalize_hours(course.get('hours', 0))
 
68
  return normalized
69
 
70
  def _normalize_name(self, name: str) -> str:
71
+ """Нормализует название курса"""
72
  if not name:
73
  return ''
74
 
75
  name = str(name).strip()
76
+
77
+ # Удаляем лишние пробелы и символы
78
  name = re.sub(r'\s+', ' ', name)
79
+ name = name.replace('"', '').replace('"', '').replace('«', '').replace('»', '')
80
+
81
+ # Убираем лишние скобки и символы
82
+ name = re.sub(r'^\s*[\(\)\[\]\-\s]+', '', name)
83
+ name = re.sub(r'[\(\)\[\]\-\s]+\s*$', '', name)
84
 
85
  return name
86
 
87
+ def _generate_short_desc(self, course: Dict) -> str:
88
+ """Генерирует короткое описание курса"""
89
  name = course.get('name', '')
90
  desc = course.get('description', '')
91
 
92
+ # Если есть описание, используем его
93
  if desc:
94
  desc = str(desc).strip()
95
  if len(desc) > 220:
96
  desc = desc[:220] + '...'
97
  return desc
98
 
99
+ # Если название длинное, используем его как описание
100
  if name and len(name) > 50:
101
  return name[:220]
102
 
103
+ # Генерируем базовое описание
104
+ program_id = course.get('program_id', '')
105
+ semester = course.get('semester', 1)
106
+
107
+ if program_id == 'ai':
108
+ return f'Курс программы "Искусственный интеллект" ({semester} семестр)'
109
+ elif program_id == 'ai_product':
110
+ return f'Курс программы "AI Product Management" ({semester} семестр)'
111
+ else:
112
+ return f'Курс из учебного плана программы ({semester} семестр)'
113
 
114
  def _generate_tags(self, course: Dict) -> List[str]:
115
+ """Генерирует теги для курса"""
116
  text = f"{course.get('name', '')} {course.get('short_desc', '')}".lower()
117
  tags = []
118
 
 
120
  if any(keyword in text for keyword in keywords):
121
  tags.append(tag)
122
 
123
+ # Добавляем теги на основе программы
124
+ program_id = course.get('program_id', '')
125
+ if program_id == 'ai':
126
+ if 'ml' not in tags:
127
+ tags.append('ml')
128
+ elif program_id == 'ai_product':
129
+ if 'product' not in tags:
130
+ tags.append('product')
131
+
132
+ return list(set(tags)) # Убираем дубликаты
133
 
134
  def _normalize_semester(self, semester) -> int:
135
+ """Нормализует номер семестра"""
136
  try:
137
  semester = int(semester)
138
  if 1 <= semester <= 4:
 
143
  return 1
144
 
145
  def _normalize_credits(self, credits) -> int:
146
+ """Нормализует количество кредитов"""
147
  try:
148
  credits = int(credits)
149
  if credits >= 0:
 
154
  return 0
155
 
156
  def _normalize_hours(self, hours) -> int:
157
+ """Нормализует количество часов"""
158
  try:
159
  hours = int(hours)
160
  if hours >= 0:
 
165
  return 0
166
 
167
  def _normalize_type(self, course_type: str) -> str:
168
+ """Нормализует тип курса"""
169
  if not course_type:
170
  return 'required'
171
 
172
  type_lower = str(course_type).lower()
173
 
174
+ if any(word in type_lower for word in ['обязательная', 'required', 'обяз', 'базовая']):
175
  return 'required'
176
+ elif any(word in type_lower for word in ['по выбору', 'elective', 'выбор', 'электив', 'факультатив']):
177
  return 'elective'
178
 
179
  return 'required'
180
 
181
  def _calculate_course_hash(self, course: Dict) -> str:
182
+ """Вычисляет хэш курса для дедупликации"""
183
  text = f"{course.get('name', '')}{course.get('program_id', '')}{course.get('semester', '')}"
184
  return hashlib.md5(text.encode()).hexdigest()
185
 
186
  def merge_courses(self, courses_list: List[List[Dict]]) -> List[Dict]:
187
+ """Объединяет несколько списков курсов"""
188
  all_courses = []
189
  for courses in courses_list:
190
  all_courses.extend(courses)
 
192
  return self.normalize_courses(all_courses)
193
 
194
  def validate_course(self, course: Dict) -> bool:
195
+ """Проверяет валидность курса"""
196
  required_fields = ['name', 'program_id', 'semester']
197
 
198
  for field in required_fields:
 
205
  return True
206
 
207
  def get_statistics(self, courses: List[Dict]) -> Dict:
208
+ """Получает статистику по курсам"""
209
  stats = {
210
  'total_courses': len(courses),
211
  'by_program': {},
 
228
  stats['by_tags'][tag] = stats['by_tags'].get(tag, 0) + 1
229
 
230
  return stats
231
+
232
+ def enrich_courses(self, courses: List[Dict]) -> List[Dict]:
233
+ """Обогащает курсы дополнительной информацией"""
234
+ for course in courses:
235
+ # Добавляем сложность курса
236
+ course['difficulty'] = self._calculate_difficulty(course)
237
+
238
+ # Добавляем рекомендуемый опыт
239
+ course['recommended_experience'] = self._calculate_recommended_experience(course)
240
+
241
+ # Добавляем категорию
242
+ course['category'] = self._determine_category(course)
243
+
244
+ return courses
245
+
246
+ def _calculate_difficulty(self, course: Dict) -> str:
247
+ """Вычисляет сложность курса"""
248
+ name = course.get('name', '').lower()
249
+ credits = course.get('credits', 0)
250
+ semester = course.get('semester', 1)
251
+
252
+ # По ключевым словам
253
+ if any(word in name for word in ['продвинутый', 'advanced', 'углубленный']):
254
+ return 'advanced'
255
+ elif any(word in name for word in ['базовый', 'basic', 'введение', 'вводный']):
256
+ return 'beginner'
257
+
258
+ # По кредитам и семестру
259
+ if credits >= 6 or semester >= 3:
260
+ return 'intermediate'
261
+ elif credits <= 3 and semester <= 2:
262
+ return 'beginner'
263
+ else:
264
+ return 'intermediate'
265
+
266
+ def _calculate_recommended_experience(self, course: Dict) -> Dict:
267
+ """Вычисляет рекомендуемый опыт для курса"""
268
+ difficulty = course.get('difficulty', 'intermediate')
269
+ tags = course.get('tags', [])
270
+
271
+ experience = {
272
+ 'programming': 1,
273
+ 'math': 1,
274
+ 'ml': 0
275
+ }
276
+
277
+ if difficulty == 'advanced':
278
+ experience['programming'] = 4
279
+ experience['math'] = 3
280
+ elif difficulty == 'intermediate':
281
+ experience['programming'] = 2
282
+ experience['math'] = 2
283
+ else: # beginner
284
+ experience['programming'] = 1
285
+ experience['math'] = 1
286
+
287
+ # Корректировка по тегам
288
+ if 'ml' in tags or 'dl' in tags:
289
+ experience['ml'] = max(experience['ml'], 1)
290
+ if 'math' in tags or 'stats' in tags:
291
+ experience['math'] = max(experience['math'], 2)
292
+ if 'python' in tags or 'java' in tags:
293
+ experience['programming'] = max(experience['programming'], 2)
294
+
295
+ return experience
296
+
297
+ def _determine_category(self, course: Dict) -> str:
298
+ """Определяет категорию курса"""
299
+ tags = course.get('tags', [])
300
+ name = course.get('name', '').lower()
301
+
302
+ if any(tag in tags for tag in ['ml', 'dl', 'nlp', 'cv']):
303
+ return 'ai_core'
304
+ elif any(tag in tags for tag in ['product', 'business', 'pm']):
305
+ return 'product_management'
306
+ elif any(tag in tags for tag in ['math', 'stats']):
307
+ return 'mathematics'
308
+ elif any(tag in tags for tag in ['systems', 'data']):
309
+ return 'systems_data'
310
+ elif 'research' in tags or 'диссертация' in name:
311
+ return 'research'
312
+ else:
313
+ return 'general'
314
 
315
  def main():
316
  normalizer = DataNormalizer()
317
 
318
+ # Тестовые курсы
319
  test_courses = [
320
  {
321
  'id': 'test_1',
 
336
  ]
337
 
338
  normalized = normalizer.normalize_courses(test_courses)
339
+ enriched = normalizer.enrich_courses(normalized)
340
+ stats = normalizer.get_statistics(enriched)
341
 
342
  print(f'Нормализовано курсов: {len(normalized)}')
343
  print(f'Статистика: {stats}')
344
+
345
+ for course in enriched:
346
+ print(f"- {course['name']}: {course['tags']} (сложность: {course['difficulty']})")
347
 
348
  if __name__ == '__main__':
349
  main()
scraper/pdf_parser.py CHANGED
@@ -1,9 +1,10 @@
1
- import pdfplumber
2
  import requests
 
 
3
  import re
4
  from typing import List, Dict
5
- import os
6
- from tqdm import tqdm
7
 
8
  class PDFParser:
9
  def __init__(self):
@@ -13,232 +14,291 @@ class PDFParser:
13
  })
14
 
15
  def download_pdf(self, url: str, filename: str) -> str:
16
- local_path = os.path.join('data/raw', filename)
17
-
18
- if os.path.exists(local_path):
19
- print(f'PDF уже загружен: {filename}')
20
- return local_path
21
-
22
  try:
23
- print(f'Загрузка PDF: {url}')
24
  response = self.session.get(url, stream=True, timeout=60)
25
  response.raise_for_status()
26
 
 
27
  os.makedirs('data/raw', exist_ok=True)
28
 
29
- with open(local_path, 'wb') as f:
 
 
30
  for chunk in response.iter_content(chunk_size=8192):
31
  f.write(chunk)
32
 
33
- print(f'PDF сохранен: {local_path}')
34
- return local_path
35
 
36
  except Exception as e:
37
- print(f'Ошибка загрузки PDF {url}: {e}')
38
  return None
39
 
40
- def parse_pdf(self, pdf_path: str, program_id: str) -> List[Dict]:
 
41
  courses = []
42
 
43
  try:
44
- with pdfplumber.open(pdf_path) as pdf:
45
- print(f'Парсинг PDF: {pdf_path}')
46
-
47
- for page_num, page in enumerate(tqdm(pdf.pages, desc='Страницы')):
48
- page_courses = self._parse_page(page, page_num + 1, program_id)
49
- courses.extend(page_courses)
50
-
51
- print(f'Найдено курсов: {len(courses)}')
52
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  except Exception as e:
54
- print(f'Ошибка парсинга PDF {pdf_path}: {e}')
55
-
56
- return courses
57
 
58
- def _parse_page(self, page, page_num: int, program_id: str) -> List[Dict]:
 
59
  courses = []
 
60
 
61
- try:
62
- tables = page.extract_tables()
63
-
64
- for table in tables:
65
- table_courses = self._parse_table(table, page_num, program_id)
66
- courses.extend(table_courses)
67
-
68
- if not courses:
69
- courses = self._parse_text_fallback(page, page_num, program_id)
70
 
71
- except Exception as e:
72
- print(f'Ошибка парсинга страницы {page_num}: {e}')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  return courses
75
 
76
- def _parse_table(self, table: list, page_num: int, program_id: str) -> List[Dict]:
 
77
  courses = []
 
78
 
79
- if not table or len(table) < 2:
80
- return courses
81
-
82
- headers = [str(cell).lower().strip() if cell else '' for cell in table[0]]
83
-
84
- for row_idx, row in enumerate(table[1:], 1):
85
- if not row or len(row) < 3:
86
- continue
 
 
 
 
 
 
87
 
88
- course = self._extract_course_from_row(row, headers, page_num, program_id)
89
- if course:
90
- courses.append(course)
91
 
92
  return courses
93
 
94
- def _extract_course_from_row(self, row: list, headers: list, page_num: int, program_id: str) -> Dict:
95
- try:
96
- row = [str(cell).strip() if cell else '' for cell in row]
97
-
98
- name = self._extract_name(row, headers)
99
- if not name or len(name) < 3:
100
- return None
101
-
102
- semester = self._extract_semester(row, headers)
103
- credits = self._extract_credits(row, headers)
104
- hours = self._extract_hours(row, headers)
105
- course_type = self._extract_type(row, headers)
106
-
107
- course = {
108
- 'id': f'{program_id}_{page_num}_{hash(name) % 10000}',
109
- 'program_id': program_id,
110
- 'semester': semester,
111
- 'name': name,
112
- 'credits': credits,
113
- 'hours': hours,
114
- 'type': course_type,
115
- 'source_pdf': os.path.basename(program_id),
116
- 'source_page': page_num
117
- }
118
-
119
- return course
120
-
121
- except Exception as e:
122
- print(f'Ошибка извлечения курса из строки: {e}')
123
- return None
124
-
125
- def _extract_name(self, row: list, headers: list) -> str:
126
- name_indicators = ['название', 'дисциплина', 'курс', 'предмет', 'name', 'course']
127
 
128
- for i, header in enumerate(headers):
129
- if any(indicator in header for indicator in name_indicators):
130
- if i < len(row) and row[i]:
131
- return row[i]
132
 
133
- if len(row) > 0 and row[0]:
134
- return row[0]
 
 
135
 
136
- return ''
137
-
138
- def _extract_semester(self, row: list, headers: list) -> int:
139
- semester_indicators = ['семестр', 'semester', 'сем']
140
-
141
- for i, header in enumerate(headers):
142
- if any(indicator in header for indicator in semester_indicators):
143
- if i < len(row) and row[i]:
144
- try:
145
- return int(re.findall(r'\d+', row[i])[0])
146
- except:
147
- pass
148
-
149
- return 1
150
-
151
- def _extract_credits(self, row: list, headers: list) -> int:
152
- credit_indicators = ['кредит', 'credit', 'зет', 'з.е.']
153
-
154
- for i, header in enumerate(headers):
155
- if any(indicator in header for indicator in credit_indicators):
156
- if i < len(row) and row[i]:
157
- try:
158
- return int(re.findall(r'\d+', row[i])[0])
159
- except:
160
- pass
161
-
162
- return 0
163
 
164
- def _extract_hours(self, row: list, headers: list) -> int:
165
- hour_indicators = ['час', 'hour', уд']
166
-
167
- for i, header in enumerate(headers):
168
- if any(indicator in header for indicator in hour_indicators):
169
- if i < len(row) and row[i]:
170
- try:
171
- return int(re.findall(r'\d+', row[i])[0])
172
- except:
173
- pass
174
-
175
- return 0
176
 
177
- def _extract_type(self, row: list, headers: list) -> str:
178
- type_indicators = ['тип', 'type', 'вид']
179
-
180
- for i, header in enumerate(headers):
181
- if any(indicator in header for indicator in type_indicators):
182
- if i < len(row) and row[i]:
183
- text = row[i].lower()
184
- if any(word in text for word in ['обязательная', 'required', 'обяз']):
185
- return 'required'
186
- elif any(word in text for word in ['по выбору', 'elective', 'выбор']):
187
- return 'elective'
188
-
189
- return 'required'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- def _parse_text_fallback(self, page, page_num: int, program_id: str) -> List[Dict]:
 
192
  courses = []
193
 
194
- try:
195
- text = page.extract_text()
196
- if not text:
197
- return courses
198
-
199
- lines = text.split('\n')
200
- current_semester = 1
201
 
202
- for line in lines:
203
- line = line.strip()
204
- if not line:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  continue
206
 
207
- if еместр' in line.lower():
208
- semester_match = re.findall(r'\d+', line)
209
- if semester_match:
210
- current_semester = int(semester_match[0])
211
- continue
212
 
213
- if len(line) > 10 and not line.isdigit():
214
- course = {
215
- 'id': f'{program_id}_{page_num}_{hash(line) % 10000}',
216
- 'program_id': program_id,
217
- 'semester': current_semester,
218
- 'name': line,
219
- 'credits': 0,
220
- 'hours': 0,
221
- 'type': 'required',
222
- 'source_pdf': os.path.basename(program_id),
223
- 'source_page': page_num
224
- }
225
- courses.append(course)
226
-
227
- except Exception as e:
228
- print(f'Ошибка fallback парсинга страницы {page_num}: {e}')
 
 
 
 
 
 
 
 
 
 
229
 
230
- return courses
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
  def main():
233
  parser = PDFParser()
234
 
235
- test_url = 'https://example.com/test.pdf'
236
- test_filename = 'test.pdf'
 
237
 
238
- local_path = parser.download_pdf(test_url, test_filename)
239
- if local_path:
240
- courses = parser.parse_pdf(local_path, 'test_program')
241
- print(f'Найдено курсов: {len(courses)}')
 
 
 
242
 
243
  if __name__ == '__main__':
244
  main()
 
 
1
  import requests
2
+ import pdfplumber
3
+ import os
4
  import re
5
  from typing import List, Dict
6
+ import tempfile
7
+ from urllib.parse import urlparse
8
 
9
  class PDFParser:
10
  def __init__(self):
 
14
  })
15
 
16
  def download_pdf(self, url: str, filename: str) -> str:
17
+ """Скачивает PDF файл и сохраняет локально"""
 
 
 
 
 
18
  try:
19
+ print(f'Скачивание PDF: {filename}')
20
  response = self.session.get(url, stream=True, timeout=60)
21
  response.raise_for_status()
22
 
23
+ # Создаем директорию если не существует
24
  os.makedirs('data/raw', exist_ok=True)
25
 
26
+ # Сохраняем файл
27
+ filepath = os.path.join('data/raw', filename)
28
+ with open(filepath, 'wb') as f:
29
  for chunk in response.iter_content(chunk_size=8192):
30
  f.write(chunk)
31
 
32
+ print(f'PDF сохранен: {filepath}')
33
+ return filepath
34
 
35
  except Exception as e:
36
+ print(f'Ошибка скачивания PDF {url}: {e}')
37
  return None
38
 
39
+ def parse_pdf(self, filepath: str, program_id: str) -> List[Dict]:
40
+ """Парсит PDF и извлекает информацию о курсах"""
41
  courses = []
42
 
43
  try:
44
+ print(f'Парсинг PDF: {filepath}')
45
+
46
+ with pdfplumber.open(filepath) as pdf:
47
+ # Пробуем извлечь таблицы
48
+ table_courses = self._extract_from_tables(pdf, program_id)
49
+ if table_courses:
50
+ courses.extend(table_courses)
51
+ print(f'Извлечено из таблиц: {len(table_courses)} курсов')
52
 
53
+ # Если таблиц нет или мало курсов, пробуем текстовый парсинг
54
+ if len(courses) < 5:
55
+ text_courses = self._extract_from_text(pdf, program_id)
56
+ courses.extend(text_courses)
57
+ print(f'Извлечено из текста: {len(text_courses)} курсов')
58
+
59
+ # Дедупликация курсов
60
+ courses = self._deduplicate_courses(courses)
61
+
62
+ print(f'Всего извлечено курсов: {len(courses)}')
63
+ return courses
64
+
65
  except Exception as e:
66
+ print(f'Ошибка парсинга PDF {filepath}: {e}')
67
+ return []
 
68
 
69
+ def _extract_from_tables(self, pdf, program_id: str) -> List[Dict]:
70
+ """Извлекает курсы из таблиц PDF"""
71
  courses = []
72
+ current_semester = 1
73
 
74
+ for page_num, page in enumerate(pdf.pages):
75
+ try:
76
+ # Извлекаем таблицы
77
+ tables = page.extract_tables()
 
 
 
 
 
78
 
79
+ for table in tables:
80
+ if not table or len(table) < 2:
81
+ continue
82
+
83
+ # Определяем семестр по заголовкам
84
+ semester = self._detect_semester_from_table(table, current_semester)
85
+ if semester:
86
+ current_semester = semester
87
+
88
+ # Парсим строки таблицы
89
+ for row in table[1:]: # Пропускаем заголовок
90
+ if not row or len(row) < 2:
91
+ continue
92
+
93
+ course = self._parse_table_row(row, program_id, current_semester, page_num + 1)
94
+ if course:
95
+ courses.append(course)
96
+
97
+ except Exception as e:
98
+ print(f'Ошибка обработки страницы {page_num + 1}: {e}')
99
+ continue
100
 
101
  return courses
102
 
103
+ def _extract_from_text(self, pdf, program_id: str) -> List[Dict]:
104
+ """Извлекает курсы из текста PDF"""
105
  courses = []
106
+ current_semester = 1
107
 
108
+ for page_num, page in enumerate(pdf.pages):
109
+ try:
110
+ text = page.extract_text()
111
+ if not text:
112
+ continue
113
+
114
+ # Определяем семестр по тексту
115
+ semester = self._detect_semester_from_text(text, current_semester)
116
+ if semester:
117
+ current_semester = semester
118
+
119
+ # Ищем курсы в тексте
120
+ page_courses = self._parse_text_for_courses(text, program_id, current_semester, page_num + 1)
121
+ courses.extend(page_courses)
122
 
123
+ except Exception as e:
124
+ print(f'Ошибка обработки текста страницы {page_num + 1}: {e}')
125
+ continue
126
 
127
  return courses
128
 
129
+ def _detect_semester_from_table(self, table: List[List], current_semester: int) -> int:
130
+ """Определяет семестр по заголовкам таблицы"""
131
+ if not table or not table[0]:
132
+ return current_semester
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
+ header_text = ' '.join([str(cell) for cell in table[0] if cell]).lower()
 
 
 
135
 
136
+ # Поиск упоминаний семестров
137
+ for i in range(1, 5):
138
+ if f'{i} семестр' in header_text or f'{i} семестре' in header_text:
139
+ return i
140
 
141
+ return current_semester
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ def _detect_semester_from_text(self, text: str, current_semester: int) -> int:
144
+ """Определяет семестр по тексту"""
145
+ text_lower = text.lower()
146
+
147
+ # Поиск упоминаний семестров
148
+ for i in range(1, 5):
149
+ if f'{i} семестр' in text_lower or f'{i} семестре' in text_lower:
150
+ return i
151
+
152
+ return current_semester
 
 
153
 
154
+ def _parse_table_row(self, row: List, program_id: str, semester: int, page: int) -> Dict:
155
+ """Парсит строку таблицы и извлекает информацию о курсе"""
156
+ if not row or len(row) < 2:
157
+ return None
158
+
159
+ # Очищаем ячейки от лишних символов
160
+ clean_row = [str(cell).strip() if cell else '' for cell in row]
161
+
162
+ # Ищем название курса (обычно в первой или второй колонке)
163
+ course_name = ''
164
+ credits = 0
165
+ hours = 0
166
+ course_type = 'required'
167
+
168
+ for i, cell in enumerate(clean_row):
169
+ if not cell or cell.lower() in ['название', 'дисциплина', 'курс', 'предмет']:
170
+ continue
171
+
172
+ # Если это похоже на название курса
173
+ if len(cell) > 10 and not cell.isdigit():
174
+ course_name = cell
175
+ break
176
+
177
+ # Ищем кредиты и часы
178
+ for cell in clean_row:
179
+ if cell.isdigit():
180
+ num = int(cell)
181
+ if 1 <= num <= 12: # Кредиты обычно 1-12
182
+ credits = num
183
+ elif 18 <= num <= 216: # Часы обычно 18-216
184
+ hours = num
185
+
186
+ # Определяем тип курса
187
+ row_text = ' '.join(clean_row).lower()
188
+ if any(word in row_text for word in ['по выбору', 'электив', 'факультатив']):
189
+ course_type = 'elective'
190
+
191
+ if not course_name or len(course_name) < 5:
192
+ return None
193
+
194
+ return {
195
+ 'id': f'{program_id}_{semester}_{len(course_name)}',
196
+ 'program_id': program_id,
197
+ 'semester': semester,
198
+ 'name': course_name,
199
+ 'credits': credits,
200
+ 'hours': hours,
201
+ 'type': course_type,
202
+ 'source_pdf': os.path.basename(filepath) if 'filepath' in locals() else '',
203
+ 'source_page': page
204
+ }
205
 
206
+ def _parse_text_for_courses(self, text: str, program_id: str, semester: int, page: int) -> List[Dict]:
207
+ """Парсит текст и ищет курсы"""
208
  courses = []
209
 
210
+ # Разбиваем текст на строки
211
+ lines = text.split('\n')
212
+
213
+ for line in lines:
214
+ line = line.strip()
215
+ if not line or len(line) < 10:
216
+ continue
217
 
218
+ # Ищем паттерны курсов
219
+ course = self._extract_course_from_line(line, program_id, semester, page)
220
+ if course:
221
+ courses.append(course)
222
+
223
+ return courses
224
+
225
+ def _extract_course_from_line(self, line: str, program_id: str, semester: int, page: int) -> Dict:
226
+ """Извлекает информацию о курсе из строки текста"""
227
+ # Паттерны для поиска курсов
228
+ patterns = [
229
+ r'([А-Я][А-Яа-я\s\-\(\)]+?)\s+(\d+)\s+(\d+)', # Название + кредиты + часы
230
+ r'([А-Я][А-Яа-я\s\-\(\)]+?)\s+(\d+)\s*кр', # Название + кредиты
231
+ r'([А-Я][А-Яа-я\s\-\(\)]+?)\s+(\d+)\s*ч', # Название + часы
232
+ ]
233
+
234
+ for pattern in patterns:
235
+ match = re.search(pattern, line)
236
+ if match:
237
+ course_name = match.group(1).strip()
238
+ if len(course_name) < 5:
239
  continue
240
 
241
+ # Извлекаем числа
242
+ numbers = [int(match.group(i)) for i in range(2, len(match.groups()) + 1)]
 
 
 
243
 
244
+ credits = 0
245
+ hours = 0
246
+
247
+ if len(numbers) >= 2:
248
+ credits, hours = numbers[0], numbers[1]
249
+ elif len(numbers) == 1:
250
+ if numbers[0] <= 12:
251
+ credits = numbers[0]
252
+ else:
253
+ hours = numbers[0]
254
+
255
+ # Определяем тип курса
256
+ course_type = 'required'
257
+ if any(word in line.lower() for word in ['по выбору', 'электив', 'факультатив']):
258
+ course_type = 'elective'
259
+
260
+ return {
261
+ 'id': f'{program_id}_{semester}_{len(course_name)}',
262
+ 'program_id': program_id,
263
+ 'semester': semester,
264
+ 'name': course_name,
265
+ 'credits': credits,
266
+ 'hours': hours,
267
+ 'type': course_type,
268
+ 'source_page': page
269
+ }
270
 
271
+ return None
272
+
273
+ def _deduplicate_courses(self, courses: List[Dict]) -> List[Dict]:
274
+ """Удаляет дубликаты курсов"""
275
+ seen = set()
276
+ unique_courses = []
277
+
278
+ for course in courses:
279
+ # Создаем ключ для дедупликации
280
+ key = f"{course['name']}_{course['semester']}_{course['program_id']}"
281
+
282
+ if key not in seen:
283
+ seen.add(key)
284
+ unique_courses.append(course)
285
+
286
+ return unique_courses
287
 
288
  def main():
289
  parser = PDFParser()
290
 
291
+ # Тестовый URL (замените на реальный)
292
+ test_url = "https://example.com/test.pdf"
293
+ filename = "test_curriculum.pdf"
294
 
295
+ # Скачивание и парсинг
296
+ filepath = parser.download_pdf(test_url, filename)
297
+ if filepath:
298
+ courses = parser.parse_pdf(filepath, 'test_program')
299
+ print(f'Извлечено курсов: {len(courses)}')
300
+ for course in courses[:5]:
301
+ print(f"- {course['name']} ({course['semester']} семестр, {course['credits']} кредитов)")
302
 
303
  if __name__ == '__main__':
304
  main()
test_chatbot.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from chatbot import ITMOChatbot
5
+ from knowledge_base import KnowledgeBase
6
+
7
+ def test_chatbot():
8
+ print("🧪 Тестирование чат-бота...")
9
+
10
+ # Инициализация
11
+ try:
12
+ chatbot = ITMOChatbot()
13
+ print("✅ Чат-бот инициализирован")
14
+ except Exception as e:
15
+ print(f"❌ Ошибка инициализации: {e}")
16
+ return
17
+
18
+ # Проверка базы знаний
19
+ kb = KnowledgeBase()
20
+ print(f"📊 Курсов в базе: {len(kb.courses)}")
21
+
22
+ # Тест поиска курсов по семестрам
23
+ for semester in [1, 2, 3, 4]:
24
+ courses = kb.get_courses_by_semester(semester)
25
+ print(f"📚 Семестр {semester}: {len(courses)} курсов")
26
+ if courses:
27
+ print(f" Пример: {courses[0]['name']}")
28
+
29
+ # Тест чата
30
+ print("\n💬 Тест чата:")
31
+ test_messages = [
32
+ "Какие дисциплины по NLP в 1 семестре программы ИИ?",
33
+ "Расскажи о программе AI Product",
34
+ "Сколько кредитов за курс машинного обучения?"
35
+ ]
36
+
37
+ history = []
38
+ for message in test_messages:
39
+ print(f"\n👤 Вопрос: {message}")
40
+ try:
41
+ response, score = chatbot.chat(message, history)
42
+ print(f"🤖 Ответ: {response[:100]}...")
43
+ print(f"📊 Релевантность: {score:.2f}")
44
+ history.append([message, response])
45
+ except Exception as e:
46
+ print(f"❌ Ошибка: {e}")
47
+
48
+ # Тест рекомендаций
49
+ print("\n🎯 Тест рекомендаций:")
50
+ test_profiles = [
51
+ {
52
+ 'programming_experience': 4,
53
+ 'math_level': 3,
54
+ 'interests': ['ml', 'dl', 'python'],
55
+ 'semester': 1
56
+ },
57
+ {
58
+ 'programming_experience': 2,
59
+ 'math_level': 2,
60
+ 'interests': ['product', 'business'],
61
+ 'semester': 2
62
+ }
63
+ ]
64
+
65
+ for i, profile in enumerate(test_profiles, 1):
66
+ print(f"\n👤 Профиль {i}: {profile}")
67
+ try:
68
+ recommendations = chatbot.recommend_courses(profile)
69
+ print(f"🎯 Рекомендации: {recommendations[:200]}...")
70
+ except Exception as e:
71
+ print(f"❌ Ошибка: {e}")
72
+
73
+ print("\n✅ Тестирование завершено!")
74
+
75
+ if __name__ == '__main__':
76
+ test_chatbot()
update_data.py CHANGED
@@ -2,36 +2,90 @@ import json
2
  import os
3
  import sys
4
  from typing import List, Dict
 
 
 
5
  from knowledge_base import KnowledgeBase
6
  from retriever import Retriever
7
 
8
  def update_data_async():
9
  try:
10
- print('Начинаем обновление данных...')
11
 
12
- # Проверяем, есть ли уже данные
13
- if check_data_exists():
14
- print('Данные уже существуют, пропускаем обновление')
15
- return
16
-
17
- # Создаем тестовые данные для быстрого старта
18
- print('Создание тестовых данных...')
19
 
20
- # Инициализация базы знаний (создаст тестовые данные)
21
- knowledge_base = KnowledgeBase()
 
 
 
 
22
 
23
- print('Создание индекса...')
24
- retriever = Retriever()
25
- retriever.build_or_load_index(knowledge_base.courses)
26
 
27
- stats = knowledge_base.get_statistics()
28
- print(f'Статистика: {stats}')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- print('Обновление данных завершено успешно!')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  except Exception as e:
33
  print(f'Ошибка обновления данных: {e}')
34
- raise
 
 
 
35
 
36
  def save_courses(courses: List[Dict], output_path: str = 'data/processed/courses.json'):
37
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
@@ -66,26 +120,57 @@ def load_existing_data() -> tuple[Dict, List[Dict]]:
66
 
67
  return programs, courses
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  def initialize_data():
70
  if check_data_exists():
71
- print('Данные уже существуют, загружаем...')
72
- programs, courses = load_existing_data()
73
 
74
- if courses:
75
- retriever = Retriever()
76
- retriever.build_or_load_index(courses)
77
- print(f'Загружено {len(courses)} курсов')
78
- else:
79
- print('Курсы не найдены, запускаем обновление...')
80
  update_data_async()
 
 
 
 
 
 
 
 
 
 
 
81
  else:
82
  print('Данные не найдены, запускаем первичное обновление...')
83
  update_data_async()
84
 
85
  def main():
86
- if len(sys.argv) > 1 and sys.argv[1] == '--force':
87
- print('Принудительное обновление данных...')
88
- update_data_async()
 
 
 
 
 
 
 
 
 
89
  else:
90
  initialize_data()
91
 
 
2
  import os
3
  import sys
4
  from typing import List, Dict
5
+ from scraper.html_scraper import HTMLScraper
6
+ from scraper.pdf_parser import PDFParser
7
+ from scraper.normalize import DataNormalizer
8
  from knowledge_base import KnowledgeBase
9
  from retriever import Retriever
10
 
11
  def update_data_async():
12
  try:
13
+ print('Начинаем обновление данных с сайтов ITMO...')
14
 
15
+ # 1. Скрапинг страниц программ
16
+ scraper = HTMLScraper()
17
+ programs = scraper.scrape_programs()
18
+ scraper.save_programs(programs)
 
 
 
19
 
20
+ if not programs:
21
+ print('Не удалось получить данные программ, используем тестовые данные')
22
+ knowledge_base = KnowledgeBase()
23
+ retriever = Retriever()
24
+ retriever.build_or_load_index(knowledge_base.courses)
25
+ return
26
 
27
+ # 2. Скачивание и парсинг PDF
28
+ pdf_parser = PDFParser()
29
+ all_courses = []
30
 
31
+ for program_id, program in programs.items():
32
+ print(f'\nОбработка программы: {program["title"]}')
33
+
34
+ if not program.get('pdf_links'):
35
+ print(f'PDF ссылки не найдены для программы {program_id}')
36
+ continue
37
+
38
+ for pdf_link in program['pdf_links']:
39
+ try:
40
+ filename = pdf_link['filename']
41
+ url = pdf_link['url']
42
+
43
+ print(f'Скачивание PDF: {filename}')
44
+ local_path = pdf_parser.download_pdf(url, filename)
45
+
46
+ if local_path:
47
+ print(f'Парсинг PDF: {filename}')
48
+ courses = pdf_parser.parse_pdf(local_path, program_id)
49
+ all_courses.extend(courses)
50
+ print(f'Извлечено курсов из {filename}: {len(courses)}')
51
+ else:
52
+ print(f'Не удалось скачать PDF: {filename}')
53
+
54
+ except Exception as e:
55
+ print(f'Ошибка обработки PDF {pdf_link["filename"]}: {e}')
56
 
57
+ # 3. Нормализация данных
58
+ if all_courses:
59
+ print(f'\nНормализация {len(all_courses)} курсов...')
60
+ normalizer = DataNormalizer()
61
+ normalized_courses = normalizer.normalize_courses(all_courses)
62
+ enriched_courses = normalizer.enrich_courses(normalized_courses)
63
+
64
+ # Сохранение курсов
65
+ save_courses(enriched_courses)
66
+
67
+ # 4. Создание индекса
68
+ print('Создание индекса...')
69
+ retriever = Retriever()
70
+ retriever.build_or_load_index(enriched_courses)
71
+
72
+ # Статистика
73
+ stats = normalizer.get_statistics(enriched_courses)
74
+ print(f'Статистика: {stats}')
75
+
76
+ print('Обновление данных завершено успешно!')
77
+ else:
78
+ print('Не удалось извлечь курсы из PDF, используем тестовые данные')
79
+ knowledge_base = KnowledgeBase()
80
+ retriever = Retriever()
81
+ retriever.build_or_load_index(knowledge_base.courses)
82
 
83
  except Exception as e:
84
  print(f'Ошибка обновления данных: {e}')
85
+ print('Используем тестовые данные...')
86
+ knowledge_base = KnowledgeBase()
87
+ retriever = Retriever()
88
+ retriever.build_or_load_index(knowledge_base.courses)
89
 
90
  def save_courses(courses: List[Dict], output_path: str = 'data/processed/courses.json'):
91
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
 
120
 
121
  return programs, courses
122
 
123
+ def check_for_updates() -> bool:
124
+ """Проверяет наличие обновлений на сайтах ITMO"""
125
+ try:
126
+ scraper = HTMLScraper()
127
+ programs, _ = load_existing_data()
128
+
129
+ if not programs:
130
+ return True # Нет данных, нужно обновление
131
+
132
+ updates = scraper.check_updates(programs)
133
+ return len(updates) > 0
134
+
135
+ except Exception as e:
136
+ print(f'Ошибка проверки обновлений: {e}')
137
+ return False
138
+
139
  def initialize_data():
140
  if check_data_exists():
141
+ print('Данные уже существуют, проверяем обновления...')
 
142
 
143
+ if check_for_updates():
144
+ print('Обнаружены обновления, запускаем обновление данных...')
 
 
 
 
145
  update_data_async()
146
+ else:
147
+ print('Обновлений не найдено, загружаем существующие данные...')
148
+ programs, courses = load_existing_data()
149
+
150
+ if courses:
151
+ retriever = Retriever()
152
+ retriever.build_or_load_index(courses)
153
+ print(f'Загружено {len(courses)} курсов')
154
+ else:
155
+ print('Курсы не найдены, запускаем обновление...')
156
+ update_data_async()
157
  else:
158
  print('Данные не найдены, запускаем первичное обновление...')
159
  update_data_async()
160
 
161
  def main():
162
+ if len(sys.argv) > 1:
163
+ if sys.argv[1] == '--force':
164
+ print('Принудительное обновление данных...')
165
+ update_data_async()
166
+ elif sys.argv[1] == '--check':
167
+ print('Проверка обновлений...')
168
+ if check_for_updates():
169
+ print('Обнаружены обновления')
170
+ else:
171
+ print('Обновлений не найдено')
172
+ else:
173
+ print('Использование: python update_data.py [--force|--check]')
174
  else:
175
  initialize_data()
176