muryshev's picture
init
57cf043
raw
history blame
9.96 kB
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