add table.py

This commit is contained in:
Andrey Kuvshinov
2025-08-30 23:30:59 +03:00
commit 3b3f7097df
2 changed files with 605 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.pdf
*.html

603
table.py Normal file
View File

@@ -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 = '<table class="pdf-table" cellpadding="5" cellspacing="0">\n'
for row_index, row in enumerate(processed_table):
html_table += " <tr>\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' <td{colspan_attr}{rowspan_attr}{class_attr}{style_attr}>{cell_text}</td>\n'
html_table += " </tr>\n"
html_table += "</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"""
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>{title}</title>
<style>
.pdf-table {{
border-collapse: collapse;
width: 100%;
margin-bottom: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background-color: white;
border: 1px solid #ddd;
table-layout: auto;
}}
.pdf-table-cell {{
padding: 10px;
text-align: left;
background-color: white;
word-wrap: break-word;
}}
.pdf-table-header {{
font-weight: bold;
background-color: #f8f9fa;
}}
.pdf-table-header-horizontal {{
text-align: center;
font-size: 1.1em;
background-color: #e8f4f8;
}}
.pdf-table-cell-empty {{
background-color: white;
}}
/* Стиль для специальных значений - запрет переноса */
.pdf-table-cell-special {{
white-space: nowrap;
min-width: 30px;
}}
.pdf-table tr:hover .pdf-table-cell {{
background-color: #f5f5f5;
}}
.pdf-table tr:hover .pdf-table-header {{
background-color: #e0e0e0;
}}
</style>
</head>
<body>
<h1>{title}</h1>
{tables_html}
</body>
</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")