Spaces:
Runtime error
Runtime error
| """ | |
| Модуль с парсером для PDF документов. | |
| """ | |
| import io | |
| import logging | |
| import os | |
| from typing import BinaryIO | |
| import fitz # PyMuPDF | |
| from ...data_classes import ParsedDocument, ParsedMeta | |
| from ..abstract_parser import AbstractParser | |
| from ..file_types import FileType | |
| from .pdf.formula_parser import PDFFormulaParser | |
| from .pdf.image_parser import PDFImageParser | |
| from .pdf.meta_parser import PDFMetaParser | |
| from .pdf.paragraph_parser import PDFParagraphParser | |
| from .pdf.table_parser import PDFTableParser | |
| logger = logging.getLogger(__name__) | |
| class PDFParser(AbstractParser): | |
| """ | |
| Парсер для PDF документов. | |
| Использует PyMuPDF (fitz) для извлечения текста, изображений, таблиц, | |
| формул и метаданных из документа. | |
| """ | |
| def __init__(self): | |
| """ | |
| Инициализирует PDF парсер и его компоненты. | |
| """ | |
| super().__init__(FileType.PDF) | |
| self.meta_parser = PDFMetaParser() | |
| self.paragraph_parser = PDFParagraphParser() | |
| self.table_parser = PDFTableParser() | |
| self.image_parser = PDFImageParser() | |
| self.formula_parser = PDFFormulaParser() | |
| def parse_by_path(self, file_path: str) -> ParsedDocument: | |
| """ | |
| Парсит PDF документ по пути к файлу и возвращает его структурное представление. | |
| Args: | |
| file_path (str): Путь к PDF файлу для парсинга. | |
| Returns: | |
| ParsedDocument: Структурное представление документа. | |
| Raises: | |
| ValueError: Если файл не существует или не может быть прочитан. | |
| """ | |
| logger.debug(f"Parsing PDF file: {file_path}") | |
| if not os.path.exists(file_path): | |
| raise ValueError(f"File not found: {file_path}") | |
| try: | |
| # Открываем PDF с помощью PyMuPDF | |
| pdf_doc = fitz.open(file_path) | |
| filename = os.path.basename(file_path) | |
| return self._parse_document(pdf_doc, filename, file_path) | |
| except Exception as e: | |
| logger.error(f"Failed to open PDF file: {e}") | |
| raise ValueError(f"Cannot open PDF file: {str(e)}") | |
| def parse(self, file: BinaryIO, file_type: FileType | str | None = None) -> ParsedDocument: | |
| """ | |
| Парсит PDF документ из объекта файла и возвращает его структурное представление. | |
| Args: | |
| file (BinaryIO): Объект файла для парсинга. | |
| file_type: Тип файла, если известен. | |
| Может быть объектом FileType или строкой с расширением (".pdf"). | |
| Returns: | |
| ParsedDocument: Структурное представление документа. | |
| Raises: | |
| ValueError: Если файл не может быть прочитан или распарсен. | |
| """ | |
| logger.debug("Parsing PDF from file object") | |
| # Проверяем соответствие типа файла | |
| if file_type and isinstance(file_type, FileType) and file_type != FileType.PDF: | |
| logger.warning( | |
| f"Provided file_type {file_type} doesn't match parser type {FileType.PDF}" | |
| ) | |
| try: | |
| # Читаем содержимое файла в память | |
| content = file.read() | |
| # Открываем PDF из потока с помощью PyMuPDF | |
| pdf_stream = io.BytesIO(content) | |
| pdf_doc = fitz.open(stream=pdf_stream, filetype="pdf") | |
| return self._parse_document(pdf_doc, "unknown.pdf", None) | |
| except Exception as e: | |
| logger.error(f"Failed to parse PDF from stream: {e}") | |
| raise ValueError(f"Cannot parse PDF content: {str(e)}") | |
| def _parse_document( | |
| self, | |
| pdf_doc: fitz.Document, | |
| filename: str, | |
| filepath: str | None, | |
| ) -> ParsedDocument: | |
| """ | |
| Внутренний метод для парсинга открытого PDF документа. | |
| Args: | |
| pdf_doc (fitz.Document): Открытый PDF документ. | |
| filename (str): Имя файла для документа. | |
| filepath (str | None): Путь к файлу (или None, если из объекта). | |
| Returns: | |
| ParsedDocument: Структурное представление документа. | |
| Raises: | |
| ValueError: Если содержимое не может быть распарсено. | |
| """ | |
| # Создание базового документа | |
| doc = ParsedDocument(name=filename, type=FileType.PDF) | |
| try: | |
| # Извлечение метаданных | |
| meta_dict = self.meta_parser.parse(pdf_doc, filepath) | |
| # Преобразуем словарь метаданных в объект ParsedMeta | |
| meta = ParsedMeta() | |
| if 'author' in meta_dict: | |
| meta.owner = meta_dict['author'] | |
| if 'creation_date' in meta_dict: | |
| meta.date = meta_dict['creation_date'] | |
| if filepath: | |
| meta.source = filepath | |
| # Сохраняем остальные метаданные в поле note | |
| meta.note = meta_dict | |
| doc.meta = meta | |
| logger.debug("Parsed metadata") | |
| # Последовательный вызов парсеров | |
| try: | |
| # Парсим таблицы | |
| doc.tables.extend(self.table_parser.parse(pdf_doc)) | |
| logger.debug(f"Parsed {len(doc.tables)} tables") | |
| # Парсим изображения | |
| doc.images.extend(self.image_parser.parse(pdf_doc)) | |
| logger.debug(f"Parsed {len(doc.images)} images") | |
| # Парсим формулы | |
| doc.formulas.extend(self.formula_parser.parse(pdf_doc)) | |
| logger.debug(f"Parsed {len(doc.formulas)} formulas") | |
| # Парсим текст | |
| doc.paragraphs.extend(self.paragraph_parser.parse(pdf_doc)) | |
| logger.debug(f"Parsed {len(doc.paragraphs)} paragraphs") | |
| # Связываем элементы с их заголовками | |
| self._link_elements_with_captions(doc) | |
| logger.debug("Linked elements with captions") | |
| except Exception as e: | |
| logger.error(f"Error during parsing components: {e}") | |
| logger.exception(e) | |
| raise ValueError(f"Error parsing document components: {str(e)}") | |
| return doc | |
| finally: | |
| # Закрываем документ после использования | |
| pdf_doc.close() | |
| def _link_elements_with_captions(self, doc: ParsedDocument) -> None: | |
| """ | |
| Связывает таблицы, изображения и формулы с их заголовками на основе анализа текста. | |
| Args: | |
| doc (ParsedDocument): Документ для обработки. | |
| """ | |
| # Находим параграфы, которые могут быть заголовками | |
| caption_paragraphs = {} | |
| for i, para in enumerate(doc.paragraphs): | |
| text = para.text.lower() | |
| if any(keyword in text for keyword in ["таблица", "рисунок", "формула", "рис.", "табл."]): | |
| caption_paragraphs[i] = { | |
| "text": text, | |
| "page": para.page_number | |
| } | |
| # Для таблиц ищем соответствующие заголовки | |
| for table in doc.tables: | |
| table_page = table.page_number | |
| # Ищем заголовки на той же странице или на предыдущей | |
| for para_idx, caption_info in caption_paragraphs.items(): | |
| if ("таблица" in caption_info["text"] or "табл." in caption_info["text"]) and \ | |
| (caption_info["page"] == table_page or caption_info["page"] == table_page - 1): | |
| table.title_index_in_paragraphs = para_idx | |
| break | |
| # Для изображений ищем соответствующие заголовки | |
| for image in doc.images: | |
| image_page = image.page_number | |
| # Ищем заголовки на той же странице | |
| for para_idx, caption_info in caption_paragraphs.items(): | |
| if ("рисунок" in caption_info["text"] or "рис." in caption_info["text"]) and \ | |
| caption_info["page"] == image_page: | |
| image.referenced_element_index = para_idx | |
| break | |
| # Для формул ищем соответствующие заголовки | |
| for formula in doc.formulas: | |
| formula_page = formula.page_number | |
| # Ищем заголовки на той же странице | |
| for para_idx, caption_info in caption_paragraphs.items(): | |
| if "формула" in caption_info["text"] and caption_info["page"] == formula_page: | |
| formula.referenced_element_index = para_idx | |
| break |