Spaces:
Running
Running
Update pdf_section_extractor.py
Browse files- pdf_section_extractor.py +493 -155
pdf_section_extractor.py
CHANGED
|
@@ -1,6 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import pdfplumber
|
| 2 |
import pandas as pd
|
| 3 |
import re
|
|
|
|
| 4 |
from typing import List, Dict, Tuple, Any
|
| 5 |
|
| 6 |
class PDFSectionExtractor:
|
|
@@ -8,8 +225,63 @@ class PDFSectionExtractor:
|
|
| 8 |
"""Initialize with path to PDF file."""
|
| 9 |
self.pdf_path = pdf_path
|
| 10 |
self.tables = []
|
| 11 |
-
self.table_names = {}
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def is_level_three_section(self, section_number: str) -> bool:
|
| 14 |
"""Check if the section number is a level three section (e.g., 2.1.1)."""
|
| 15 |
return len(section_number.split('.')) == 3
|
|
@@ -20,77 +292,139 @@ class PDFSectionExtractor:
|
|
| 20 |
return [(match.group(1), match.group(2).strip())
|
| 21 |
for match in re.finditer(table_pattern, text)]
|
| 22 |
|
| 23 |
-
def
|
| 24 |
-
"""
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
else:
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
def extract_tables(self) -> List[Dict[str, Any]]:
|
| 57 |
-
"""Extract tables with their names and merge split tables."""
|
| 58 |
-
tables_data = []
|
| 59 |
-
current_section = None
|
| 60 |
-
current_table_data = None
|
| 61 |
-
current_table_name = None
|
| 62 |
-
|
| 63 |
with pdfplumber.open(self.pdf_path) as pdf:
|
| 64 |
for page in pdf.pages:
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
for i, table in enumerate(tables):
|
| 76 |
-
table_name = None
|
| 77 |
-
if i < len(table_names):
|
| 78 |
-
table_num, name = table_names[i]
|
| 79 |
-
table_name = f"Table {table_num}: {name}"
|
| 80 |
-
|
| 81 |
-
if table: # Check if table has content
|
| 82 |
-
df = pd.DataFrame(table)
|
| 83 |
-
# Clean the DataFrame
|
| 84 |
-
df = df.dropna(how='all').dropna(axis=1, how='all')
|
| 85 |
-
# Replace None with empty string
|
| 86 |
-
df = df.fillna('')
|
| 87 |
-
|
| 88 |
-
tables_data.append({
|
| 89 |
-
'name': table_name,
|
| 90 |
-
'data': df
|
| 91 |
-
})
|
| 92 |
-
|
| 93 |
-
return tables_data
|
| 94 |
|
| 95 |
def extract_sections(self) -> List[Dict[str, str]]:
|
| 96 |
"""Extract sections from PDF with content, excluding tables."""
|
|
@@ -98,116 +432,120 @@ class PDFSectionExtractor:
|
|
| 98 |
current_section = None
|
| 99 |
current_content = []
|
| 100 |
section_pattern = r'^(\d+\.(?:\d+)?(?:\.\d+)?)\s+(.+)$'
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
with pdfplumber.open(self.pdf_path) as pdf:
|
| 103 |
for page in pdf.pages:
|
| 104 |
text = page.extract_text(x_tolerance=1)
|
| 105 |
if not text:
|
| 106 |
continue
|
| 107 |
-
|
| 108 |
lines = text.split('\n')
|
| 109 |
-
|
|
|
|
| 110 |
for line in lines:
|
| 111 |
-
|
| 112 |
-
|
|
|
|
| 113 |
if match:
|
| 114 |
if current_section:
|
| 115 |
-
content_text =
|
| 116 |
-
content_text = self.
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
sections.append({
|
| 121 |
-
'section_number': current_section[0],
|
| 122 |
-
'section_name': '',
|
| 123 |
-
'content': full_content
|
| 124 |
-
})
|
| 125 |
-
else:
|
| 126 |
-
sections.append({
|
| 127 |
-
'section_number': current_section[0],
|
| 128 |
-
'section_name': current_section[1],
|
| 129 |
-
'content': content_text
|
| 130 |
-
})
|
| 131 |
-
|
| 132 |
current_section = (match.group(1), match.group(2))
|
| 133 |
current_content = []
|
| 134 |
elif current_section:
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
if current_section:
|
| 139 |
-
content_text =
|
| 140 |
-
content_text = self.
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
sections.append({
|
| 145 |
-
'section_number': current_section[0],
|
| 146 |
-
'section_name': '',
|
| 147 |
-
'content': full_content
|
| 148 |
-
})
|
| 149 |
-
else:
|
| 150 |
-
sections.append({
|
| 151 |
-
'section_number': current_section[0],
|
| 152 |
-
'section_name': current_section[1],
|
| 153 |
-
'content': content_text
|
| 154 |
-
})
|
| 155 |
-
|
| 156 |
return sections
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
def convert_pdf_to_excel(pdf_path: str, excel_path: str):
|
| 159 |
-
"""Convert PDF with sections and tables to Excel file."""
|
| 160 |
try:
|
| 161 |
extractor = PDFSectionExtractor(pdf_path)
|
| 162 |
-
|
|
|
|
| 163 |
tables = extractor.extract_tables()
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
with pd.ExcelWriter(excel_path, engine='openpyxl') as writer:
|
| 166 |
-
# Write sections
|
| 167 |
-
df_sections = pd.DataFrame(sections)
|
| 168 |
df_sections.to_excel(writer, index=False, sheet_name='Sections')
|
| 169 |
|
| 170 |
-
#
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
# Write tables to separate sheets
|
| 176 |
-
for i, table_info in enumerate(tables, 1):
|
| 177 |
-
if table_info['name']:
|
| 178 |
-
sheet_name = table_info['name'][:31] # Excel sheet name length limit
|
| 179 |
-
else:
|
| 180 |
-
sheet_name = f'Table_{i}'
|
| 181 |
-
|
| 182 |
-
# Write table data
|
| 183 |
-
table_info['data'].to_excel(writer, sheet_name=sheet_name, index=False)
|
| 184 |
-
|
| 185 |
-
# Auto-adjust table sheet
|
| 186 |
-
worksheet = writer.sheets[sheet_name]
|
| 187 |
-
for column in worksheet.columns:
|
| 188 |
-
max_length = 0
|
| 189 |
-
column = [cell for cell in column]
|
| 190 |
-
for cell in column:
|
| 191 |
-
try:
|
| 192 |
-
if len(str(cell.value)) > max_length:
|
| 193 |
-
max_length = len(cell.value)
|
| 194 |
-
except:
|
| 195 |
-
pass
|
| 196 |
-
adjusted_width = (max_length + 2)
|
| 197 |
-
worksheet.column_dimensions[column[0].column_letter].width = adjusted_width
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
except Exception as e:
|
| 202 |
-
print(f"Error
|
| 203 |
-
|
| 204 |
|
| 205 |
if __name__ == "__main__":
|
| 206 |
-
pdf_path = "
|
| 207 |
-
excel_path = "
|
| 208 |
|
| 209 |
-
|
| 210 |
-
if success:
|
| 211 |
-
print("Successfully converted PDF to Excel!")
|
| 212 |
-
else:
|
| 213 |
-
print("Failed to convert PDF to Excel.")
|
|
|
|
| 1 |
+
# import pdfplumber
|
| 2 |
+
# import pandas as pd
|
| 3 |
+
# import re
|
| 4 |
+
# from typing import List, Dict, Tuple, Any
|
| 5 |
+
|
| 6 |
+
# class PDFSectionExtractor:
|
| 7 |
+
# def __init__(self, pdf_path: str):
|
| 8 |
+
# """Initialize with path to PDF file."""
|
| 9 |
+
# self.pdf_path = pdf_path
|
| 10 |
+
# self.tables = []
|
| 11 |
+
# self.table_names = {} # Store table names and their content
|
| 12 |
+
|
| 13 |
+
# def is_level_three_section(self, section_number: str) -> bool:
|
| 14 |
+
# """Check if the section number is a level three section (e.g., 2.1.1)."""
|
| 15 |
+
# return len(section_number.split('.')) == 3
|
| 16 |
+
|
| 17 |
+
# def find_table_names(self, text: str) -> List[Dict[str, str]]:
|
| 18 |
+
# """Extract table names from text."""
|
| 19 |
+
# table_pattern = r'\*\*Table\s+(\d+):\s+([^*]+)\*\*'
|
| 20 |
+
# return [(match.group(1), match.group(2).strip())
|
| 21 |
+
# for match in re.finditer(table_pattern, text)]
|
| 22 |
+
|
| 23 |
+
# def clean_content(self, content: str) -> str:
|
| 24 |
+
# """Remove table references and names from content."""
|
| 25 |
+
# # Remove table references
|
| 26 |
+
# content = re.sub(r'\*\*Table\s+\d+:\s+[^*]+\*\*', '', content)
|
| 27 |
+
# # Remove any empty lines created
|
| 28 |
+
# content = '\n'.join(line for line in content.split('\n') if line.strip())
|
| 29 |
+
# return content
|
| 30 |
+
|
| 31 |
+
# def merge_split_tables(self, tables: List[List]) -> List[List]:
|
| 32 |
+
# """Merge tables that are split across pages."""
|
| 33 |
+
# merged_tables = []
|
| 34 |
+
# current_table = None
|
| 35 |
+
|
| 36 |
+
# for table in tables:
|
| 37 |
+
# if not table:
|
| 38 |
+
# continue
|
| 39 |
+
|
| 40 |
+
# if current_table is None:
|
| 41 |
+
# current_table = table
|
| 42 |
+
# else:
|
| 43 |
+
# # Check if this table is a continuation
|
| 44 |
+
# # Compare the number of columns
|
| 45 |
+
# if len(table[0]) == len(current_table[0]):
|
| 46 |
+
# current_table.extend(table)
|
| 47 |
+
# else:
|
| 48 |
+
# merged_tables.append(current_table)
|
| 49 |
+
# current_table = table
|
| 50 |
+
|
| 51 |
+
# if current_table:
|
| 52 |
+
# merged_tables.append(current_table)
|
| 53 |
+
|
| 54 |
+
# return merged_tables
|
| 55 |
+
|
| 56 |
+
# def extract_tables(self) -> List[Dict[str, Any]]:
|
| 57 |
+
# """Extract tables with their names and merge split tables."""
|
| 58 |
+
# tables_data = []
|
| 59 |
+
# current_section = None
|
| 60 |
+
# current_table_data = None
|
| 61 |
+
# current_table_name = None
|
| 62 |
+
|
| 63 |
+
# with pdfplumber.open(self.pdf_path) as pdf:
|
| 64 |
+
# for page in pdf.pages:
|
| 65 |
+
# text = page.extract_text(x_tolerance=1,y_tolerance=0) or ''
|
| 66 |
+
|
| 67 |
+
# # Find table names in the text
|
| 68 |
+
# table_names = self.find_table_names(text)
|
| 69 |
+
# tables = page.extract_tables()
|
| 70 |
+
|
| 71 |
+
# # Process each table found
|
| 72 |
+
# if tables:
|
| 73 |
+
# tables = self.merge_split_tables(tables)
|
| 74 |
+
|
| 75 |
+
# for i, table in enumerate(tables):
|
| 76 |
+
# table_name = None
|
| 77 |
+
# if i < len(table_names):
|
| 78 |
+
# table_num, name = table_names[i]
|
| 79 |
+
# table_name = f"Table {table_num}: {name}"
|
| 80 |
+
|
| 81 |
+
# if table: # Check if table has content
|
| 82 |
+
# df = pd.DataFrame(table)
|
| 83 |
+
# # Clean the DataFrame
|
| 84 |
+
# df = df.dropna(how='all').dropna(axis=1, how='all')
|
| 85 |
+
# # Replace None with empty string
|
| 86 |
+
# df = df.fillna('')
|
| 87 |
+
|
| 88 |
+
# tables_data.append({
|
| 89 |
+
# 'name': table_name,
|
| 90 |
+
# 'data': df
|
| 91 |
+
# })
|
| 92 |
+
|
| 93 |
+
# return tables_data
|
| 94 |
+
|
| 95 |
+
# def extract_sections(self) -> List[Dict[str, str]]:
|
| 96 |
+
# """Extract sections from PDF with content, excluding tables."""
|
| 97 |
+
# sections = []
|
| 98 |
+
# current_section = None
|
| 99 |
+
# current_content = []
|
| 100 |
+
# section_pattern = r'^(\d+\.(?:\d+)?(?:\.\d+)?)\s+(.+)$'
|
| 101 |
+
|
| 102 |
+
# with pdfplumber.open(self.pdf_path) as pdf:
|
| 103 |
+
# for page in pdf.pages:
|
| 104 |
+
# text = page.extract_text(x_tolerance=1)
|
| 105 |
+
# if not text:
|
| 106 |
+
# continue
|
| 107 |
+
|
| 108 |
+
# lines = text.split('\n')
|
| 109 |
+
|
| 110 |
+
# for line in lines:
|
| 111 |
+
# match = re.match(section_pattern, line.strip())
|
| 112 |
+
|
| 113 |
+
# if match:
|
| 114 |
+
# if current_section:
|
| 115 |
+
# content_text = '\n'.join(current_content)
|
| 116 |
+
# content_text = self.clean_content(content_text)
|
| 117 |
+
|
| 118 |
+
# if self.is_level_three_section(current_section[0]):
|
| 119 |
+
# full_content = current_section[1] + '\n' + content_text
|
| 120 |
+
# sections.append({
|
| 121 |
+
# 'section_number': current_section[0],
|
| 122 |
+
# 'section_name': '',
|
| 123 |
+
# 'content': full_content
|
| 124 |
+
# })
|
| 125 |
+
# else:
|
| 126 |
+
# sections.append({
|
| 127 |
+
# 'section_number': current_section[0],
|
| 128 |
+
# 'section_name': current_section[1],
|
| 129 |
+
# 'content': content_text
|
| 130 |
+
# })
|
| 131 |
+
|
| 132 |
+
# current_section = (match.group(1), match.group(2))
|
| 133 |
+
# current_content = []
|
| 134 |
+
# elif current_section:
|
| 135 |
+
# current_content.append(line.strip())
|
| 136 |
+
|
| 137 |
+
# # Handle the last section
|
| 138 |
+
# if current_section:
|
| 139 |
+
# content_text = '\n'.join(current_content)
|
| 140 |
+
# content_text = self.clean_content(content_text)
|
| 141 |
+
|
| 142 |
+
# if self.is_level_three_section(current_section[0]):
|
| 143 |
+
# full_content = current_section[1] + '\n' + content_text
|
| 144 |
+
# sections.append({
|
| 145 |
+
# 'section_number': current_section[0],
|
| 146 |
+
# 'section_name': '',
|
| 147 |
+
# 'content': full_content
|
| 148 |
+
# })
|
| 149 |
+
# else:
|
| 150 |
+
# sections.append({
|
| 151 |
+
# 'section_number': current_section[0],
|
| 152 |
+
# 'section_name': current_section[1],
|
| 153 |
+
# 'content': content_text
|
| 154 |
+
# })
|
| 155 |
+
|
| 156 |
+
# return sections
|
| 157 |
+
|
| 158 |
+
# def convert_pdf_to_excel(pdf_path: str, excel_path: str):
|
| 159 |
+
# """Convert PDF with sections and tables to Excel file."""
|
| 160 |
+
# try:
|
| 161 |
+
# extractor = PDFSectionExtractor(pdf_path)
|
| 162 |
+
# sections = extractor.extract_sections()
|
| 163 |
+
# tables = extractor.extract_tables()
|
| 164 |
+
|
| 165 |
+
# with pd.ExcelWriter(excel_path, engine='openpyxl') as writer:
|
| 166 |
+
# # Write sections to main sheet
|
| 167 |
+
# df_sections = pd.DataFrame(sections)
|
| 168 |
+
# df_sections.to_excel(writer, index=False, sheet_name='Sections')
|
| 169 |
+
|
| 170 |
+
# # Auto-adjust sections sheet
|
| 171 |
+
# worksheet = writer.sheets['Sections']
|
| 172 |
+
# for idx, col in enumerate(['A', 'B', 'C']):
|
| 173 |
+
# worksheet.column_dimensions[col].width = 15 if idx < 2 else 50
|
| 174 |
+
|
| 175 |
+
# # Write tables to separate sheets
|
| 176 |
+
# for i, table_info in enumerate(tables, 1):
|
| 177 |
+
# if table_info['name']:
|
| 178 |
+
# sheet_name = table_info['name'][:31] # Excel sheet name length limit
|
| 179 |
+
# else:
|
| 180 |
+
# sheet_name = f'Table_{i}'
|
| 181 |
+
|
| 182 |
+
# # Write table data
|
| 183 |
+
# table_info['data'].to_excel(writer, sheet_name=sheet_name, index=False)
|
| 184 |
+
|
| 185 |
+
# # Auto-adjust table sheet
|
| 186 |
+
# worksheet = writer.sheets[sheet_name]
|
| 187 |
+
# for column in worksheet.columns:
|
| 188 |
+
# max_length = 0
|
| 189 |
+
# column = [cell for cell in column]
|
| 190 |
+
# for cell in column:
|
| 191 |
+
# try:
|
| 192 |
+
# if len(str(cell.value)) > max_length:
|
| 193 |
+
# max_length = len(cell.value)
|
| 194 |
+
# except:
|
| 195 |
+
# pass
|
| 196 |
+
# adjusted_width = (max_length + 2)
|
| 197 |
+
# worksheet.column_dimensions[column[0].column_letter].width = adjusted_width
|
| 198 |
+
|
| 199 |
+
# return True
|
| 200 |
+
|
| 201 |
+
# except Exception as e:
|
| 202 |
+
# print(f"Error converting PDF to Excel: {str(e)}")
|
| 203 |
+
# return False
|
| 204 |
+
|
| 205 |
+
# if __name__ == "__main__":
|
| 206 |
+
# pdf_path = "/Users/aakanksha.n/Desktop/pdf_to_excel/[00 12 10] 26251-100-3DR-S04-00001_002.docx.pdf"
|
| 207 |
+
# excel_path = "/Users/aakanksha.n/Desktop/pdf_to_excel/format4.xlsx"
|
| 208 |
+
|
| 209 |
+
# success = convert_pdf_to_excel(pdf_path, excel_path)
|
| 210 |
+
# if success:
|
| 211 |
+
# print("Successfully converted PDF to Excel!")
|
| 212 |
+
# else:
|
| 213 |
+
# print("Failed to convert PDF to Excel.")
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
|
| 217 |
import pdfplumber
|
| 218 |
import pandas as pd
|
| 219 |
import re
|
| 220 |
+
import os
|
| 221 |
from typing import List, Dict, Tuple, Any
|
| 222 |
|
| 223 |
class PDFSectionExtractor:
|
|
|
|
| 225 |
"""Initialize with path to PDF file."""
|
| 226 |
self.pdf_path = pdf_path
|
| 227 |
self.tables = []
|
| 228 |
+
self.table_names = {}
|
| 229 |
+
self.table_content_markers = set()
|
| 230 |
+
self.table_content_lines = set()
|
| 231 |
+
self.table_headers = set()
|
| 232 |
+
self.document_name = None # Set to None to identify when it's missing
|
| 233 |
+
self.document_id = None
|
| 234 |
+
|
| 235 |
+
def extract_document_info_from_pdf(self) -> Tuple[str, str]:
|
| 236 |
+
"""Extract document name and ID from the first page of PDF."""
|
| 237 |
+
with pdfplumber.open(self.pdf_path) as pdf:
|
| 238 |
+
if not pdf.pages:
|
| 239 |
+
return "", ""
|
| 240 |
+
|
| 241 |
+
first_page_text = pdf.pages[0].extract_text()
|
| 242 |
+
|
| 243 |
+
if not first_page_text:
|
| 244 |
+
return "", ""
|
| 245 |
+
|
| 246 |
+
doc_name_pattern = r'[dD]ocument\s*[nN]ame:\s*(.*?)(?:\n|$)'
|
| 247 |
+
doc_id_pattern = r'[dD]ocument\s*[iI][dD]:\s*(.*?)(?:\n|$)'
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
doc_name_match = re.search(doc_name_pattern, first_page_text)
|
| 251 |
+
doc_id_match = re.search(doc_id_pattern, first_page_text)
|
| 252 |
+
print(f" extract document info function{doc_name_match}")
|
| 253 |
+
|
| 254 |
+
doc_name = doc_name_match.group(1).strip() if doc_name_match else ""
|
| 255 |
+
doc_id = doc_id_match.group(1).strip() if doc_id_match else ""
|
| 256 |
+
|
| 257 |
+
# print(f"Document name from pdf inside function{doc_name},{doc_id}")
|
| 258 |
+
|
| 259 |
+
return doc_name, doc_id
|
| 260 |
+
|
| 261 |
+
def get_document_info(self) -> Tuple[str, str]:
|
| 262 |
+
"""
|
| 263 |
+
Get document info from the PDF content.
|
| 264 |
+
If missing, fall back to filename for document ID only.
|
| 265 |
+
"""
|
| 266 |
+
if self.document_name is None or self.document_id is None:
|
| 267 |
+
# Try extracting from PDF
|
| 268 |
+
doc_name, doc_id = self.extract_document_info_from_pdf()
|
| 269 |
+
self.document_name = doc_name
|
| 270 |
+
self.document_id = doc_id
|
| 271 |
+
# print(f"Entering if one,{self.document_name}")
|
| 272 |
+
|
| 273 |
+
# Fallback only if document ID or name is missing
|
| 274 |
+
if not self.document_name or not self.document_id:
|
| 275 |
+
doc_name = os.path.basename(self.pdf_path)
|
| 276 |
+
doc_id_match = re.search(r'\[\d{2}\s+\d{2}\s+\d{2}\]\s+(\d+)', doc_name)
|
| 277 |
+
if not self.document_id:
|
| 278 |
+
self.document_id = doc_id_match.group(1) if doc_id_match else ""
|
| 279 |
+
if not self.document_name:
|
| 280 |
+
self.document_name = doc_name
|
| 281 |
+
|
| 282 |
+
# print(f"final{self.document_name},{self.document_id}")
|
| 283 |
+
return self.document_name, self.document_id
|
| 284 |
+
|
| 285 |
def is_level_three_section(self, section_number: str) -> bool:
|
| 286 |
"""Check if the section number is a level three section (e.g., 2.1.1)."""
|
| 287 |
return len(section_number.split('.')) == 3
|
|
|
|
| 292 |
return [(match.group(1), match.group(2).strip())
|
| 293 |
for match in re.finditer(table_pattern, text)]
|
| 294 |
|
| 295 |
+
def store_table_headers(self, table: List[List[str]]):
|
| 296 |
+
"""Store table headers."""
|
| 297 |
+
if table and table[0]:
|
| 298 |
+
header_row = table[0]
|
| 299 |
+
for header in header_row:
|
| 300 |
+
if header:
|
| 301 |
+
header_text = self.fix_table_cell_spacing(header)
|
| 302 |
+
if header_text:
|
| 303 |
+
self.table_headers.add(header_text)
|
| 304 |
+
|
| 305 |
+
def store_table_content(self, table: List[List[str]]):
|
| 306 |
+
"""Store all content from table cells, including headers."""
|
| 307 |
+
if not table:
|
| 308 |
+
return
|
| 309 |
+
|
| 310 |
+
# Store headers separately
|
| 311 |
+
self.store_table_headers(table)
|
| 312 |
+
|
| 313 |
+
# Store all cell content
|
| 314 |
+
for row in table:
|
| 315 |
+
for cell in row:
|
| 316 |
+
if cell:
|
| 317 |
+
cell_text = self.fix_table_cell_spacing(cell)
|
| 318 |
+
if cell_text:
|
| 319 |
+
self.table_content_lines.add(cell_text)
|
| 320 |
+
|
| 321 |
+
def fix_table_cell_spacing(self, cell_text: str) -> str:
|
| 322 |
+
"""Fix spacing issues within table cells."""
|
| 323 |
+
if not isinstance(cell_text, str):
|
| 324 |
+
return str(cell_text)
|
| 325 |
+
|
| 326 |
+
text = str(cell_text).strip()
|
| 327 |
+
text = re.sub(r',(?=\S)', ', ', text)
|
| 328 |
+
text = re.sub(r'(\d)([a-zA-Z])', r'\1 \2', text)
|
| 329 |
+
text = re.sub(r'([a-zA-Z])(\d)', r'\1 \2', text)
|
| 330 |
+
text = re.sub(r'([a-z])([A-Z])', r'\1 \2', text)
|
| 331 |
+
text = re.sub(r'\s+', ' ', text)
|
| 332 |
+
return text.strip()
|
| 333 |
+
|
| 334 |
+
def contains_table_content(self, line: str) -> bool:
|
| 335 |
+
"""Check if a line contains any stored table content or headers."""
|
| 336 |
+
line = self.fix_table_cell_spacing(line)
|
| 337 |
+
return (any(table_line in line for table_line in self.table_content_lines) or
|
| 338 |
+
any(header in line for header in self.table_headers))
|
| 339 |
+
|
| 340 |
+
def is_table_content(self, line: str) -> bool:
|
| 341 |
+
"""Identify if a line is part of a table."""
|
| 342 |
+
if any(marker in line for marker in ['|', '+', '─', '│', '��', '┐', '└', '┘', '├', '┤', '┬', '┴']):
|
| 343 |
+
return True
|
| 344 |
+
if re.search(r'\S+\s{2,}\S+', line):
|
| 345 |
+
return True
|
| 346 |
+
if re.match(r'^[\s\-+|=_]{3,}$', line):
|
| 347 |
+
return True
|
| 348 |
+
return False
|
| 349 |
+
|
| 350 |
+
def filter_table_content(self, content: str) -> str:
|
| 351 |
+
"""Remove lines that contain table content."""
|
| 352 |
+
lines = content.split('\n')
|
| 353 |
+
filtered_lines = []
|
| 354 |
+
|
| 355 |
+
for line in lines:
|
| 356 |
+
line = line.strip()
|
| 357 |
+
if line and not self.contains_table_content(line):
|
| 358 |
+
filtered_lines.append(line)
|
| 359 |
+
|
| 360 |
+
return '\n'.join(filtered_lines)
|
| 361 |
+
|
| 362 |
+
def process_table(self, table: List[List[str]], table_num: int) -> pd.DataFrame:
|
| 363 |
+
"""Process table data with proper spacing and ensure unique column names."""
|
| 364 |
+
if not table:
|
| 365 |
+
return pd.DataFrame()
|
| 366 |
+
|
| 367 |
+
# Store all table content including headers
|
| 368 |
+
self.store_table_content(table)
|
| 369 |
+
|
| 370 |
+
# Create DataFrame with default column names
|
| 371 |
+
df = pd.DataFrame(table)
|
| 372 |
+
if df.empty:
|
| 373 |
+
return df
|
| 374 |
+
|
| 375 |
+
# Get the header row (first row)
|
| 376 |
+
header = df.iloc[0].tolist()
|
| 377 |
+
|
| 378 |
+
# Create unique column names
|
| 379 |
+
unique_cols = []
|
| 380 |
+
col_count = {}
|
| 381 |
+
|
| 382 |
+
for col in header:
|
| 383 |
+
col = str(col) if col else "Unnamed"
|
| 384 |
+
if col in col_count:
|
| 385 |
+
col_count[col] += 1
|
| 386 |
+
unique_cols.append(f"{col}_{col_count[col]}")
|
| 387 |
else:
|
| 388 |
+
col_count[col] = 0
|
| 389 |
+
unique_cols.append(col)
|
| 390 |
+
|
| 391 |
+
# Set the unique column names
|
| 392 |
+
df.columns = unique_cols
|
| 393 |
+
|
| 394 |
+
# Remove the header row since we used it for column names
|
| 395 |
+
df = df.iloc[1:].reset_index(drop=True)
|
| 396 |
+
|
| 397 |
+
# Fix spacing in cells
|
| 398 |
+
for col in df.columns:
|
| 399 |
+
df[col] = df[col].astype(str).apply(self.fix_table_cell_spacing)
|
| 400 |
+
|
| 401 |
+
# Add metadata
|
| 402 |
+
df.insert(0, 'Document_Name', self.document_name)
|
| 403 |
+
df.insert(1, 'Document_ID', self.document_id)
|
| 404 |
+
df.insert(2, 'Table_Number', table_num)
|
| 405 |
+
|
| 406 |
+
return df
|
| 407 |
+
|
| 408 |
+
def extract_tables(self) -> List[pd.DataFrame]:
|
| 409 |
+
"""Extract tables from PDF and return a list of DataFrames."""
|
| 410 |
+
tables = []
|
| 411 |
+
table_counter = 1
|
| 412 |
+
|
| 413 |
+
# Ensure we have document info before processing tables
|
| 414 |
+
self.get_document_info()
|
| 415 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
with pdfplumber.open(self.pdf_path) as pdf:
|
| 417 |
for page in pdf.pages:
|
| 418 |
+
detected_tables = page.extract_tables()
|
| 419 |
+
|
| 420 |
+
for table in detected_tables:
|
| 421 |
+
if table:
|
| 422 |
+
df = self.process_table(table, table_counter)
|
| 423 |
+
if not df.empty:
|
| 424 |
+
tables.append(df)
|
| 425 |
+
table_counter += 1
|
| 426 |
+
|
| 427 |
+
return tables
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
|
| 429 |
def extract_sections(self) -> List[Dict[str, str]]:
|
| 430 |
"""Extract sections from PDF with content, excluding tables."""
|
|
|
|
| 432 |
current_section = None
|
| 433 |
current_content = []
|
| 434 |
section_pattern = r'^(\d+\.(?:\d+)?(?:\.\d+)?)\s+(.+)$'
|
| 435 |
+
|
| 436 |
+
# Ensure we have document info before processing sections
|
| 437 |
+
doc_name, doc_id = self.get_document_info()
|
| 438 |
+
table_counter = 1
|
| 439 |
+
|
| 440 |
with pdfplumber.open(self.pdf_path) as pdf:
|
| 441 |
for page in pdf.pages:
|
| 442 |
text = page.extract_text(x_tolerance=1)
|
| 443 |
if not text:
|
| 444 |
continue
|
| 445 |
+
|
| 446 |
lines = text.split('\n')
|
| 447 |
+
table_found = False
|
| 448 |
+
|
| 449 |
for line in lines:
|
| 450 |
+
line = line.strip()
|
| 451 |
+
match = re.match(section_pattern, line)
|
| 452 |
+
|
| 453 |
if match:
|
| 454 |
if current_section:
|
| 455 |
+
content_text = self.process_content(current_content)
|
| 456 |
+
content_text = self.filter_table_content(content_text)
|
| 457 |
+
sections.append(self.create_section_dict(
|
| 458 |
+
doc_name, doc_id, current_section, content_text))
|
| 459 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
current_section = (match.group(1), match.group(2))
|
| 461 |
current_content = []
|
| 462 |
elif current_section:
|
| 463 |
+
if self.is_table_content(line):
|
| 464 |
+
if not table_found:
|
| 465 |
+
current_content.append(f"[Table {table_counter} is available in Table_{table_counter} sheet]")
|
| 466 |
+
table_counter += 1
|
| 467 |
+
table_found = True
|
| 468 |
+
else:
|
| 469 |
+
current_content.append(line)
|
| 470 |
+
table_found = False
|
| 471 |
+
|
| 472 |
if current_section:
|
| 473 |
+
content_text = self.process_content(current_content)
|
| 474 |
+
content_text = self.filter_table_content(content_text)
|
| 475 |
+
sections.append(self.create_section_dict(
|
| 476 |
+
doc_name, doc_id, current_section, content_text))
|
| 477 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
return sections
|
| 479 |
|
| 480 |
+
def process_content(self, content_lines: List[str]) -> str:
|
| 481 |
+
"""Process content lines and remove duplicate table references."""
|
| 482 |
+
processed_lines = []
|
| 483 |
+
last_line_was_table_ref = False
|
| 484 |
+
|
| 485 |
+
for line in content_lines: # Fixed: using content_lines parameter
|
| 486 |
+
line = line.strip()
|
| 487 |
+
if '[Table' in line:
|
| 488 |
+
if not last_line_was_table_ref:
|
| 489 |
+
processed_lines.append(line)
|
| 490 |
+
last_line_was_table_ref = True
|
| 491 |
+
else:
|
| 492 |
+
processed_lines.append(line)
|
| 493 |
+
last_line_was_table_ref = False
|
| 494 |
+
|
| 495 |
+
return '\n'.join(processed_lines)
|
| 496 |
+
|
| 497 |
+
def create_section_dict(self, doc_name: str, doc_id: str,
|
| 498 |
+
section_tuple: Tuple[str, str], content: str) -> Dict[str, str]:
|
| 499 |
+
"""Create a dictionary for section data."""
|
| 500 |
+
section_number, section_name = section_tuple
|
| 501 |
+
|
| 502 |
+
if self.is_level_three_section(section_number):
|
| 503 |
+
return {
|
| 504 |
+
'document_name': doc_name,
|
| 505 |
+
'document_id': doc_id,
|
| 506 |
+
'section_number': section_number,
|
| 507 |
+
'section_name': '',
|
| 508 |
+
'content': f"{section_name}\n{content}"
|
| 509 |
+
}
|
| 510 |
+
else:
|
| 511 |
+
return {
|
| 512 |
+
'document_name': doc_name,
|
| 513 |
+
'document_id': doc_id,
|
| 514 |
+
'section_number': section_number,
|
| 515 |
+
'section_name': section_name,
|
| 516 |
+
'content': content
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
def convert_pdf_to_excel(pdf_path: str, excel_path: str):
|
| 520 |
+
"""Convert PDF with sections and tables to Excel file with separate sheets for each table."""
|
| 521 |
try:
|
| 522 |
extractor = PDFSectionExtractor(pdf_path)
|
| 523 |
+
|
| 524 |
+
# Extract and process tables
|
| 525 |
tables = extractor.extract_tables()
|
| 526 |
+
|
| 527 |
+
# Extract sections
|
| 528 |
+
sections = extractor.extract_sections()
|
| 529 |
+
df_sections = pd.DataFrame(sections)
|
| 530 |
+
|
| 531 |
+
# Write to Excel file
|
| 532 |
with pd.ExcelWriter(excel_path, engine='openpyxl') as writer:
|
| 533 |
+
# Write sections sheet
|
|
|
|
| 534 |
df_sections.to_excel(writer, index=False, sheet_name='Sections')
|
| 535 |
|
| 536 |
+
# Write each table to its own sheet
|
| 537 |
+
for i, table_df in enumerate(tables, 1):
|
| 538 |
+
sheet_name = f'Table_{i}'
|
| 539 |
+
table_df.to_excel(writer, index=False, sheet_name=sheet_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
|
| 541 |
+
print(f"Successfully saved PDF data to {excel_path}")
|
| 542 |
+
print(f"Created {len(tables)} table sheets")
|
| 543 |
except Exception as e:
|
| 544 |
+
print(f"Error occurred: {e}")
|
| 545 |
+
raise
|
| 546 |
|
| 547 |
if __name__ == "__main__":
|
| 548 |
+
pdf_path = "[00 12 10] 26251-100-3DR-S04-00001_002.docx.pdf"
|
| 549 |
+
excel_path = "format.xlsx"
|
| 550 |
|
| 551 |
+
convert_pdf_to_excel(pdf_path, excel_path)
|
|
|
|
|
|
|
|
|
|
|
|