add table.py
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.pdf
|
||||
*.html
|
||||
603
table.py
Normal file
603
table.py
Normal 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")
|
||||
Reference in New Issue
Block a user