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 @classmethod 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, ) @staticmethod 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 @staticmethod 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 @staticmethod 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