Spaces:
Runtime error
Runtime error
| """ | |
| Модуль содержит классы для представления таблиц в документе. | |
| """ | |
| import warnings | |
| from dataclasses import asdict, dataclass, field | |
| from typing import Any, Callable, Optional | |
| from .parsed_structure import DocumentElement | |
| from .parsed_text_block import TextStyle | |
| class TableTag: | |
| """ | |
| Тег для классификации таблицы. | |
| """ | |
| name: str = "" | |
| value: str = "" | |
| def to_dict(self) -> dict[str, Any]: | |
| """ | |
| Преобразует тег таблицы в словарь. | |
| Returns: | |
| dict[str, Any]: Словарное представление тега таблицы. | |
| """ | |
| return asdict(self) | |
| class ParsedRow(DocumentElement): | |
| """ | |
| Строка таблицы. | |
| """ | |
| index: int | str | None = None | |
| cells: list[str] = field(default_factory=list) | |
| style: TextStyle = field(default_factory=TextStyle) | |
| anchors: list[str] = field(default_factory=list) | |
| links: list[str] = field(default_factory=list) | |
| is_header: bool = False | |
| def to_string( | |
| self, | |
| header: Optional['ParsedRow'] = None, | |
| note: Optional[str] = None, | |
| ) -> str: | |
| """ | |
| Преобразует строку таблицы в строковое представление в виде маркированного списка. | |
| Args: | |
| header (Optional[ParsedRow]): Заголовок столбцов для форматирования. | |
| note (Optional[str]): Примечание к строке. Не будет использовано, если строка не содержит *, | |
| а в примечании есть * | |
| Returns: | |
| str: Строковое представление строки таблицы. | |
| """ | |
| if not self.cells: | |
| return "" | |
| # Если у нас есть хедер, то форматируем как "ключ: значение" с маркерами | |
| if header: | |
| if len(header.cells) != len(self.cells): | |
| raise ValueError("Количество ячеек в строке и хедере не совпадает") | |
| result = '\n'.join( | |
| f"- {header.cells[i]}: {self.cells[i]}" for i in range(len(self.cells)) | |
| ) | |
| # Если у нас есть только две колонки, форматируем как "ключ: значение" с маркером | |
| elif len(self.cells) == 2: | |
| result = f"- {self.cells[0].strip()}: {self.cells[1].strip()}" | |
| # Иначе просто форматируем все ячейки через разделитель | |
| else: | |
| result = '\n'.join(f"- {cell.strip()}" for cell in self.cells) | |
| if note: | |
| if ('*' in result) != ('*' in note): | |
| return result | |
| else: | |
| return f"{result}\nПримечание: {note}" | |
| return result | |
| def apply(self, func: Callable[[str], str]) -> None: | |
| """ | |
| Применяет функцию ко всем ячейкам строки. | |
| Args: | |
| func (Callable[[str], str]): Функция для применения к текстовым элементам. | |
| """ | |
| self.cells = [func(cell) for cell in self.cells] | |
| self.anchors = [func(anchor) for anchor in self.anchors] | |
| self.links = [func(link) for link in self.links] | |
| def to_dict(self) -> dict[str, Any]: | |
| """ | |
| Преобразует строку таблицы в словарь. | |
| Returns: | |
| dict[str, Any]: Словарное представление строки таблицы. | |
| """ | |
| return { | |
| 'index': self.index, | |
| 'cells': self.cells, | |
| 'style': self.style.to_dict(), | |
| 'anchors': self.anchors, | |
| 'links': self.links, | |
| 'is_header': self.is_header, | |
| 'page_number': self.page_number, | |
| 'index_in_document': self.index_in_document, | |
| } | |
| class ParsedSubtable(DocumentElement): | |
| """ | |
| Подтаблица внутри основной таблицы. | |
| """ | |
| title: str | None = None | |
| header: ParsedRow | None = None | |
| rows: list[ParsedRow] = field(default_factory=list) | |
| def to_string( | |
| self, | |
| header: Optional['ParsedRow'] = None, | |
| note: Optional[str] = None, | |
| ) -> str: | |
| """ | |
| Преобразует подтаблицу в строковое представление. | |
| Returns: | |
| str: Строковое представление подтаблицы. | |
| """ | |
| if self.header: | |
| header = self.header | |
| result = [] | |
| if self.title: | |
| result.append(f"## {self.title}") | |
| if len(self.rows) == 0: | |
| if header: | |
| result.append(header.to_string(note=note)) | |
| if note: | |
| result.append(f"Примечание: {note}") | |
| # Обрабатываем каждую строку таблицы | |
| for i, row in enumerate(self.rows, start=1): | |
| # Добавляем номер строки (начиная с 1) | |
| result.append(f"### Строка {i}") | |
| result.append(row.to_string(header=header, note=note)) | |
| return "\n".join(result) | |
| def apply(self, func: Callable[[str], str]) -> None: | |
| """ | |
| Применяет функцию ко всем элементам подтаблицы. | |
| Args: | |
| func (Callable[[str], str]): Функция для применения к текстовым элементам. | |
| """ | |
| if self.title: | |
| self.title = func(self.title) | |
| if self.header: | |
| self.header.apply(func) | |
| for row in self.rows: | |
| row.apply(func) | |
| def to_dict(self) -> dict[str, Any]: | |
| """ | |
| Преобразует подтаблицу в словарь. | |
| Returns: | |
| dict[str, Any]: Словарное представление подтаблицы. | |
| """ | |
| result = {'title': self.title, 'rows': [row.to_dict() for row in self.rows]} | |
| if self.header: | |
| result['header'] = self.header.to_dict() | |
| # Добавляем поля из DocumentElement | |
| result['page_number'] = self.page_number | |
| result['index_in_document'] = self.index_in_document | |
| return result | |
| def has_merged_cells(self) -> bool: | |
| """ | |
| Проверяет наличие объединенных ячеек в подтаблице. | |
| Returns: | |
| bool: True, если в подтаблице есть строки с разным количеством ячеек. | |
| """ | |
| if not self.rows: | |
| return False | |
| # Получаем количество ячеек в строках | |
| cell_counts = [len(row.cells) for row in self.rows] | |
| if len(set(cell_counts)) > 1: | |
| return True | |
| return False | |
| class ParsedTable(DocumentElement): | |
| """ | |
| Таблица из документа. | |
| """ | |
| title: str | None = None | |
| note: str | None = None | |
| classified_tags: list[TableTag] = field(default_factory=list) | |
| index: list[str] = field(default_factory=list) | |
| headers: list[ParsedRow] = field(default_factory=list) | |
| subtables: list[ParsedSubtable] = field(default_factory=list) | |
| table_style: dict[str, Any] = field(default_factory=dict) | |
| title_index_in_paragraphs: int | None = None | |
| def to_string(self) -> str: | |
| """ | |
| Преобразует таблицу в строковое представление. | |
| Returns: | |
| str: Строковое представление таблицы. | |
| """ | |
| # Формируем заголовок таблицы | |
| table_header = "" | |
| if self.title: | |
| table_header = f"# {self.title}" | |
| final_result = [] | |
| common_header = None | |
| if self.headers: | |
| common_header = ParsedRow( | |
| cells=[ | |
| '/'.join(header.cells[i] for header in self.headers) | |
| for i in range(len(self.headers[0].cells)) | |
| ] | |
| ) | |
| if len(self.subtables) == 0: | |
| if common_header: | |
| final_result.append(common_header.to_string(note=self.note)) | |
| else: | |
| final_result.append(self.note) | |
| # Обрабатываем каждую подтаблицу | |
| for subtable in self.subtables: | |
| # Получаем строковое представление подтаблицы | |
| subtable_lines = subtable.to_string(common_header, self.note).split('\n') | |
| # Для каждой линии в подтаблице | |
| current_block = [] | |
| for line in subtable_lines: | |
| # Если это начало новой строки (заголовок строки) | |
| if line.startswith('### Строка'): | |
| # Если у нас уже есть блок данных, добавляем его с дополнительным переносом | |
| if current_block: | |
| final_result.append('\n'.join(current_block)) | |
| final_result.append("") # Дополнительный перенос между строками | |
| # Начинаем новый блок с заголовка таблицы | |
| current_block = [] | |
| if table_header: | |
| current_block.append(table_header) | |
| # Если у подтаблицы есть заголовок, добавляем его | |
| if subtable.title: | |
| current_block.append(f"## {subtable.title}") | |
| # Добавляем заголовок строки | |
| current_block.append(line) | |
| else: | |
| # Добавляем данные строки | |
| current_block.append(line) | |
| # Добавляем последний блок, если он есть | |
| if current_block: | |
| final_result.append('\n'.join(current_block)) | |
| final_result.append("") # Дополнительный перенос между блоками | |
| return '\n'.join(final_result) | |
| def apply(self, func: Callable[[str], str]) -> None: | |
| """ | |
| Применяет функцию ко всем элементам таблицы. | |
| Args: | |
| func (Callable[[str], str]): Функция для применения к текстовым элементам. | |
| """ | |
| if self.title: | |
| self.title = func(self.title) | |
| self.note = func(self.note) | |
| self.index = [func(idx) for idx in self.index] | |
| for tag in self.classified_tags: | |
| tag.name = func(tag.name) | |
| tag.value = func(tag.value) | |
| for header in self.headers: | |
| header.apply(func) | |
| for subtable in self.subtables: | |
| subtable.apply(func) | |
| def to_dict(self) -> dict[str, Any]: | |
| """ | |
| Преобразует таблицу в словарь. | |
| Returns: | |
| dict[str, Any]: Словарное представление таблицы. | |
| """ | |
| result = { | |
| 'title': self.title, | |
| 'note': self.note, | |
| 'classified_tags': [tag.to_dict() for tag in self.classified_tags], | |
| 'index': self.index, | |
| 'headers': [header.to_dict() for header in self.headers], | |
| 'subtables': [subtable.to_dict() for subtable in self.subtables], | |
| 'table_style': self.table_style, | |
| 'page_number': self.page_number, | |
| 'index_in_document': self.index_in_document, | |
| 'title_index_in_paragraphs': self.title_index_in_paragraphs, | |
| } | |
| return result | |
| def has_merged_cells(self) -> bool: | |
| """ | |
| Проверяет наличие объединенных ячеек в таблице. | |
| Returns: | |
| bool: True, если в таблице есть строки с разным количеством ячеек. | |
| """ | |
| # Проверяем заголовки | |
| if self.headers: | |
| header_cell_counts = [len(header.cells) for header in self.headers] | |
| if len(set(header_cell_counts)) > 1: | |
| return True | |
| expected_cell_count = header_cell_counts[0] if header_cell_counts else 0 | |
| else: | |
| expected_cell_count = 0 | |
| # Проверяем подтаблицы | |
| for subtable in self.subtables: | |
| if subtable.has_merged_cells(): | |
| return True | |
| # Проверяем соответствие количества ячеек заголовку | |
| if subtable.rows and expected_cell_count > 0: | |
| for row in subtable.rows: | |
| if len(row.cells) != expected_cell_count: | |
| return True | |
| return False | |
| def to_pandas(self, merged_ok: bool = False) -> Optional['pandas.DataFrame']: # type: ignore | |
| """ | |
| Преобразует таблицу в pandas DataFrame. | |
| Args: | |
| merged_ok (bool): Флаг, указывающий, допустимы ли объединенные ячейки. | |
| Если False и обнаружены объединенные ячейки, будет выдано предупреждение. | |
| Returns: | |
| pandas.DataFrame: DataFrame, представляющий таблицу. | |
| Примечание: | |
| Этот метод требует установленного пакета pandas. | |
| """ | |
| try: | |
| import pandas as pd | |
| except ImportError: | |
| raise ImportError( | |
| "Для использования to_pandas требуется установить pandas." | |
| ) | |
| # Проверка объединенных ячеек | |
| if not merged_ok and self.has_merged_cells(): | |
| warnings.warn( | |
| "Таблица содержит объединенные ячейки, что может привести к некорректному " | |
| "отображению в DataFrame. Установите параметр merged_ok=True, чтобы скрыть это предупреждение." | |
| ) | |
| # Собираем данные для DataFrame | |
| data = [] | |
| # Заголовки столбцов | |
| columns = [] | |
| if self.headers: | |
| # Объединяем многострочные заголовки, используя разделитель '->' | |
| if len(self.headers) > 1: | |
| # Собираем все строки заголовков | |
| header_cells = [] | |
| for i in range(len(self.headers[0].cells)): | |
| header_values = [ | |
| header.cells[i] if i < len(header.cells) else "" | |
| for header in self.headers | |
| ] | |
| header_cells.append(" -> ".join(filter(None, header_values))) | |
| columns = header_cells | |
| else: | |
| columns = self.headers[0].cells | |
| # Собираем данные из подтаблиц | |
| for subtable in self.subtables: | |
| # Если есть заголовок подтаблицы, добавляем его как строку с пустыми значениями | |
| if subtable.title: | |
| row_data = ( | |
| [subtable.title] + [""] * (len(columns) - 1) | |
| if columns | |
| else [subtable.title] | |
| ) | |
| data.append(row_data) | |
| # Добавляем данные из строк подтаблицы | |
| for row in subtable.rows: | |
| row_data = row.cells | |
| # Если количество ячеек не совпадает с количеством столбцов, заполняем пустыми | |
| if columns and len(row_data) < len(columns): | |
| row_data.extend([""] * (len(columns) - len(row_data))) | |
| data.append(row_data) | |
| # Создаем DataFrame | |
| if not columns: | |
| # Если нет заголовков, определяем максимальное количество столбцов | |
| max_cols = max([len(row) for row in data]) if data else 0 | |
| df = pd.DataFrame(data) | |
| else: | |
| df = pd.DataFrame(data, columns=columns) | |
| # Добавляем название таблицы как атрибут | |
| if self.title: | |
| df.attrs['title'] = self.title | |
| # Добавляем примечание как атрибут | |
| if self.note: | |
| df.attrs['note'] = self.note | |
| return df | |
| def to_markdown(self, merged_ok: bool = False) -> str: | |
| """ | |
| Преобразует таблицу в формат Markdown. | |
| Args: | |
| merged_ok (bool): Флаг, указывающий, допустимы ли объединенные ячейки. | |
| Если False и обнаружены объединенные ячейки, будет выдано предупреждение. | |
| Returns: | |
| str: Markdown представление таблицы. | |
| """ | |
| # Проверка объединенных ячеек | |
| if not merged_ok and self.has_merged_cells(): | |
| warnings.warn( | |
| "Таблица содержит объединенные ячейки, что может привести к некорректному " | |
| "отображению в Markdown. Установите параметр merged_ok=True, чтобы скрыть это предупреждение." | |
| ) | |
| lines = [] | |
| # Добавляем заголовок таблицы, если он есть | |
| if self.title: | |
| lines.append(f"**{self.title}**\n") | |
| # Если есть заголовок таблицы, используем его | |
| if self.headers: | |
| # Берем первую строку заголовка | |
| header_cells = self.headers[0].cells | |
| # Формируем строку заголовка | |
| header_line = "| " + " | ".join(header_cells) + " |" | |
| lines.append(header_line) | |
| # Формируем разделительную строку | |
| separator_line = "| " + " | ".join(["---"] * len(header_cells)) + " |" | |
| lines.append(separator_line) | |
| # Если есть дополнительные строки заголовка, добавляем их | |
| for i in range(1, len(self.headers)): | |
| subheader_cells = self.headers[i].cells | |
| if len(subheader_cells) < len(header_cells): | |
| subheader_cells.extend( | |
| [""] * (len(header_cells) - len(subheader_cells)) | |
| ) | |
| subheader_line = ( | |
| "| " + " | ".join(subheader_cells[: len(header_cells)]) + " |" | |
| ) | |
| lines.append(subheader_line) | |
| # Обходим подтаблицы | |
| for subtable in self.subtables: | |
| # Если есть заголовок подтаблицы, добавляем его как строку | |
| if subtable.title: | |
| lines.append( | |
| f"| **{subtable.title}** | " | |
| + " | ".join([""] * (len(header_cells) - 1)) | |
| + " |" | |
| ) | |
| # Добавляем строки подтаблицы | |
| for row in subtable.rows: | |
| row_cells = row.cells | |
| # Если количество ячеек не совпадает с количеством заголовков, добавляем пустые | |
| if len(row_cells) < len(header_cells): | |
| row_cells.extend([""] * (len(header_cells) - len(row_cells))) | |
| row_line = "| " + " | ".join(row_cells[: len(header_cells)]) + " |" | |
| lines.append(row_line) | |
| else: | |
| # Если заголовка нет, просто выводим строки как текст | |
| for subtable in self.subtables: | |
| if subtable.title: | |
| lines.append(f"**{subtable.title}**") | |
| for row in subtable.rows: | |
| lines.append(row.to_string()) | |
| # Добавляем примечание, если оно есть | |
| if self.note: | |
| lines.append(f"\n*Примечание: {self.note}*") | |
| return "\n".join(lines) | |