muryshev's picture
update
86c402d
raw
history blame
21.4 kB
"""
Модуль содержит классы для представления таблиц в документе.
"""
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
@dataclass
class TableTag:
"""
Тег для классификации таблицы.
"""
name: str = ""
value: str = ""
def to_dict(self) -> dict[str, Any]:
"""
Преобразует тег таблицы в словарь.
Returns:
dict[str, Any]: Словарное представление тега таблицы.
"""
return asdict(self)
@dataclass
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,
}
@dataclass
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
@dataclass
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)