File size: 12,465 Bytes
172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 53fe915 172ee17 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
import requests
import pdfplumber
import os
import re
from typing import List, Dict
import tempfile
from urllib.parse import urlparse
class PDFParser:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
def download_pdf(self, url: str, filename: str) -> str:
"""Скачивает PDF файл и сохраняет локально"""
try:
print(f'Скачивание PDF: {filename}')
response = self.session.get(url, stream=True, timeout=60)
response.raise_for_status()
# Создаем директорию если не существует
os.makedirs('data/raw', exist_ok=True)
# Сохраняем файл
filepath = os.path.join('data/raw', filename)
with open(filepath, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f'PDF сохранен: {filepath}')
return filepath
except Exception as e:
print(f'Ошибка скачивания PDF {url}: {e}')
return None
def parse_pdf(self, filepath: str, program_id: str) -> List[Dict]:
"""Парсит PDF и извлекает информацию о курсах"""
courses = []
try:
print(f'Парсинг PDF: {filepath}')
with pdfplumber.open(filepath) as pdf:
# Пробуем извлечь таблицы
table_courses = self._extract_from_tables(pdf, program_id)
if table_courses:
courses.extend(table_courses)
print(f'Извлечено из таблиц: {len(table_courses)} курсов')
# Если таблиц нет или мало курсов, пробуем текстовый парсинг
if len(courses) < 5:
text_courses = self._extract_from_text(pdf, program_id)
courses.extend(text_courses)
print(f'Извлечено из текста: {len(text_courses)} курсов')
# Дедупликация курсов
courses = self._deduplicate_courses(courses)
print(f'Всего извлечено курсов: {len(courses)}')
return courses
except Exception as e:
print(f'Ошибка парсинга PDF {filepath}: {e}')
return []
def _extract_from_tables(self, pdf, program_id: str) -> List[Dict]:
"""Извлекает курсы из таблиц PDF"""
courses = []
current_semester = 1
for page_num, page in enumerate(pdf.pages):
try:
# Извлекаем таблицы
tables = page.extract_tables()
for table in tables:
if not table or len(table) < 2:
continue
# Определяем семестр по заголовкам
semester = self._detect_semester_from_table(table, current_semester)
if semester:
current_semester = semester
# Парсим строки таблицы
for row in table[1:]: # Пропускаем заголовок
if not row or len(row) < 2:
continue
course = self._parse_table_row(row, program_id, current_semester, page_num + 1)
if course:
courses.append(course)
except Exception as e:
print(f'Ошибка обработки страницы {page_num + 1}: {e}')
continue
return courses
def _extract_from_text(self, pdf, program_id: str) -> List[Dict]:
"""Извлекает курсы из текста PDF"""
courses = []
current_semester = 1
for page_num, page in enumerate(pdf.pages):
try:
text = page.extract_text()
if not text:
continue
# Определяем семестр по тексту
semester = self._detect_semester_from_text(text, current_semester)
if semester:
current_semester = semester
# Ищем курсы в тексте
page_courses = self._parse_text_for_courses(text, program_id, current_semester, page_num + 1)
courses.extend(page_courses)
except Exception as e:
print(f'Ошибка обработки текста страницы {page_num + 1}: {e}')
continue
return courses
def _detect_semester_from_table(self, table: List[List], current_semester: int) -> int:
"""Определяет семестр по заголовкам таблицы"""
if not table or not table[0]:
return current_semester
header_text = ' '.join([str(cell) for cell in table[0] if cell]).lower()
# Поиск упоминаний семестров
for i in range(1, 5):
if f'{i} семестр' in header_text or f'{i} семестре' in header_text:
return i
return current_semester
def _detect_semester_from_text(self, text: str, current_semester: int) -> int:
"""Определяет семестр по тексту"""
text_lower = text.lower()
# Поиск упоминаний семестров
for i in range(1, 5):
if f'{i} семестр' in text_lower or f'{i} семестре' in text_lower:
return i
return current_semester
def _parse_table_row(self, row: List, program_id: str, semester: int, page: int) -> Dict:
"""Парсит строку таблицы и извлекает информацию о курсе"""
if not row or len(row) < 2:
return None
# Очищаем ячейки от лишних символов
clean_row = [str(cell).strip() if cell else '' for cell in row]
# Ищем название курса (обычно в первой или второй колонке)
course_name = ''
credits = 0
hours = 0
course_type = 'required'
for i, cell in enumerate(clean_row):
if not cell or cell.lower() in ['название', 'дисциплина', 'курс', 'предмет']:
continue
# Если это похоже на название курса
if len(cell) > 10 and not cell.isdigit():
course_name = cell
break
# Ищем кредиты и часы
for cell in clean_row:
if cell.isdigit():
num = int(cell)
if 1 <= num <= 12: # Кредиты обычно 1-12
credits = num
elif 18 <= num <= 216: # Часы обычно 18-216
hours = num
# Определяем тип курса
row_text = ' '.join(clean_row).lower()
if any(word in row_text for word in ['по выбору', 'электив', 'факультатив']):
course_type = 'elective'
if not course_name or len(course_name) < 5:
return None
return {
'id': f'{program_id}_{semester}_{len(course_name)}',
'program_id': program_id,
'semester': semester,
'name': course_name,
'credits': credits,
'hours': hours,
'type': course_type,
'source_pdf': os.path.basename(filepath) if 'filepath' in locals() else '',
'source_page': page
}
def _parse_text_for_courses(self, text: str, program_id: str, semester: int, page: int) -> List[Dict]:
"""Парсит текст и ищет курсы"""
courses = []
# Разбиваем текст на строки
lines = text.split('\n')
for line in lines:
line = line.strip()
if not line or len(line) < 10:
continue
# Ищем паттерны курсов
course = self._extract_course_from_line(line, program_id, semester, page)
if course:
courses.append(course)
return courses
def _extract_course_from_line(self, line: str, program_id: str, semester: int, page: int) -> Dict:
"""Извлекает информацию о курсе из строки текста"""
# Паттерны для поиска курсов
patterns = [
r'([А-Я][А-Яа-я\s\-\(\)]+?)\s+(\d+)\s+(\d+)', # Название + кредиты + часы
r'([А-Я][А-Яа-я\s\-\(\)]+?)\s+(\d+)\s*кр', # Название + кредиты
r'([А-Я][А-Яа-я\s\-\(\)]+?)\s+(\d+)\s*ч', # Название + часы
]
for pattern in patterns:
match = re.search(pattern, line)
if match:
course_name = match.group(1).strip()
if len(course_name) < 5:
continue
# Извлекаем числа
numbers = [int(match.group(i)) for i in range(2, len(match.groups()) + 1)]
credits = 0
hours = 0
if len(numbers) >= 2:
credits, hours = numbers[0], numbers[1]
elif len(numbers) == 1:
if numbers[0] <= 12:
credits = numbers[0]
else:
hours = numbers[0]
# Определяем тип курса
course_type = 'required'
if any(word in line.lower() for word in ['по выбору', 'электив', 'факультатив']):
course_type = 'elective'
return {
'id': f'{program_id}_{semester}_{len(course_name)}',
'program_id': program_id,
'semester': semester,
'name': course_name,
'credits': credits,
'hours': hours,
'type': course_type,
'source_page': page
}
return None
def _deduplicate_courses(self, courses: List[Dict]) -> List[Dict]:
"""Удаляет дубликаты курсов"""
seen = set()
unique_courses = []
for course in courses:
# Создаем ключ для дедупликации
key = f"{course['name']}_{course['semester']}_{course['program_id']}"
if key not in seen:
seen.add(key)
unique_courses.append(course)
return unique_courses
def main():
parser = PDFParser()
# Тестовый URL (замените на реальный)
test_url = "https://example.com/test.pdf"
filename = "test_curriculum.pdf"
# Скачивание и парсинг
filepath = parser.download_pdf(test_url, filename)
if filepath:
courses = parser.parse_pdf(filepath, 'test_program')
print(f'Извлечено курсов: {len(courses)}')
for course in courses[:5]:
print(f"- {course['name']} ({course['semester']} семестр, {course['credits']} кредитов)")
if __name__ == '__main__':
main()
|