Spaces:
Sleeping
Sleeping
""" | |
Модуль содержит классы для представления таблиц в документе. | |
""" | |
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) | |