# 2026-08-20 — Ревью Sonnet: вопросы и ответы (Q&A) Серия: ревью скорости обработки + объяснение сбоев. Предыдущие файлы: - History/2026-08-20-sonnet-query-review-speed.md — исходный промпт ревью - History/2026-08-20-sonnet-query-review-speed-followup.md — вопросы по спорным пунктам - Настоящий файл — ответы Sonnet + решения --- ## В1. Фильтр `page.lines or page.curves` перед `extract_tables()` (extractor.py) ### В1.1 — Какие таблицы extract_tables() найдёт, но фильтр отбросит? **Sonnet: допущение (не факт).** `extract_tables()` со стратегией по умолчанию ищет только векторные линии (strategy="lines"). - Таблицы на цветном фоне (только `page.rects`, без линий) — фильтр `lines or curves` их пропустит → **дыра**. Нужно `or page.rects`. - Whitespace-таблицы (только пробелы/выравнивание) — не найдёт вообще без strategy="text". - Растровые таблицы в embedded images — не найдёт вообще. **Решение:** фильтр = `page.lines or page.curves or page.rects`. Обязательно проверить фактом (В1.2). ### В1.2 — Как проверить, что фильтр не потеряет таблицы? **Sonnet:** ```python with pdfplumber.open(fname) as pdf: for i, page in enumerate(pdf.pages): t_all = page.extract_tables() has_lines = bool(page.lines or page.curves or page.rects) t_filtered = page.extract_tables() if has_lines else [] if len(t_all) != len(t_filtered): print(f"p{i}: потеряно {len(t_all)-len(t_filtered)} таблиц") ``` Прогнать на TMP-спецификациях и 0144-03-2023_отчет об оценке.pdf. **Решение:** сделать замер до включения фильтра в код. ТАБЛИЦЫ КРИТИЧНЫ (прецедент PyMuPDF 40 vs 94). ### В1.3 — Насколько page.lines/curves дешевле extract_tables()? **Sonnet: допущение.** `page.lines` — `@cached_property` (уже разобран при первом обращении к странице). Стоимость ≈ 0. Дорогой в `extract_tables()` — `TableFinder`: строит граф пересечений линий. На скан-PDF всё равно инициализируется. Замер: ```python t0 = time.perf_counter(); _ = page.lines; t1 = time.perf_counter() t2 = time.perf_counter(); _ = page.extract_tables(); t3 = time.perf_counter() print(f"lines={t1-t0:.4f}s extract_tables={t3-t2:.4f}s") ``` ### В1.4 — find_tables() как guard? **Sonnet:** `find_tables()` и `extract_tables()` — одна стоимость (`extract_tables()` вызывает `finder.find_tables()` внутри). Guard бесполезен. --- ## В2. `_decode_name`: попытка decode("utf-8") перед decode("cp866") ### В2.1 — Риск ложного срабатывания UTF-8 на CP866-байтах? **Sonnet:** CP866 кириллические заглавные = 0xC0–0xDF. В UTF-8 0xC0, 0xC1 — overlong (невалидны), 0xC2–0xDF — валидное начало двухбайта, требует продолжения 0x80–0xBF. CP866 строчные = 0xA0–0xBF — попадают в диапазон UTF-8 continuation bytes. **Реальная коллизия:** «Т» (0xD2) + «г» (0xA3) = 0xD2 0xA3 = валидный UTF-8 (U+04A3 Ң). Риск для имён «Тг…», «Рп…» и т.п. **Защита:** после `decode("utf-8")` проверить, что все символы — в U+0400–U+04FF (кириллица) или ASCII. Если есть символы вне диапазона — ложное срабатывание → фоллбэк cp866. ### В2.2 — Подтверждение порядка **Sonnet: факт.** - Info-ZIP UTF-8 без флага: zipfile(cp437) → `encode("cp437")` → исходные UTF-8 байты → `decode("utf-8")` → корректно. - CP866 1С: zipfile(cp437) → `encode("cp437")` → CP866 байты → `decode("utf-8")` → почти всегда UnicodeDecodeError (0xC0/0xC1 overlong, 0xE0–0xFF без продолжения) → фоллбэк `decode("cp866")` → корректно. **Решение:** фикс принимается, но с валидацией диапазона кириллицы/ASCII после utf-8. --- ## В3. ProcessPoolExecutor по страницам/файлам ### В3.1 — Риск памяти при fork в managed-поде **Sonnet: допущение.** fork → COW. pdfplumber в дочернем создаёт новые объекты (запись → COW). 500МБ session files в родителе — COW, читаются, не копируются. Пик: pdfplumber на 14.8МБ PDF ~150–300МБ на воркер. 2 воркера + родитель ≈ 1–1.5ГБ пик. При 4Gi — умеренный риск. `spawn` (не fork): дочерние стартуют чисто, память сессии не копируется, overhead ~0.5с/процесс. ### В3.2 — Безопасный план распараллеливания **Sonnet:** - Единица параллелизма: **файл** (не страница), воркер получает `(fname, content_bytes)` через pickle. - content_bytes по pipe: 15МБ ≈ 0.1с overhead. - Порядок: `{i: executor.submit(pdf_to_markdown, content)}`, collect `{i: future.result()}`. - progress_cb из основного потока после future.result(). ### В3.3 — Выигрыш на CPU=2 **Sonnet:** на 2 ядрах при ОДНОМ PDF — нет выигрыша (один процесс на одно ядро). Выигрыш только при нескольких PDF: 3 файла × 25с → ~30с вместо 75с. **Рекомендация Sonnet:** сначала В1 (фильтр) + В2 (decode) — бесплатно и безопасно. ProcessPool — потом, если замер покажет, что узкое место реально там. **Решение:** ОТЛОЖЕНО. Сначала В1+В2, замерить. --- ## В4. Разброс 70–470с на Spartan10Manual.pdf (14.8 МБ) ### В4.1 — Как измерить, что дороже: extract_text или extract_tables **Sonnet:** ```python with pdfplumber.open(io.BytesIO(content)) as pdf: for i, page in enumerate(pdf.pages): t0 = time.perf_counter() page.extract_text() t1 = time.perf_counter() page.extract_tables() t2 = time.perf_counter() print(f"p{i:3d}: text={t1-t0:.3f}s tables={t2-t1:.3f}s") ``` ### В4.2 — Причина разброса **Sonnet:** - Факт: pdfplumber не кэширует между запросами — каждый вызов парсит заново. - Допущение: CPU throttling при limit=2 в managed-поде (нагрузка → 470с, пусто → 70с). Проверить `kubectl top pod` во время обработки. - Допущение: битмапы скана разного размера (цветной vs ч/б) → разное время декомпрессии (JPEG2000/JBIG2). Проверить замером по страницам. ### В4.3 — Скрытый повторный парсинг в extract_text? **Sonnet: факт.** `extract_text()` читает `self.chars` (@cached_property), `extract_tables()` — `self.edges` (@cached_property). Оба используют уже разобранные структуры. Повторного чтения PDF в рамках одного `with pdfplumber.open()` нет. --- ## Итоговый план (по приоритету) 1. **В2** — `_decode_name`: utf-8 + валидация диапазона (кириллица U+0400–U+04FF/ASCII) → фоллбэк cp866. Фикс Info-ZIP-зипов (кракозябры). 2. **В1** — фильтр `page.lines or page.curves or page.rects` перед `extract_tables()` + замер числа таблиц до/после на реальных файлах. 3. Замер ускорения на Spartan10Manual.pdf. 4. ProcessPool — после фактов, если узкое место там (отложено). 5. Диагностика разброса 70–470с: `kubectl top pod` во время обработки + замер по страницам. ## Статус реализации - Пока НЕ реализовано (ждёт «делай»). Код не менялся в рамках этого Q&A.