vydrking commited on
Commit
a371816
·
verified ·
1 Parent(s): 98a7a72

Update parser.py

Browse files
Files changed (1) hide show
  1. parser.py +313 -314
parser.py CHANGED
@@ -1,314 +1,313 @@
1
- import requests
2
- from bs4 import BeautifulSoup
3
- import re
4
- import json
5
- import os
6
- from typing import List, Dict, Optional
7
- import logging
8
-
9
- logging.basicConfig(level=logging.INFO)
10
- logger = logging.getLogger(__name__)
11
-
12
- def parse_program_page(url: str, program_id: str) -> Dict:
13
- """Парсинг страницы программы"""
14
- try:
15
- logger.info(f'Парсинг страницы {program_id}: {url}')
16
- response = requests.get(url, timeout=10)
17
- response.raise_for_status()
18
-
19
- soup = BeautifulSoup(response.content, 'html.parser')
20
-
21
- # Ищем заголовок
22
- title = soup.find('h1')
23
- title_text = title.get_text().strip() if title else f'Программа {program_id}'
24
-
25
- # Ищем описание
26
- description = soup.find('div', class_='description') or soup.find('p')
27
- desc_text = description.get_text().strip() if description else f'Описание программы {program_id}'
28
-
29
- # Ищем ссылки на PDF
30
- pdf_links = []
31
- for link in soup.find_all('a', href=True):
32
- href = link['href']
33
- if '.pdf' in href.lower() or 'curriculum' in href.lower() or 'plan' in href.lower():
34
- if href.startswith('/'):
35
- href = 'https://abit.itmo.ru' + href
36
- elif not href.startswith('http'):
37
- href = 'https://abit.itmo.ru/' + href
38
- pdf_links.append(href)
39
-
40
- logger.info(f'Найдено {len(pdf_links)} PDF ссылок для {program_id}')
41
-
42
- return {
43
- 'title': title_text,
44
- 'description': desc_text,
45
- 'pdf_links': pdf_links,
46
- 'source_url': url
47
- }
48
-
49
- except Exception as e:
50
- logger.error(f'Ошибка парсинга страницы {program_id}: {e}')
51
- return {
52
- 'title': f'Программа {program_id}',
53
- 'description': f'Описание программы {program_id}',
54
- 'pdf_links': [],
55
- 'source_url': url
56
- }
57
-
58
- def parse_pdf(url: str, program_id: str) -> List[Dict]:
59
- """Парсинг PDF файла с учебным планом"""
60
- try:
61
- logger.info(f'Попытка парсинга PDF: {url}')
62
-
63
- # Пока используем заглушку, так как PDF парсинг сложен
64
- # В реальной реализации здесь был бы код для извлечения таблиц из PDF
65
-
66
- # Возвращаем пустой список, чтобы не ломать приложение
67
- return []
68
-
69
- except Exception as e:
70
- logger.error(f'Ошибка парсинга PDF {url}: {e}')
71
- return []
72
-
73
- def normalize_course(course_data: Dict, program_id: str) -> Dict:
74
- """Нормализация данных курса"""
75
- # Создаем short_desc из названия если нет
76
- if 'short_desc' not in course_data:
77
- course_data['short_desc'] = course_data.get('name', '')[:200]
78
-
79
- # Генерируем теги на основе названия и описания
80
- text = f"{course_data.get('name', '')} {course_data.get('short_desc', '')}".lower()
81
- tags = []
82
-
83
- if any(word in text for word in ['машинное обучение', 'ml', 'machine learning']):
84
- tags.append('ml')
85
- if any(word in text for word in ['глубокое обучение', 'dl', 'neural', 'нейрон']):
86
- tags.append('dl')
87
- if any(word in text for word in ['nlp', 'язык', 'текст', 'natural language']):
88
- tags.append('nlp')
89
- if any(word in text for word in ['зрение', 'vision', 'image', 'изображение']):
90
- tags.append('cv')
91
- if any(word in text for word in ['продукт', 'product', 'менеджмент', 'management']):
92
- tags.append('product')
93
- if any(word in text for word in ['бизнес', 'business', 'аналитика', 'analytics']):
94
- tags.append('business')
95
- if any(word in text for word in ['исследование', 'research', 'наука']):
96
- tags.append('research')
97
- if any(word in text for word in ['данные', 'data', 'статистика']):
98
- tags.append('data')
99
- if any(word in text for word in ['системы', 'systems', 'архитектура']):
100
- tags.append('systems')
101
- if any(word in text for word in ['python', 'программирование']):
102
- tags.append('python')
103
- if any(word in text for word in ['математика', 'math', 'статистика', 'оптимизация']):
104
- tags.append('math')
105
-
106
- course_data['tags'] = tags
107
- course_data['program_id'] = program_id
108
-
109
- return course_data
110
-
111
- def get_fallback_courses() -> List[Dict]:
112
- """Fallback курсы на случай недоступности парсинга"""
113
- return [
114
- # Программа ИИ
115
- {
116
- 'id': 'ai_1_1',
117
- 'program_id': 'ai',
118
- 'semester': 1,
119
- 'name': 'Машинное обучение',
120
- 'credits': 6,
121
- 'hours': 108,
122
- 'type': 'required',
123
- 'short_desc': 'Основы машинного обучения, алгоритмы классификации и регрессии',
124
- 'tags': ['ml', 'math', 'stats', 'python'],
125
- 'source_url': 'https://abit.itmo.ru/program/master/ai'
126
- },
127
- {
128
- 'id': 'ai_1_2',
129
- 'program_id': 'ai',
130
- 'semester': 1,
131
- 'name': 'Глубокое обучение',
132
- 'credits': 4,
133
- 'hours': 72,
134
- 'type': 'required',
135
- 'short_desc': 'Нейронные сети, CNN, RNN, трансформеры',
136
- 'tags': ['dl', 'ml', 'neural', 'python'],
137
- 'source_url': 'https://abit.itmo.ru/program/master/ai'
138
- },
139
- {
140
- 'id': 'ai_2_1',
141
- 'program_id': 'ai',
142
- 'semester': 2,
143
- 'name': 'Обработка естественного языка',
144
- 'credits': 5,
145
- 'hours': 90,
146
- 'type': 'required',
147
- 'short_desc': 'Методы обработки текста, токенизация, эмбеддинги',
148
- 'tags': ['nlp', 'dl', 'text', 'transformers'],
149
- 'source_url': 'https://abit.itmo.ru/program/master/ai'
150
- },
151
- {
152
- 'id': 'ai_2_2',
153
- 'program_id': 'ai',
154
- 'semester': 2,
155
- 'name': 'Компьютерное зрение',
156
- 'credits': 4,
157
- 'hours': 72,
158
- 'type': 'required',
159
- 'short_desc': 'Обработка изображений, CNN, детекция объектов',
160
- 'tags': ['cv', 'dl', 'image', 'cnn'],
161
- 'source_url': 'https://abit.itmo.ru/program/master/ai'
162
- },
163
- {
164
- 'id': 'ai_3_1',
165
- 'program_id': 'ai',
166
- 'semester': 3,
167
- 'name': 'Продвинутые методы машинного обучения',
168
- 'credits': 5,
169
- 'hours': 90,
170
- 'type': 'required',
171
- 'short_desc': 'Продвинутые алгоритмы ML, ансамбли, оптимизация',
172
- 'tags': ['ml', 'advanced', 'algorithms'],
173
- 'source_url': 'https://abit.itmo.ru/program/master/ai'
174
- },
175
- {
176
- 'id': 'ai_4_1',
177
- 'program_id': 'ai',
178
- 'semester': 4,
179
- 'name': 'Магистерская диссертация',
180
- 'credits': 12,
181
- 'hours': 216,
182
- 'type': 'required',
183
- 'short_desc': 'Научно-исследовательская работа, защита диссертации',
184
- 'tags': ['research', 'thesis', 'project'],
185
- 'source_url': 'https://abit.itmo.ru/program/master/ai'
186
- },
187
-
188
- # Программа AI Product
189
- {
190
- 'id': 'ai_product_1_1',
191
- 'program_id': 'ai_product',
192
- 'semester': 1,
193
- 'name': 'Продуктовая аналитика',
194
- 'credits': 6,
195
- 'hours': 108,
196
- 'type': 'required',
197
- 'short_desc': 'Анализ продуктовых метрик, A/B тестирование',
198
- 'tags': ['product', 'business', 'data', 'analytics'],
199
- 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
200
- },
201
- {
202
- 'id': 'ai_product_1_2',
203
- 'program_id': 'ai_product',
204
- 'semester': 1,
205
- 'name': 'Управление проектами',
206
- 'credits': 4,
207
- 'hours': 72,
208
- 'type': 'required',
209
- 'short_desc': 'Методологии управления проектами, Agile, Scrum',
210
- 'tags': ['pm', 'business', 'management', 'agile'],
211
- 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
212
- },
213
- {
214
- 'id': 'ai_product_2_1',
215
- 'program_id': 'ai_product',
216
- 'semester': 2,
217
- 'name': 'UX/UI для ИИ продуктов',
218
- 'credits': 4,
219
- 'hours': 72,
220
- 'type': 'required',
221
- 'short_desc': 'Дизайн интерфейсов для ИИ, UX исследования',
222
- 'tags': ['ux', 'ui', 'design', 'ai'],
223
- 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
224
- },
225
- {
226
- 'id': 'ai_product_2_2',
227
- 'program_id': 'ai_product',
228
- 'semester': 2,
229
- 'name': 'Эт��ка ИИ',
230
- 'credits': 3,
231
- 'hours': 54,
232
- 'type': 'required',
233
- 'short_desc': 'Этические принципы ИИ, справедливость, прозрачность',
234
- 'tags': ['ethics', 'ai', 'responsible', 'fairness'],
235
- 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
236
- },
237
- {
238
- 'id': 'ai_product_3_1',
239
- 'program_id': 'ai_product',
240
- 'semester': 3,
241
- 'name': 'Управление ИИ продуктами',
242
- 'credits': 6,
243
- 'hours': 108,
244
- 'type': 'required',
245
- 'short_desc': 'Стратегическое управление ИИ продуктами, команды',
246
- 'tags': ['product', 'management', 'ai', 'leadership'],
247
- 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
248
- },
249
- {
250
- 'id': 'ai_product_4_1',
251
- 'program_id': 'ai_product',
252
- 'semester': 4,
253
- 'name': 'Дипломный проект',
254
- 'credits': 12,
255
- 'hours': 216,
256
- 'type': 'required',
257
- 'short_desc': 'Разработка ИИ продукта, защита проекта',
258
- 'tags': ['project', 'thesis', 'product'],
259
- 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
260
- }
261
- ]
262
-
263
- def parse_all() -> bool:
264
- """Основная функция парсинга всех данных"""
265
- try:
266
- logger.info('Начинаем парсинг всех данных')
267
-
268
- # Создаем директории если нет
269
- os.makedirs('data/processed', exist_ok=True)
270
-
271
- # Парсим страницы программ
272
- programs = {
273
- 'ai': 'https://abit.itmo.ru/program/master/ai',
274
- 'ai_product': 'https://abit.itmo.ru/program/master/ai_product'
275
- }
276
-
277
- all_courses = []
278
-
279
- for program_id, url in programs.items():
280
- # Парсим страницу программы
281
- program_info = parse_program_page(url, program_id)
282
-
283
- # Пытаемся парсить PDF файлы
284
- for pdf_url in program_info['pdf_links']:
285
- pdf_courses = parse_pdf(pdf_url, program_id)
286
- for course in pdf_courses:
287
- normalized_course = normalize_course(course, program_id)
288
- all_courses.append(normalized_course)
289
-
290
- # Если парсинг не дал результатов, используем fallback
291
- if not all_courses:
292
- logger.warning('Парсинг не дал результатов, используем fallback курсы')
293
- all_courses = get_fallback_courses()
294
-
295
- # Сохраняем в JSON
296
- courses_file = 'data/processed/courses.json'
297
- with open(courses_file, 'w', encoding='utf-8') as f:
298
- json.dump(all_courses, f, ensure_ascii=False, indent=2)
299
-
300
- logger.info(f'Сохранено {len(all_courses)} курсов в {courses_file}')
301
- return True
302
-
303
- except Exception as e:
304
- logger.error(f'Ошибка парсинга: {e}')
305
- # Сохраняем fallback курсы
306
- try:
307
- os.makedirs('data/processed', exist_ok=True)
308
- with open('data/processed/courses.json', 'w', encoding='utf-8') as f:
309
- json.dump(get_fallback_courses(), f, ensure_ascii=False, indent=2)
310
- logger.info('Сохранены fallback курсы')
311
- return True
312
- except Exception as e2:
313
- logger.error(f'Ошибка сохранения fallback курсов: {e2}')
314
- return False
 
1
+ import requests
2
+ from bs4 import BeautifulSoup
3
+ import re
4
+ import json
5
+ import os
6
+ import logging
7
+
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ def parse_program_page(url, program_id):
12
+ """Парсинг страницы программы"""
13
+ try:
14
+ logger.info(f'Парсинг страницы {program_id}: {url}')
15
+ response = requests.get(url, timeout=10)
16
+ response.raise_for_status()
17
+
18
+ soup = BeautifulSoup(response.content, 'html.parser')
19
+
20
+ # Ищем заголовок
21
+ title = soup.find('h1')
22
+ title_text = title.get_text().strip() if title else f'Программа {program_id}'
23
+
24
+ # Ищем описание
25
+ description = soup.find('div', class_='description') or soup.find('p')
26
+ desc_text = description.get_text().strip() if description else f'Описание программы {program_id}'
27
+
28
+ # Ищем ссылки на PDF
29
+ pdf_links = []
30
+ for link in soup.find_all('a', href=True):
31
+ href = link['href']
32
+ if '.pdf' in href.lower() or 'curriculum' in href.lower() or 'plan' in href.lower():
33
+ if href.startswith('/'):
34
+ href = 'https://abit.itmo.ru' + href
35
+ elif not href.startswith('http'):
36
+ href = 'https://abit.itmo.ru/' + href
37
+ pdf_links.append(href)
38
+
39
+ logger.info(f'Найдено {len(pdf_links)} PDF ссылок для {program_id}')
40
+
41
+ return {
42
+ 'title': title_text,
43
+ 'description': desc_text,
44
+ 'pdf_links': pdf_links,
45
+ 'source_url': url
46
+ }
47
+
48
+ except Exception as e:
49
+ logger.error(f'Ошибка парсинга страницы {program_id}: {e}')
50
+ return {
51
+ 'title': f'Программа {program_id}',
52
+ 'description': f'Описание программы {program_id}',
53
+ 'pdf_links': [],
54
+ 'source_url': url
55
+ }
56
+
57
+ def parse_pdf(url, program_id):
58
+ """Парсинг PDF файла с учебным планом"""
59
+ try:
60
+ logger.info(f'Попытка парсинга PDF: {url}')
61
+
62
+ # Пока используем заглушку, так как PDF парсинг сложен
63
+ # В реальной реализации здесь был бы код для извлечения таблиц из PDF
64
+
65
+ # Возвращаем пустой список, чтобы не ломать приложение
66
+ return []
67
+
68
+ except Exception as e:
69
+ logger.error(f'Ошибка парсинга PDF {url}: {e}')
70
+ return []
71
+
72
+ def normalize_course(course_data, program_id):
73
+ """Нормализация данных курса"""
74
+ # Создаем short_desc из названия если нет
75
+ if 'short_desc' not in course_data:
76
+ course_data['short_desc'] = course_data.get('name', '')[:200]
77
+
78
+ # Генерируем теги на основе названия и описания
79
+ text = f"{course_data.get('name', '')} {course_data.get('short_desc', '')}".lower()
80
+ tags = []
81
+
82
+ if any(word in text for word in ['машинное обучение', 'ml', 'machine learning']):
83
+ tags.append('ml')
84
+ if any(word in text for word in ['глубокое обучение', 'dl', 'neural', 'нейрон']):
85
+ tags.append('dl')
86
+ if any(word in text for word in ['nlp', 'язык', 'текст', 'natural language']):
87
+ tags.append('nlp')
88
+ if any(word in text for word in ['зрение', 'vision', 'image', 'изображение']):
89
+ tags.append('cv')
90
+ if any(word in text for word in ['продукт', 'product', 'менеджмент', 'management']):
91
+ tags.append('product')
92
+ if any(word in text for word in ['бизнес', 'business', 'аналитика', 'analytics']):
93
+ tags.append('business')
94
+ if any(word in text for word in ['исследование', 'research', 'наука']):
95
+ tags.append('research')
96
+ if any(word in text for word in ['данные', 'data', 'статистика']):
97
+ tags.append('data')
98
+ if any(word in text for word in ['системы', 'systems', 'архитектура']):
99
+ tags.append('systems')
100
+ if any(word in text for word in ['python', 'программирование']):
101
+ tags.append('python')
102
+ if any(word in text for word in ['математика', 'math', 'статистика', 'оптимизация']):
103
+ tags.append('math')
104
+
105
+ course_data['tags'] = tags
106
+ course_data['program_id'] = program_id
107
+
108
+ return course_data
109
+
110
+ def get_fallback_courses():
111
+ """Fallback курсы на случай недоступности парсинга"""
112
+ return [
113
+ # Программа ИИ
114
+ {
115
+ 'id': 'ai_1_1',
116
+ 'program_id': 'ai',
117
+ 'semester': 1,
118
+ 'name': 'Машинное обучение',
119
+ 'credits': 6,
120
+ 'hours': 108,
121
+ 'type': 'required',
122
+ 'short_desc': 'Основы машинного обучения, алгоритмы классификации и регрессии',
123
+ 'tags': ['ml', 'math', 'stats', 'python'],
124
+ 'source_url': 'https://abit.itmo.ru/program/master/ai'
125
+ },
126
+ {
127
+ 'id': 'ai_1_2',
128
+ 'program_id': 'ai',
129
+ 'semester': 1,
130
+ 'name': 'Глубокое обучение',
131
+ 'credits': 4,
132
+ 'hours': 72,
133
+ 'type': 'required',
134
+ 'short_desc': 'Нейронные сети, CNN, RNN, трансформеры',
135
+ 'tags': ['dl', 'ml', 'neural', 'python'],
136
+ 'source_url': 'https://abit.itmo.ru/program/master/ai'
137
+ },
138
+ {
139
+ 'id': 'ai_2_1',
140
+ 'program_id': 'ai',
141
+ 'semester': 2,
142
+ 'name': 'Обработка естественного языка',
143
+ 'credits': 5,
144
+ 'hours': 90,
145
+ 'type': 'required',
146
+ 'short_desc': 'Методы обработки текста, токенизация, эмбеддинги',
147
+ 'tags': ['nlp', 'dl', 'text', 'transformers'],
148
+ 'source_url': 'https://abit.itmo.ru/program/master/ai'
149
+ },
150
+ {
151
+ 'id': 'ai_2_2',
152
+ 'program_id': 'ai',
153
+ 'semester': 2,
154
+ 'name': 'Компьютерное зрение',
155
+ 'credits': 4,
156
+ 'hours': 72,
157
+ 'type': 'required',
158
+ 'short_desc': 'Обработка изображений, CNN, детекция объектов',
159
+ 'tags': ['cv', 'dl', 'image', 'cnn'],
160
+ 'source_url': 'https://abit.itmo.ru/program/master/ai'
161
+ },
162
+ {
163
+ 'id': 'ai_3_1',
164
+ 'program_id': 'ai',
165
+ 'semester': 3,
166
+ 'name': 'Продвинутые методы машинного обучения',
167
+ 'credits': 5,
168
+ 'hours': 90,
169
+ 'type': 'required',
170
+ 'short_desc': 'Продвинутые алгоритмы ML, ансамбли, оптимизация',
171
+ 'tags': ['ml', 'advanced', 'algorithms'],
172
+ 'source_url': 'https://abit.itmo.ru/program/master/ai'
173
+ },
174
+ {
175
+ 'id': 'ai_4_1',
176
+ 'program_id': 'ai',
177
+ 'semester': 4,
178
+ 'name': 'Магистерская диссертация',
179
+ 'credits': 12,
180
+ 'hours': 216,
181
+ 'type': 'required',
182
+ 'short_desc': 'Научно-исследовательская работа, защита диссертации',
183
+ 'tags': ['research', 'thesis', 'project'],
184
+ 'source_url': 'https://abit.itmo.ru/program/master/ai'
185
+ },
186
+
187
+ # Программа AI Product
188
+ {
189
+ 'id': 'ai_product_1_1',
190
+ 'program_id': 'ai_product',
191
+ 'semester': 1,
192
+ 'name': 'Продуктовая аналитика',
193
+ 'credits': 6,
194
+ 'hours': 108,
195
+ 'type': 'required',
196
+ 'short_desc': 'Анализ продуктовых метрик, A/B тестирование',
197
+ 'tags': ['product', 'business', 'data', 'analytics'],
198
+ 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
199
+ },
200
+ {
201
+ 'id': 'ai_product_1_2',
202
+ 'program_id': 'ai_product',
203
+ 'semester': 1,
204
+ 'name': 'Управление проектами',
205
+ 'credits': 4,
206
+ 'hours': 72,
207
+ 'type': 'required',
208
+ 'short_desc': 'Методологии управления проектами, Agile, Scrum',
209
+ 'tags': ['pm', 'business', 'management', 'agile'],
210
+ 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
211
+ },
212
+ {
213
+ 'id': 'ai_product_2_1',
214
+ 'program_id': 'ai_product',
215
+ 'semester': 2,
216
+ 'name': 'UX/UI для ИИ продуктов',
217
+ 'credits': 4,
218
+ 'hours': 72,
219
+ 'type': 'required',
220
+ 'short_desc': 'Дизайн интерфейсов для ИИ, UX исследования',
221
+ 'tags': ['ux', 'ui', 'design', 'ai'],
222
+ 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
223
+ },
224
+ {
225
+ 'id': 'ai_product_2_2',
226
+ 'program_id': 'ai_product',
227
+ 'semester': 2,
228
+ 'name': 'Этика ИИ',
229
+ 'credits': 3,
230
+ 'hours': 54,
231
+ 'type': 'required',
232
+ 'short_desc': 'Этические принципы ИИ, справедливость, прозрачность',
233
+ 'tags': ['ethics', 'ai', 'responsible', 'fairness'],
234
+ 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
235
+ },
236
+ {
237
+ 'id': 'ai_product_3_1',
238
+ 'program_id': 'ai_product',
239
+ 'semester': 3,
240
+ 'name': 'Управление ИИ продуктами',
241
+ 'credits': 6,
242
+ 'hours': 108,
243
+ 'type': 'required',
244
+ 'short_desc': 'Стратегическое управление ИИ продуктами, команды',
245
+ 'tags': ['product', 'management', 'ai', 'leadership'],
246
+ 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
247
+ },
248
+ {
249
+ 'id': 'ai_product_4_1',
250
+ 'program_id': 'ai_product',
251
+ 'semester': 4,
252
+ 'name': 'Дипломный проект',
253
+ 'credits': 12,
254
+ 'hours': 216,
255
+ 'type': 'required',
256
+ 'short_desc': 'Разработка ИИ продукта, защита проекта',
257
+ 'tags': ['project', 'thesis', 'product'],
258
+ 'source_url': 'https://abit.itmo.ru/program/master/ai_product'
259
+ }
260
+ ]
261
+
262
+ def parse_all():
263
+ """Основная функция парсинга всех данных"""
264
+ try:
265
+ logger.info('Начинаем парсинг всех данных')
266
+
267
+ # Создаем директории если нет
268
+ os.makedirs('data/processed', exist_ok=True)
269
+
270
+ # Парсим страницы программ
271
+ programs = {
272
+ 'ai': 'https://abit.itmo.ru/program/master/ai',
273
+ 'ai_product': 'https://abit.itmo.ru/program/master/ai_product'
274
+ }
275
+
276
+ all_courses = []
277
+
278
+ for program_id, url in programs.items():
279
+ # Парсим страницу программы
280
+ program_info = parse_program_page(url, program_id)
281
+
282
+ # Пытаемся парсить PDF файлы
283
+ for pdf_url in program_info['pdf_links']:
284
+ pdf_courses = parse_pdf(pdf_url, program_id)
285
+ for course in pdf_courses:
286
+ normalized_course = normalize_course(course, program_id)
287
+ all_courses.append(normalized_course)
288
+
289
+ # Если парсинг не дал результатов, используем fallback
290
+ if not all_courses:
291
+ logger.warning('Парсинг не дал результатов, используем fallback курсы')
292
+ all_courses = get_fallback_courses()
293
+
294
+ # Сохраняем в JSON
295
+ courses_file = 'data/processed/courses.json'
296
+ with open(courses_file, 'w', encoding='utf-8') as f:
297
+ json.dump(all_courses, f, ensure_ascii=False, indent=2)
298
+
299
+ logger.info(f'Сохранено {len(all_courses)} курсов в {courses_file}')
300
+ return True
301
+
302
+ except Exception as e:
303
+ logger.error(f'Ошибка парсинга: {e}')
304
+ # Сохраняем fallback курсы
305
+ try:
306
+ os.makedirs('data/processed', exist_ok=True)
307
+ with open('data/processed/courses.json', 'w', encoding='utf-8') as f:
308
+ json.dump(get_fallback_courses(), f, ensure_ascii=False, indent=2)
309
+ logger.info('Сохранены fallback курсы')
310
+ return True
311
+ except Exception as e2:
312
+ logger.error(f'Ошибка сохранения fallback курсов: {e2}')
313
+ return False