commit 3b3f7097df433c69c6a933eaaa33aba1a1a161c7 Author: Andrey Kuvshinov Date: Sat Aug 30 23:30:59 2025 +0300 add table.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4df0be --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pdf +*.html diff --git a/table.py b/table.py new file mode 100644 index 0000000..ca9209f --- /dev/null +++ b/table.py @@ -0,0 +1,603 @@ +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 = '\n' + + for row_index, row in enumerate(processed_table): + html_table += " \n" + + for cell_info in row: + content = cell_info['content'] + colspan = cell_info['colspan'] + rowspan = cell_info['rowspan'] + cell_type = cell_info['type'] + is_last_row = cell_info.get('is_last_row', False) + is_last_col = cell_info.get('is_last_col', False) + is_special = cell_info.get('is_special', False) + is_header = cell_info.get('is_header', False) + + # Экранируем HTML символы + cell_text = str(content) if content is not None else "" + cell_text = html.escape(cell_text) + + if colspan == 0 and rowspan == 0: + continue + + # Определяем CSS классы + classes = ["pdf-table-cell"] + + if cell_type == 'horizontal' or is_header: + classes.extend(["pdf-table-header", "pdf-table-header-horizontal"]) + elif cell_type == 'empty': + classes.append("pdf-table-cell-empty") + if is_special: + classes.append("pdf-table-cell-special") + + # Определяем стили границ + border_styles = [] + + if cell_type in ['normal', 'horizontal'] or is_header: + border_styles.append("border-top: 1px solid #ddd") + border_styles.append("border-bottom: none") + else: + border_styles.append("border: none") + + border_styles.append("border-left: 1px solid #ddd") + border_styles.append("border-right: 1px solid #ddd") + + if is_last_row: + border_styles.append("border-bottom: 1px solid #ddd") + + # Собираем атрибуты + colspan_attr = f' colspan="{colspan}"' if colspan > 1 else '' + rowspan_attr = f' rowspan="{rowspan}"' if rowspan > 1 else '' + class_attr = f' class="{" ".join(classes)}"' + style_attr = f' style="{"; ".join(border_styles)}"' + + html_table += f' {cell_text}\n' + + html_table += " \n" + + html_table += "
" + return html_table + +def extract_tables_from_pdf(pdf_path, start_page, end_page): + """Извлекает таблицы из PDF с умным объединением страниц.""" + all_page_tables = [] + + with pdfplumber.open(pdf_path) as pdf: + if end_page >= len(pdf.pages): + end_page = len(pdf.pages) - 1 + + print(f"Обрабатываются страницы с {start_page + 1} по {end_page + 1}") + + for page_num in range(start_page, end_page + 1): + page = pdf.pages[page_num] + tables = page.extract_tables() + + if tables: + print(f"На странице {page_num + 1} найдено {len(tables)} таблиц") + + for table in tables: + cleaned_table = remove_empty_rows(table) + if cleaned_table: + all_page_tables.append(cleaned_table) + else: + print(f"На странице {page_num + 1} таблиц не найдено") + + if not all_page_tables: + print("Не найдено ни одной таблицы для обработки") + return "" + + # Умное объединение таблиц с учетом ОГРН продолжений + merged_tables = merge_ogrn_continuation_tables(all_page_tables) + + # Разделяем таблицы по заголовкам + separated_tables = split_tables_by_headers(merged_tables) + + print(f"После объединения и разделения по заголовкам получено {len(separated_tables)} таблиц") + + # Конвертируем каждую таблицу в HTML + html_tables = [] + for table in separated_tables: + processed_table = process_table_for_merging(table) + if processed_table: + html_table = table_to_html(processed_table) + html_tables.append(html_table) + + return '\n'.join(html_tables) + +def save_tables_to_html(tables_html, output_html_path, full_html=True, title="Таблицы из PDF"): + """Сохраняет HTML-код таблиц в файл.""" + if full_html: + full_html_content = f""" + + + + + {title} + + + +

{title}

+ {tables_html} + + + """ + else: + full_html_content = tables_html + + with open(output_html_path, "w", encoding="utf-8") as f: + f.write(full_html_content) + print(f"HTML файл сохранен как: {output_html_path}") + +def extract_tables_to_html(pdf_path, start_page, end_page, output_html_path, full_html=True): + """Полная функция для извлечения таблиц и сохранения в HTML.""" + tables_html = extract_tables_from_pdf(pdf_path, start_page, end_page) + if tables_html: + save_tables_to_html(tables_html, output_html_path, full_html) + else: + print("Нечего сохранять - таблицы не найдены") + +# Пример использования +if __name__ == "__main__": + extract_tables_to_html("1.pdf", 5, 500, "1.html") \ No newline at end of file