-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreader.py
More file actions
365 lines (306 loc) · 15.8 KB
/
Copy pathreader.py
File metadata and controls
365 lines (306 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
import fitz # PyMuPDF
import json
import os
def analyze_pdf(pdf_path, output_json="output.json", images_dir="book_images"):
# 1. Создаем папку для картинок, если её нет
if not os.path.exists(images_dir):
os.makedirs(images_dir)
print(f"Создана папка для изображений: {images_dir}")
try:
doc = fitz.open(pdf_path)
except Exception as e:
print(f"Ошибка открытия файла: {e}")
return
full_data = []
print(f"Начинаем обработку: {pdf_path} ({doc.page_count} стр.)")
for page_num, page in enumerate(doc):
page_data = {
"page": page_num + 1,
"width": page.rect.width,
"height": page.rect.height,
"elements": [], # Текст
"images": [] # Картинки
}
# Получаем структуру страницы
blocks = page.get_text("dict")["blocks"]
for block in blocks:
# --- ТЕКСТ (Type 0) ---
if block["type"] == 0:
for line in block["lines"]:
for span in line["spans"]:
font_flags = span["flags"]
page_data["elements"].append({
"type": "text",
"text": span["text"],
"bbox": span["bbox"],
"font": span["font"],
"size": span["size"],
"color": span["color"],
"styles": {
"bold": bool(font_flags & 2 ** 4),
"italic": bool(font_flags & 2 ** 1)
}
})
# --- КАРТИНКИ (Type 1) ---
elif block["type"] == 1:
# Генерируем уникальное имя файла
# Пример: book_images/img_p5_1.png (страница 5, индекс 1)
image_filename = f"img_p{page_num + 1}_{len(page_data['images'])}.{block['ext']}"
image_path = os.path.join(images_dir, image_filename)
# СОХРАНЯЕМ ФАЙЛ НА ДИСК
try:
with open(image_path, "wb") as img_file:
img_file.write(block["image"]) # block["image"] содержит байты
except Exception as e:
print(f"Ошибка сохранения картинки {image_filename}: {e}")
continue
# Добавляем инфу в JSON (но не сами байты, а путь!)
image_info = {
"type": "image",
"bbox": block["bbox"],
"width": block["width"],
"height": block["height"],
"ext": block["ext"],
"src": image_path # <--- ВАЖНО: Путь к сохраненному файлу
}
page_data["images"].append(image_info)
full_data.append(page_data)
if (page_num + 1) % 50 == 0:
print(f"Обработано {page_num + 1} страниц...")
doc.close()
# Сохраняем JSON
with open(output_json, "w", encoding="utf-8") as f:
json.dump(full_data, f, ensure_ascii=False, indent=2)
print(f"Готово! Данные в '{output_json}', картинки в папке '{images_dir}'")
# --- ЗАПУСК ---
if __name__ == "__main__":
analyze_pdf(input("Введите путь к PDF файлу: "))
import json
import re
from collections import Counter
class PDFStructurer:
def __init__(self, json_path):
self.json_path = json_path
self.raw_pages = []
self.content_flow = []
self.structure = []
self.body_font_size = 0
self.header_levels = {}
# Сет для запоминания номеров глав, чтобы не дублировать заголовки
self.seen_chapters = set()
def load(self):
try:
with open(self.json_path, 'r', encoding='utf-8') as f:
self.raw_pages = json.load(f)
print(f"Загружено {len(self.raw_pages)} страниц.")
except FileNotFoundError:
print(f"Ошибка: Файл {self.json_path} не найден.")
exit()
def run(self):
self.load()
print("1. Сборка контента...")
self._process_content_flow()
print("2. Поиск скрытых заголовков (FIX)...")
self._fix_embedded_headers()
print("3. Анализ стилей...")
self._analyze_styles()
print("4. Построение иерархии (с проверкой дубликатов)...")
self._build_hierarchy()
return self.structure
# --- 1. ЛОГИКА РАЗДЕЛЕНИЯ СКЛЕЕННЫХ ЗАГОЛОВКОВ ---
def _fix_embedded_headers(self):
"""
Ищет 'Глава X' внутри строки.
Срабатывает ТОЛЬКО если есть спецсимвол (точка или пуля),
что гарантирует, что это именно НАЗВАНИЕ, а не просто текст.
"""
new_flow = []
# Паттерн ищет: Текст... цифра... Глава ЧИСЛО [• или .]
pattern = r'(.*?)(?:\s+\d+\s+)?((?:Глава|Chapter|Часть|Part)\s+[\dIVX]+\s*[•\.].*)'
for item in self.content_flow:
if item['type'] == 'image':
new_flow.append(item)
continue
text = item['text']
match = re.search(pattern, text, re.IGNORECASE)
# Если нашли склейку и она не в самом начале строки
if match and len(match.group(1).strip()) > 1:
pre_text = match.group(1).strip()
header_text = match.group(2).strip()
# 1. Текст ДО
item_pre = item.copy()
item_pre['text'] = pre_text
new_flow.append(item_pre)
# 2. Заголовок ПОСЛЕ (искусственно увеличиваем шрифт, чтобы помочь детектору)
item_header = item.copy()
item_header['text'] = header_text
item_header['size'] = item['size'] * 1.5
new_flow.append(item_header)
else:
new_flow.append(item)
self.content_flow = new_flow
# --- 2. ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ---
def _is_page_number(self, element, page_height):
text = element['text'].strip()
y0 = element['bbox'][1]
if y0 <= (page_height * 0.93): return False
if re.match(r'^[\d\s\-\./]+$|^(page|стр\.?)\s*\d+$', text, re.IGNORECASE): return True
return False
def _get_chapter_id(self, text):
"""Извлекает уникальный ID главы (например, '1' или 'IV' из 'Глава 1...')"""
clean_text = text.strip().lower()
# Ищем 'Глава X'
match = re.search(r'^(?:глава|chapter|часть|part)\s+([\dIVX]+)', clean_text)
if match:
return f"{clean_text.split()[0]}_{match.group(1)}" # Пример: глава_01
# Если это Введение/Заключение (без номера)
if re.match(r'^(введение|introduction|предисловие|заключение|эпилог)$', clean_text):
return clean_text
return None
def _get_forced_header_level(self, text):
clean_text = text.strip().lower()
if len(clean_text) > 150: return None # Слишком длинно для заголовка
# Строгий паттерн с точкой/пулей (точно заголовок)
if re.search(r'^(?:глава|chapter)\s+[\dIVX]+\s*[•\.]', clean_text):
return 1
# Обычные паттерны
patterns = [
r'^(?:глава|chapter|часть|part|раздел)\s*[\dIVX\.]+',
r'^(?:введение|introduction|предисловие|заключение|эпилог)$',
r'^\d+\.\s+\w+',
]
for p in patterns:
if re.search(p, clean_text): return 1
return None
def _process_content_flow(self):
# (Этот метод остался без изменений, он собирает строки и картинки)
all_content = []
for page in self.raw_pages:
page_height = page['height']
text_elements = [el for el in page['elements'] if el['type'] == 'text']
image_elements = []
for img in page.get('images', []):
img['content_type'] = 'image'
img['text'] = '[IMAGE]'
image_elements.append(img)
valid_elements = []
for el in text_elements:
if not self._is_page_number(el, page_height):
el['content_type'] = 'text'
valid_elements.append(el)
valid_elements.extend(image_elements)
valid_elements.sort(key=lambda x: (round(x['bbox'][1], 1), x['bbox'][0]))
if not valid_elements: continue
current_line = None
for el in valid_elements:
if el['content_type'] == 'image':
if current_line:
all_content.append(current_line)
current_line = None
all_content.append(el) # Сохраняем весь объект картинки
else:
if current_line is None:
current_line = {
"type": "text_line", "text": el['text'], "size": el['size'],
"is_bold": el['styles']['bold'], "y": el['bbox'][1], "page": page['page']
}
else:
if abs(el['bbox'][1] - current_line['y']) < 5:
current_line['text'] += " " + el['text']
if el['size'] > current_line['size']:
current_line['size'] = el['size']
current_line['is_bold'] = el['styles']['bold']
else:
all_content.append(current_line)
current_line = {
"type": "text_line", "text": el['text'], "size": el['size'],
"is_bold": el['styles']['bold'], "y": el['bbox'][1], "page": page['page']
}
if current_line: all_content.append(current_line)
self.content_flow = all_content
def _analyze_styles(self):
text_lines = [line for line in self.content_flow if line.get('type') == 'text_line']
if not text_lines: return
sizes = [round(line['size'] * 2) / 2 for line in text_lines if len(line['text'].strip()) > 2]
if not sizes: return
size_counts = Counter(sizes)
self.body_font_size = size_counts.most_common(1)[0][0]
header_candidates = [s for s, c in size_counts.items() if s > self.body_font_size * 1.1]
header_candidates.sort(reverse=True)
for i, size in enumerate(header_candidates):
self.header_levels[size] = i + 1
# --- 3. ПОСТРОЕНИЕ ИЕРАРХИИ (ГЛАВНАЯ ЛОГИКА) ---
def _build_hierarchy(self):
root = {"type": "root", "children": []}
stack = [root]
for item in self.content_flow:
# КАРТИНКА
if item.get('content_type') == 'image':
real_src = item.get('src', "")
if not real_src: real_src = f"img_p{item['page']}.{item['ext']}"
stack[-1]["children"].append({
"type": "image", "src": real_src, "width": item['width'], "height": item['height']
})
continue
# ТЕКСТ
text = item['text'].strip()
if not text: continue
size = round(item['size'] * 2) / 2
# 1. Определяем, похоже ли это на заголовок технически
forced_level = self._get_forced_header_level(text)
style_level = self.header_levels.get(size)
final_level = forced_level if forced_level else style_level
# Флаг для курсива
make_italic = False
# 2. Если это похоже на заголовок уровня 1 (Глава...), проверяем дубликаты
if final_level == 1:
chapter_id = self._get_chapter_id(text)
if chapter_id:
if chapter_id in self.seen_chapters:
# МЫ ЭТО УЖЕ ВИДЕЛИ!
# Отменяем статус заголовка
final_level = None
# Включаем курсив
make_italic = True
else:
# Первый раз видим -> запоминаем
self.seen_chapters.add(chapter_id)
# --- ВЕТКА ЗАГОЛОВКА ---
if final_level:
node = {"type": "header", "level": final_level, "title": text, "children": []}
while len(stack) > 1:
if stack[-1].get("level", 0) >= final_level: stack.pop()
else: break
stack[-1]["children"].append(node)
stack.append(node)
# --- ВЕТКА ОБЫЧНОГО ТЕКСТА ---
else:
if size <= self.body_font_size * 1.1 or make_italic:
parent = stack[-1]
children = parent["children"]
# Если нужно сделать курсив (дубликат главы)
if make_italic:
# Добавляем как новый параграф с пометкой style: italic
children.append({
"type": "paragraph",
"text": text,
"style": "italic"
})
else:
# Обычный текст - склеиваем с предыдущим
if children and children[-1]["type"] == "paragraph" and not children[-1].get("style"):
children[-1]["text"] += " " + text
else:
children.append({"type": "paragraph", "text": text})
self.structure = root
def save_json(self, output_path):
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(self.structure, f, ensure_ascii=False, indent=2)
print(f"Готово! Результат в файле: {output_path}")
# --- ЗАПУСК ---
if __name__ == "__main__":
# Убедись, что 'output.json' (из первого скрипта) лежит рядом
processor = PDFStructurer("output.json")
processor.run()
processor.save_json("final_book.json")
os.remove("output.json") # Удаляем временный файл после использования