-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpdf_smart_split.py
More file actions
332 lines (263 loc) · 13.2 KB
/
Copy pathpdf_smart_split.py
File metadata and controls
332 lines (263 loc) · 13.2 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
#!/usr/bin/env python3
"""
Интеллектуальная нарезка PDF с анализом контента
Версия 3.0 - Python с глубоким анализом
"""
import os
import sys
import subprocess
from pathlib import Path
from typing import List, Tuple, Optional
from dataclasses import dataclass
from datetime import datetime
try:
from pypdf import PdfReader
except ImportError:
print("Install: apt install python3-pypdf2 or pip install pypdf")
subprocess.run([sys.executable, "-m", "pip", "install", "pypdf", "--break-system-packages", "-q"], check=True)
from pypdf import PdfReader
@dataclass
class PageInfo:
"""Информация о странице"""
number: int
estimated_size: int
has_images: bool
has_fonts: bool
complexity_score: float
class PDFAnalyzer:
"""Анализатор PDF для определения характеристик страниц"""
def __init__(self, filepath: str):
self.filepath = filepath
self.file_size = os.path.getsize(filepath)
self.reader = PdfReader(filepath)
self.page_count = len(self.reader.pages)
self.base_avg = self.file_size / self.page_count
def analyze_page(self, page_num: int) -> PageInfo:
"""Анализирует отдельную страницу"""
page = self.reader.pages[page_num]
# Проверяем наличие изображений
has_images = False
image_count = 0
if '/XObject' in page['/Resources']:
xobjects = page['/Resources']['/XObject'].get_object()
for obj in xobjects:
if xobjects[obj]['/Subtype'] == '/Image':
has_images = True
image_count += 1
# Проверяем наличие шрифтов
has_fonts = False
font_count = 0
if '/Font' in page['/Resources']:
fonts = page['/Resources']['/Font'].get_object()
has_fonts = len(fonts) > 0
font_count = len(fonts)
# Получаем размер содержимого страницы (приблизительно)
content_size = len(page.get_contents().get_data()) if page.get_contents() else 0
# Вычисляем коэффициент сложности
complexity = 1.0
if has_images:
# Чем больше изображений, тем выше коэффициент
complexity *= (1.0 + 0.3 * min(image_count, 10))
if has_fonts:
# Встроенные шрифты увеличивают размер
complexity *= (1.0 + 0.15 * min(font_count, 5))
# Размер контента также влияет
if content_size > 100000: # Большой контент
complexity *= 1.4
elif content_size > 50000:
complexity *= 1.2
# Оцениваем размер страницы
estimated_size = int(self.base_avg * complexity)
return PageInfo(
number=page_num + 1,
estimated_size=estimated_size,
has_images=has_images,
has_fonts=has_fonts,
complexity_score=complexity
)
def analyze_sample(self, sample_size: int = 10) -> Tuple[float, int, int]:
"""
Анализирует образец страниц
Возвращает: (средний коэффициент сложности, страниц с изображениями, страниц со шрифтами)
"""
# Выбираем страницы равномерно по документу
step = max(1, self.page_count // sample_size)
sample_pages = list(range(0, self.page_count, step))[:sample_size]
total_complexity = 0
images_count = 0
fonts_count = 0
print(f"[Анализ] Проверяю {len(sample_pages)} образцов из {self.page_count} страниц...")
for page_num in sample_pages:
info = self.analyze_page(page_num)
total_complexity += info.complexity_score
if info.has_images:
images_count += 1
if info.has_fonts:
fonts_count += 1
avg_complexity = total_complexity / len(sample_pages)
return avg_complexity, images_count, fonts_count
class PDFSplitter:
"""Умная нарезка PDF"""
def __init__(self, input_file: str, max_size_mb: int = 10):
self.input_file = input_file
self.max_bytes = max_size_mb * 1024 * 1024
self.max_size_mb = max_size_mb
# Коэффициенты безопасности
self.safety_margin = 0.80 # 20% запаса
self.min_pages = 1
self.adaptive_growth = 5
# Инициализация анализатора
self.analyzer = PDFAnalyzer(input_file)
self.page_count = self.analyzer.page_count
# Проверка qpdf
if not self._check_qpdf():
raise RuntimeError("qpdf не установлен. Установите: apt-get install qpdf")
def _check_qpdf(self) -> bool:
"""Проверяет наличие qpdf"""
try:
subprocess.run(['qpdf', '--version'], capture_output=True, check=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def _test_segment(self, start: int, end: int, output: str) -> int:
"""Тестирует создание сегмента и возвращает его размер"""
try:
subprocess.run(
['qpdf', '--empty', '--pages', self.input_file, f'{start}-{end}', '--', output],
capture_output=True,
check=True
)
return os.path.getsize(output)
except subprocess.CalledProcessError:
return 0
def _binary_search_optimal(self, start: int, initial_end: int) -> int:
"""Бинарный поиск оптимального диапазона страниц"""
low = start
high = min(initial_end, self.page_count)
best_end = start
temp_file = f"/tmp/test_{os.getpid()}.pdf"
# Сначала проверяем начальную оценку
test_size = self._test_segment(start, high, temp_file)
if test_size <= self.max_bytes and high < self.page_count:
# Начальная оценка влезла, пробуем расширить
best_end = high
# Адаптивное расширение
while high < self.page_count:
new_high = min(high + self.adaptive_growth, self.page_count)
test_size = self._test_segment(start, new_high, temp_file)
if test_size <= self.max_bytes:
best_end = new_high
high = new_high
else:
# Превысили, делаем точный бинарный поиск
low = high
high = new_high
while low < high - 1:
mid = (low + high) // 2
test_size = self._test_segment(start, mid, temp_file)
if test_size <= self.max_bytes:
best_end = mid
low = mid
else:
high = mid
break
elif test_size > self.max_bytes:
# Начальная оценка слишком большая, уменьшаем
while low < high - 1:
mid = (low + high) // 2
test_size = self._test_segment(start, mid, temp_file)
if test_size <= self.max_bytes:
best_end = mid
low = mid
else:
high = mid
else:
best_end = high
# Удаляем временный файл
if os.path.exists(temp_file):
os.remove(temp_file)
return best_end
def split(self) -> List[str]:
"""Выполняет нарезку PDF"""
print("=" * 70)
print("Интеллектуальная нарезка PDF v3.0")
print("=" * 70)
# Анализируем документ
print(f"\n[Анализ] Файл: {self.input_file}")
print(f"[Анализ] Размер: {self.analyzer.file_size / (1024*1024):.2f} МБ")
print(f"[Анализ] Страниц: {self.page_count}")
print(f"[Анализ] Средний размер страницы: {self.analyzer.base_avg / 1024:.2f} КБ")
# Проводим выборочный анализ
avg_complexity, img_count, font_count = self.analyzer.analyze_sample()
print(f"[Анализ] Средняя сложность: {avg_complexity:.2f}x")
print(f"[Анализ] Страниц с изображениями: {img_count}/10")
print(f"[Анализ] Страниц со шрифтами: {font_count}/10")
# Корректируем базовую оценку
adjusted_avg = int(self.analyzer.base_avg * avg_complexity)
print(f"[Анализ] Откорректированный средний: {adjusted_avg / 1024:.2f} КБ")
print("\n" + "=" * 70)
print(f"Начинаем нарезку с лимитом {self.max_size_mb} МБ")
print("=" * 70 + "\n")
current = 1
part = 1
results = []
while current <= self.page_count:
# Оцениваем количество страниц для сегмента
safe_limit = int(self.max_bytes * self.safety_margin)
estimate = max(self.min_pages, safe_limit // adjusted_avg)
initial_end = min(current + estimate - 1, self.page_count)
print(f"[Часть {part}] Начало со стр. {current}, оценка до стр. {initial_end}")
# Ищем оптимальный диапазон
final_end = self._binary_search_optimal(current, initial_end)
# Если даже одна страница не влезает
if final_end < current:
print(f" ⚠️ Страница {current} превышает лимит {self.max_size_mb}МБ!")
final_end = current
# Создаём финальный сегмент
output = f"part-{part:03d}.pdf"
actual_size = self._test_segment(current, final_end, output)
pages_in_part = final_end - current + 1
size_mb = actual_size / (1024 * 1024)
avg_in_part = actual_size / pages_in_part if pages_in_part > 0 else 0
status = "✓" if actual_size <= self.max_bytes else "⚠️"
print(f" {status} Создан {output}: стр. {current}-{final_end} "
f"({pages_in_part} стр., {size_mb:.2f} МБ)")
print(f" Средний в части: {avg_in_part / 1024:.2f} КБ/стр")
if actual_size > self.max_bytes:
print(f" ⚠️ ПРЕВЫШЕНИЕ ЛИМИТА!")
# Обновляем скользящее среднее для следующей итерации
adjusted_avg = int((adjusted_avg * 3 + avg_in_part) / 4)
results.append(output)
current = final_end + 1
part += 1
print("\n" + "=" * 70)
print(f"✓ ГОТОВО! Создано сегментов: {len(results)}")
print("=" * 70)
# Итоговая таблица
print("\nРазмеры сегментов:")
for result in results:
size = os.path.getsize(result)
print(f" {result}: {size / (1024*1024):.2f} МБ")
return results
def main():
"""Главная функция"""
if len(sys.argv) < 2:
print("Использование: python3 pdf_smart_split.py <input.pdf> [max_size_mb]")
print("Пример: python3 pdf_smart_split.py document.pdf 10")
sys.exit(1)
input_file = sys.argv[1]
max_size_mb = int(sys.argv[2]) if len(sys.argv) > 2 else 10
if not os.path.exists(input_file):
print(f"Ошибка: файл не найден: {input_file}")
sys.exit(1)
try:
splitter = PDFSplitter(input_file, max_size_mb)
results = splitter.split()
print(f"\nГотово! Создано файлов: {len(results)}")
except Exception as e:
print(f"\nОшибка: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()