244 lines
9.2 KiB
Python
244 lines
9.2 KiB
Python
|
|
"""
|
|||
|
|
Обработка документов различных форматов
|
|||
|
|
Поддержка PDF, DOCX, текстовых файлов с обработкой больших файлов (>100MB)
|
|||
|
|
"""
|
|||
|
|
import logging
|
|||
|
|
import io
|
|||
|
|
from typing import Optional, Tuple
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DocumentProcessor:
|
|||
|
|
"""Обработчик документов различных форматов"""
|
|||
|
|
|
|||
|
|
def __init__(self, max_file_size: int = 100 * 1024 * 1024):
|
|||
|
|
"""
|
|||
|
|
Инициализация процессора документов
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
max_file_size: Максимальный размер файла для прямой обработки (байты)
|
|||
|
|
"""
|
|||
|
|
self.max_file_size = max_file_size
|
|||
|
|
|
|||
|
|
def process_file(
|
|||
|
|
self,
|
|||
|
|
file_content: bytes,
|
|||
|
|
filename: str,
|
|||
|
|
mime_type: Optional[str] = None
|
|||
|
|
) -> Tuple[str, bool]:
|
|||
|
|
"""
|
|||
|
|
Обработка файла и извлечение текста
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
file_content: Содержимое файла
|
|||
|
|
filename: Имя файла (для определения типа)
|
|||
|
|
mime_type: MIME тип файла (опционально)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Кортеж (текст, is_large_file) где is_large_file указывает,
|
|||
|
|
что файл был обработан потоково из-за большого размера
|
|||
|
|
"""
|
|||
|
|
file_size = len(file_content)
|
|||
|
|
is_large_file = file_size > self.max_file_size
|
|||
|
|
|
|||
|
|
# Определение типа файла
|
|||
|
|
file_ext = Path(filename).suffix.lower()
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
if file_ext == '.pdf' or mime_type == 'application/pdf':
|
|||
|
|
return self._process_pdf(file_content, is_large_file), is_large_file
|
|||
|
|
|
|||
|
|
elif file_ext in ['.docx', '.doc'] or mime_type in ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/msword']:
|
|||
|
|
return self._process_docx(file_content), False
|
|||
|
|
|
|||
|
|
elif file_ext in ['.txt', '.md', '.markdown'] or mime_type in ['text/plain', 'text/markdown']:
|
|||
|
|
return self._process_text(file_content), False
|
|||
|
|
|
|||
|
|
elif file_ext == '.csv' or mime_type == 'text/csv':
|
|||
|
|
return self._process_csv(file_content), False
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
logger.warning(f"Неподдерживаемый формат файла: {filename} (тип: {mime_type})")
|
|||
|
|
# Пытаемся обработать как текст
|
|||
|
|
try:
|
|||
|
|
return self._process_text(file_content), False
|
|||
|
|
except Exception:
|
|||
|
|
raise ValueError(f"Не удалось обработать файл {filename}: неподдерживаемый формат")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Ошибка при обработке файла {filename}: {e}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
def _process_pdf(self, content: bytes, is_large: bool) -> str:
|
|||
|
|
"""
|
|||
|
|
Обработка PDF файла
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
content: Содержимое PDF
|
|||
|
|
is_large: Флаг большого файла (для потоковой обработки)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Извлеченный текст
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
import pypdf
|
|||
|
|
|
|||
|
|
pdf_file = io.BytesIO(content)
|
|||
|
|
pdf_reader = pypdf.PdfReader(pdf_file)
|
|||
|
|
|
|||
|
|
text_parts = []
|
|||
|
|
total_pages = len(pdf_reader.pages)
|
|||
|
|
|
|||
|
|
logger.info(f"Обработка PDF: {total_pages} страниц")
|
|||
|
|
|
|||
|
|
# Для больших файлов обрабатываем страницы порциями
|
|||
|
|
if is_large:
|
|||
|
|
# Ограничиваем количество страниц для очень больших файлов
|
|||
|
|
max_pages = min(total_pages, 1000) # Максимум 1000 страниц
|
|||
|
|
logger.warning(f"Большой PDF файл. Обрабатываются первые {max_pages} из {total_pages} страниц")
|
|||
|
|
else:
|
|||
|
|
max_pages = total_pages
|
|||
|
|
|
|||
|
|
for page_num in range(max_pages):
|
|||
|
|
try:
|
|||
|
|
page = pdf_reader.pages[page_num]
|
|||
|
|
text = page.extract_text()
|
|||
|
|
if text.strip():
|
|||
|
|
text_parts.append(f"--- Страница {page_num + 1} ---\n{text}")
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"Ошибка при обработке страницы {page_num + 1}: {e}")
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
result = "\n\n".join(text_parts)
|
|||
|
|
|
|||
|
|
if not result.strip():
|
|||
|
|
raise ValueError("Не удалось извлечь текст из PDF")
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
except ImportError:
|
|||
|
|
raise ImportError(
|
|||
|
|
"Библиотека pypdf не установлена. Установите: pip install pypdf"
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Ошибка при обработке PDF: {e}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
def _process_docx(self, content: bytes) -> str:
|
|||
|
|
"""
|
|||
|
|
Обработка DOCX файла
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
content: Содержимое DOCX
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Извлеченный текст
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
import docx
|
|||
|
|
|
|||
|
|
doc_file = io.BytesIO(content)
|
|||
|
|
doc = docx.Document(doc_file)
|
|||
|
|
|
|||
|
|
text_parts = []
|
|||
|
|
|
|||
|
|
# Извлечение текста из параграфов
|
|||
|
|
for paragraph in doc.paragraphs:
|
|||
|
|
if paragraph.text.strip():
|
|||
|
|
text_parts.append(paragraph.text)
|
|||
|
|
|
|||
|
|
# Извлечение текста из таблиц
|
|||
|
|
for table in doc.tables:
|
|||
|
|
for row in table.rows:
|
|||
|
|
row_text = " | ".join(cell.text.strip() for cell in row.cells)
|
|||
|
|
if row_text.strip():
|
|||
|
|
text_parts.append(row_text)
|
|||
|
|
|
|||
|
|
result = "\n\n".join(text_parts)
|
|||
|
|
|
|||
|
|
if not result.strip():
|
|||
|
|
raise ValueError("Не удалось извлечь текст из DOCX")
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
except ImportError:
|
|||
|
|
raise ImportError(
|
|||
|
|
"Библиотека python-docx не установлена. Установите: pip install python-docx"
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Ошибка при обработке DOCX: {e}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
def _process_text(self, content: bytes) -> str:
|
|||
|
|
"""
|
|||
|
|
Обработка текстового файла
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
content: Содержимое файла
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Текст с правильной кодировкой
|
|||
|
|
"""
|
|||
|
|
# Попытка различных кодировок
|
|||
|
|
encodings = ['utf-8', 'utf-8-sig', 'cp1251', 'latin-1']
|
|||
|
|
|
|||
|
|
for encoding in encodings:
|
|||
|
|
try:
|
|||
|
|
return content.decode(encoding)
|
|||
|
|
except UnicodeDecodeError:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# Если ничего не помогло, используем errors='replace'
|
|||
|
|
return content.decode('utf-8', errors='replace')
|
|||
|
|
|
|||
|
|
def _process_csv(self, content: bytes) -> str:
|
|||
|
|
"""
|
|||
|
|
Обработка CSV файла (конвертация в читаемый текст)
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
content: Содержимое CSV
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Текстовая версия CSV
|
|||
|
|
"""
|
|||
|
|
import csv
|
|||
|
|
|
|||
|
|
text_content = self._process_text(content)
|
|||
|
|
csv_reader = csv.reader(io.StringIO(text_content))
|
|||
|
|
|
|||
|
|
rows = []
|
|||
|
|
for row_num, row in enumerate(csv_reader, 1):
|
|||
|
|
rows.append(f"Строка {row_num}: {' | '.join(row)}")
|
|||
|
|
|
|||
|
|
return "\n".join(rows)
|
|||
|
|
|
|||
|
|
def is_supported_format(self, filename: str, mime_type: Optional[str] = None) -> bool:
|
|||
|
|
"""
|
|||
|
|
Проверка поддержки формата файла
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
filename: Имя файла
|
|||
|
|
mime_type: MIME тип
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
True если формат поддерживается
|
|||
|
|
"""
|
|||
|
|
file_ext = Path(filename).suffix.lower()
|
|||
|
|
supported_extensions = {'.pdf', '.docx', '.doc', '.txt', '.md', '.markdown', '.csv'}
|
|||
|
|
|
|||
|
|
if file_ext in supported_extensions:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
supported_mimes = {
|
|||
|
|
'application/pdf',
|
|||
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|||
|
|
'application/msword',
|
|||
|
|
'text/plain',
|
|||
|
|
'text/markdown',
|
|||
|
|
'text/csv'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return mime_type in supported_mimes if mime_type else False
|