import pdfplumber import re import html def is_empty_row(row): """Проверяет, является ли строка полностью пустой.""" if not row: return True for cell in row: cell_content = str(cell).strip() if cell is not None else "" if cell_content: return False return True def remove_empty_rows(table): """Удаляет полностью пустые строки из таблицы.""" if not table: return [] return [row for row in table if not is_empty_row(row)] def should_merge_row(row): """Проверяет, нужно ли объединять ячейки в строке по горизонтали.""" if not row: return False first_cell = str(row[0]).strip() if row[0] is not None else "" if not first_cell: return False for cell in row[1:]: cell_content = str(cell).strip() if cell is not None else "" if cell_content: return False return True def is_ogrn_continuation(cell_value): """ Проверяет, является ли значение продолжением ОГРН записи. Возвращает True, если значение начинается с 'ОГРН:'. """ if not cell_value: return False cell_str = str(cell_value).strip() return cell_str.startswith('ОГРН:') or cell_str.startswith('ОГРН :') def get_last_non_empty_value(table, column_index): """ Возвращает последнее непустое значение в указанной колонке таблицы. Ищет снизу вверх. """ if not table: return None for row in reversed(table): if column_index < len(row): cell_value = str(row[column_index]).strip() if row[column_index] is not None else "" if cell_value: return cell_value return None def get_first_non_empty_value(table, column_index): """ Возвращает первое непустое значение в указанной колонке таблицы. Ищет сверху вниз. """ if not table: return None for row in table: if column_index < len(row): cell_value = str(row[column_index]).strip() if row[column_index] is not None else "" if cell_value: return cell_value return None def is_pagination_continuation(prev_table, new_table): """ Проверяет, является ли новая таблица продолжением предыдущей из-за пагинации. Новое условие: если в первой колонке разные значения, но в последней колонке пусто, то это общее значение, которое съехало на вторую страницу. """ if not prev_table or not new_table: return False # Получаем последние значения из предыдущей таблицы prev_first_col = get_last_non_empty_value(prev_table, 0) prev_last_col = get_last_non_empty_value(prev_table, -1) # Получаем первые значения из новой таблицы new_first_col = get_first_non_empty_value(new_table, 0) new_last_col = get_first_non_empty_value(new_table, -1) # Новое условие: разные значения в первой колонке И пустая последняя колонка в новой таблице if (prev_first_col and new_first_col and prev_first_col != new_first_col and not new_last_col): return True return False def is_continuation_table(prev_table, new_table): """ Проверяет, является ли новая таблица продолжением предыдущей. Сравнивает последние непустые значения первой и последней колонок. """ if not prev_table or not new_table: return False # Если первая строка новой таблицы содержит ОГРН продолжение - это продолжение if new_table and new_table[0] and is_ogrn_continuation(new_table[0][0]): return True # Проверяем пагинационное продолжение (новое условие) if is_pagination_continuation(prev_table, new_table): return True # Получаем последние непустые значения из предыдущей таблицы prev_first_col_value = get_last_non_empty_value(prev_table, 0) prev_last_col_value = get_last_non_empty_value(prev_table, -1) # Получаем первые непустые значения из новой таблиции new_first_col_value = get_first_non_empty_value(new_table, 0) new_last_col_value = get_first_non_empty_value(new_table, -1) # Если оба значения совпадают и не пустые - это продолжение if (prev_first_col_value and new_first_col_value and prev_first_col_value == new_first_col_value and prev_last_col_value and new_last_col_value and prev_last_col_value == new_last_col_value): return True return False def merge_pagination_continuation(prev_table, new_table): """ Объединяет таблицы при пагинационном продолжении. Склеивает общее значение, которое съехало на вторую страницу. """ if not prev_table or not new_table: return prev_table # Получаем последнее значение из предыдущей таблицы (общее значение) last_prev_value = get_last_non_empty_value(prev_table, 0) if not last_prev_value: return prev_table # Находим индекс последней непустой строки в предыдущей таблице last_row_index = -1 for i in range(len(prev_table) - 1, -1, -1): if not is_empty_row(prev_table[i]): last_row_index = i break if last_row_index < 0: return prev_table # Объединяем значения: добавляем первое значение новой таблицы к последнему значению предыдущей first_new_value = get_first_non_empty_value(new_table, 0) if first_new_value: # Объединяем значения через пробел merged_value = f"{last_prev_value} {first_new_value}" # Заменяем значение в предыдущей таблице prev_table[last_row_index] = [merged_value] + list(prev_table[last_row_index][1:]) # Удаляем первую строку из новой таблицы (так как мы её уже объединили) if len(new_table) > 1: return prev_table + new_table[1:] else: return prev_table return prev_table + new_table def merge_ogrn_continuation_tables(all_tables): """ Объединяет таблицы, где первая строка содержит продолжение ОГРН. """ if not all_tables: return [] merged_tables = [] current_table = None for i, table in enumerate(all_tables): if not table: continue cleaned_table = remove_empty_rows(table) if not cleaned_table: continue if current_table is None: current_table = cleaned_table continue # Проверяем, является ли первая строка продолжением ОГРН first_row = cleaned_table[0] if cleaned_table else [] first_cell = first_row[0] if first_row and len(first_row) > 0 else "" if is_ogrn_continuation(first_cell): # Это продолжение ОГРН - объединяем с предыдущей таблицей print(f"Таблица {i+1} содержит продолжение ОГРН - объединяем") # Находим последнюю непустую строку в текущей таблице last_row_index = -1 for j in range(len(current_table) - 1, -1, -1): if not is_empty_row(current_table[j]): last_row_index = j break if last_row_index >= 0: # Объединяем ОГРН значение с последней строкой last_row = current_table[last_row_index] if len(last_row) > 0: # Добавляем ОГРН значение к последней ячейке первой колонки ogrn_value = str(first_cell).strip() last_row_value = str(last_row[0]).strip() if last_row[0] is not None else "" if last_row_value: # Объединяем значения merged_value = f"{last_row_value} {ogrn_value}" current_table[last_row_index] = [merged_value] + list(last_row[1:]) # Добавляем остальные строки из новой таблицы (кроме первой) if len(cleaned_table) > 1: current_table.extend(cleaned_table[1:]) else: current_table.extend(cleaned_table) else: current_table.extend(cleaned_table) else: # Проверяем обычное продолжение таблицы if is_continuation_table(current_table, cleaned_table): print(f"Таблица {i+1} является продолжением - объединяем") # Проверяем специальный случай пагинационного продолжения if is_pagination_continuation(current_table, cleaned_table): current_table = merge_pagination_continuation(current_table, cleaned_table) else: # Находим с какой строки начинать добавление (пропускаем дублирующиеся значения) start_index = 0 for j, new_row in enumerate(cleaned_table): new_first_val = str(new_row[0]).strip() if new_row and new_row[0] is not None else "" new_last_val = str(new_row[-1]).strip() if new_row and len(new_row) > 0 and new_row[-1] is not None else "" if new_first_val and new_last_val: # Проверяем, есть ли такое же значение в конце текущей таблицы last_current_first = get_last_non_empty_value(current_table, 0) last_current_last = get_last_non_empty_value(current_table, -1) if (new_first_val == last_current_first and new_last_val == last_current_last): start_index = j + 1 # Пропускаем эту строку else: break else: break # Добавляем только непропущенные строки if start_index < len(cleaned_table): current_table.extend(cleaned_table[start_index:]) else: # Это новая таблица merged_tables.append(current_table) current_table = cleaned_table # Добавляем последнюю таблицу if current_table is not None: merged_tables.append(current_table) return merged_tables def is_special_value(value): """ Проверяет, является ли значение специальным (типа -(-), -(1) и т.д.). Эти значения не должны переноситься и сужать колонки. """ if not value: return False # Паттерны для специальных значений special_patterns = [ r'^\-\(.*\)$', # -(something) r'^\-\.$', # -. r'^\-$', # - r'^\.$', # . r'^\(.*\)$', # (something) r'^[\-\+]\d+$', # -123, +456 r'^\d+[\-\+]$', # 123-, 456+ ] for pattern in special_patterns: if re.match(pattern, value): return True return False def is_header_row(row): """ Проверяет, является ли строка заголовком таблицы. Заголовок - это строка, которая будет иметь классы pdf-table-header pdf-table-header-horizontal. """ if not row: return False # Проверяем, что это объединенная строка (одна ячейка заполнена, остальные пустые) if should_merge_row(row): return True # Дополнительные проверки для заголовков first_cell = str(row[0]).strip() if row[0] is not None else "" header_patterns = [ r'таблица\s+\d+', r'table\s+\d+', r'раздел\s+\d+', r'section\s+\d+', r'глава\s+\d+', r'chapter\s+\d+', r'часть\s+\d+', r'part\s+\d+' ] for pattern in header_patterns: if re.search(pattern, first_cell, re.IGNORECASE): return True return False def split_tables_by_headers(merged_tables): """ Разделяет объединенные таблицы по заголовкам. Каждая строка-заголовок начинает новую таблицу. """ if not merged_tables: return [] separated_tables = [] current_table = [] for table in merged_tables: for row in table: if is_header_row(row): # Если нашли заголовок и current_table не пуст, сохраняем предыдущую таблицу if current_table: separated_tables.append(current_table) current_table = [] # Добавляем заголовок в новую таблицу current_table.append(row) else: # Добавляем обычную строку в текущую таблицу current_table.append(row) # Добавляем последнюю таблицу if current_table: separated_tables.append(current_table) return separated_tables def process_table_for_merging(table): """Обрабатывает таблицу для объединения ячеек.""" if not table: return [] cleaned_table = remove_empty_rows(table) if not cleaned_table: return [] processed_table = [] col_count = len(cleaned_table[0]) row_count = len(cleaned_table) # Обрабатываем каждую строку for row_index, row in enumerate(cleaned_table): processed_row = [] is_last_row = (row_index == row_count - 1) is_header = is_header_row(row) if should_merge_row(row): # Горизонтальное объединение для всей строки cell_info = { 'content': row[0], 'colspan': col_count, 'rowspan': 1, 'type': 'horizontal', 'is_last_row': is_last_row, 'is_special': is_special_value(str(row[0]).strip() if row[0] is not None else ""), 'is_header': is_header } processed_row.append(cell_info) for i in range(1, col_count): cell_info = { 'content': '', 'colspan': 0, 'rowspan': 0, 'type': 'hidden', 'is_last_row': is_last_row and (i == col_count - 1), 'is_special': False, 'is_header': False } processed_row.append(cell_info) else: # Обычная строка for col_index, cell in enumerate(row): cell_content = str(cell).strip() if cell is not None else "" is_empty_cell = not cell_content is_special = is_special_value(cell_content) cell_info = { 'content': cell, 'colspan': 1, 'rowspan': 1, 'type': 'empty' if is_empty_cell else 'normal', 'is_last_row': is_last_row, 'is_last_col': (col_index == col_count - 1), 'is_special': is_special, 'is_header': False } processed_row.append(cell_info) processed_table.append(processed_row) return processed_table def table_to_html(processed_table): """Конвертирует обработанную таблицу в HTML.""" if not processed_table: return "" html_table = '
| {cell_text} | \n' html_table += "