Spaces:
Sleeping
Sleeping
import logging | |
from bs4 import BeautifulSoup | |
from components.parser.abbreviations.abbreviation import Abbreviation | |
from components.parser.xml.constants import (ABBREVIATIONS, | |
ABBREVIATIONS_PATTERNS, | |
REGULATIONS, REGULATIONS_PATTERNS) | |
from components.parser.xml.structures import (ParsedRow, ParsedTable, | |
ParsedTables) | |
logger = logging.getLogger(__name__) | |
class XMLTableParser: | |
""" | |
Класс для парсинга таблиц из xml файлов. | |
""" | |
def __init__(self, soup: BeautifulSoup): | |
self.soup = soup | |
self.abbreviations = [] | |
def parse(self) -> ParsedTables: | |
""" | |
Парсинг таблиц из xml файла. | |
Returns: | |
ParsedTables - все таблицы, полученные из xml файла | |
""" | |
tables = self.soup.find_all('w:tbl') | |
logger.info(f"Found {len(tables)} tables in XML") | |
parsed_tables = [] | |
self.abbreviations = [] | |
for table_ind, table in enumerate(tables): | |
table_name = self._extract_table_name(table) | |
type_short = self._classify_special_types(table_name, table) | |
first_row = table.find('w:tr') | |
columns_count = len(first_row.find_all('w:tc')) if first_row else 0 | |
parsed_table = self._parse_table( | |
table=table, | |
table_index=table_ind + 1, | |
type_short=type_short, | |
use_header=columns_count != 2, | |
table_name=table_name, | |
) | |
parsed_tables.append(parsed_table) | |
# Если таблица содержит сокращения, извлекаем их | |
if type_short == ABBREVIATIONS: | |
abbreviations_from_table = self._extract_abbreviations_from_table( | |
parsed_table | |
) | |
if abbreviations_from_table: | |
self.abbreviations.extend(abbreviations_from_table) | |
logger.debug(f"Parsed {len(parsed_tables)} tables") | |
# Создаем и нормализуем таблицы | |
parsed_tables_obj = ParsedTables(tables=parsed_tables) | |
normalized_tables = parsed_tables_obj.normalize() | |
logger.debug(f"Normalized tables: {len(normalized_tables.tables)} main tables") | |
if self.abbreviations: | |
logger.debug( | |
f"Extracted {len(self.abbreviations)} abbreviations from tables" | |
) | |
return normalized_tables | |
def get_abbreviations(self) -> list[Abbreviation]: | |
""" | |
Возвращает список аббревиатур, извлеченных из таблиц. | |
Returns: | |
list[Abbreviation]: Список аббревиатур | |
""" | |
return self.abbreviations | |
def _extract_abbreviations_from_table( | |
self, table: ParsedTable | |
) -> list[Abbreviation]: | |
""" | |
Извлечение аббревиатур из таблицы, помеченной как "сокращения". | |
Args: | |
table: ParsedTable - таблица сокращений | |
Returns: | |
list[Abbreviation]: Список аббревиатур | |
""" | |
abbreviations = [] | |
# Проверяем, что таблица имеет нужный формат (обычно 2 колонки) | |
for row in table.rows: | |
if len(row.cols) >= 2: | |
# Первая колонка обычно содержит сокращение, вторая - расшифровку | |
short_form = row.cols[0].strip() | |
full_form = row.cols[1].strip() | |
# Создаем объект аббревиатуры только если оба поля не пусты | |
if short_form and full_form: | |
abbreviation = Abbreviation( | |
short_form=short_form, | |
full_form=full_form, | |
) | |
# Обрабатываем аббревиатуру для определения типа и очистки | |
abbreviation.process() | |
abbreviations.append(abbreviation) | |
return abbreviations | |
def _parse_table( | |
cls, | |
table: BeautifulSoup, | |
table_index: int, | |
type_short: str | None, | |
use_header: bool = False, | |
table_name: str | None = None, | |
) -> ParsedTable: | |
""" | |
Парсинг таблицы. | |
Args: | |
table: BeautifulSoup - объект таблицы | |
table_index: int - номер таблицы в xml-файле | |
type_short: str | None - например, "сокращения" или "регламентирующие документы" | |
use_header: bool - рассматривать ли первую строку таблицы как шапку таблицы | |
table_name: str | None - название таблицы, если найдено | |
Returns: | |
ParsedTable - таблица, полученная из xml файла | |
""" | |
parsed_rows = [] | |
header = [] if use_header else None | |
rows = table.find_all('w:tr') | |
for row_index, row in enumerate(rows): | |
columns = row.find_all('w:tc') | |
columns = [col.get_text() for col in columns] | |
if (row_index == 0) and use_header: | |
header = columns | |
else: | |
parsed_rows.append(ParsedRow(index=row_index, cols=columns)) | |
# Вычисляем статистические показатели таблицы | |
rows_count = len(parsed_rows) | |
# Определяем модальное количество столбцов | |
if rows_count > 0: | |
col_counts = [len(row.cols) for row in parsed_rows] | |
from collections import Counter | |
modal_cols_count = Counter(col_counts).most_common(1)[0][0] | |
else: | |
modal_cols_count = len(header) if header else 0 | |
# Инициализируем has_merged_cells как False, | |
# actual value will be determined in normalize method | |
has_merged_cells = False | |
return ParsedTable( | |
index=table_index, | |
short_type=type_short, | |
header=header, | |
rows=parsed_rows, | |
name=table_name, | |
rows_count=rows_count, | |
modal_cols_count=modal_cols_count, | |
has_merged_cells=has_merged_cells, | |
) | |
def _extract_columns_from_row( | |
table_row: BeautifulSoup, | |
) -> list[str]: | |
""" | |
Парсинг колонок из строки таблицы. | |
Args: | |
table_row: BeautifulSoup - объект строки таблицы | |
Returns: | |
list[str] - список колонок, полученных из строки таблицы | |
""" | |
parsed_columns = [] | |
for cell in table_row.find_all('w:tc'): | |
cell_text_parts = [] | |
for text_element in cell.find_all('w:t'): | |
text_content = text_element.get_text() | |
# Join all text parts from this cell and add to columns | |
if cell_text_parts: | |
parsed_columns.append(''.join(cell_text_parts)) | |
return parsed_columns | |
def _classify_special_types( | |
table_name: str | None, | |
table: BeautifulSoup, | |
) -> str | None: | |
""" | |
Поиск указаний на то, что таблица является специальной: "сокращения" или "регламентирующие документы". | |
Args: | |
table_name: str - название таблицы | |
table: BeautifulSoup - объект таблицы | |
Returns: | |
str | None - либо "сокращения", либо "регламентирующие документы", либо None, если сокращения и регламенты не найдены | |
""" | |
first_row = table.find('w:tr').text | |
# Проверяем наличие шаблонов в тексте перед таблицей | |
for pattern in ABBREVIATIONS_PATTERNS: | |
if (table_name and pattern.lower() in table_name.lower()) or ( | |
pattern in first_row.lower() | |
): | |
return ABBREVIATIONS | |
for pattern in REGULATIONS_PATTERNS: | |
if (table_name and pattern.lower() in table_name.lower()) or ( | |
pattern in first_row.lower() | |
): | |
return REGULATIONS | |
return None | |
def _extract_table_name( | |
table: BeautifulSoup, | |
) -> str | None: | |
""" | |
Извлечение названия таблицы из текста перед таблицей. | |
Метод ищет строки, содержащие типичные маркеры заголовков таблиц, такие как | |
"Таблица", "Таблица N", "Табл.", и т.д., с учетом различных вариантов написания. | |
Args: | |
before_table_xml: str - блок xml-файла, предшествующий таблице | |
Returns: | |
str | None - название таблицы, если найдено, иначе None | |
""" | |
# Создаем объект BeautifulSoup для парсинга XML фрагмента | |
previous_paragraph = table.find_previous('w:p') | |
if previous_paragraph: | |
return previous_paragraph.get_text() | |
return None | |