Files
drhider/History/sonnet/2026-08-20-sonnet-review-qa.md
T

128 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 кириллические заглавные = 0xC00xDF. В 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, 0xE00xFF без продолжения) → фоллбэк `decode("cp866")` → корректно.
**Решение:** фикс принимается, но с валидацией диапазона кириллицы/ASCII после utf-8.
---
## В3. ProcessPoolExecutor по страницам/файлам
### В3.1 — Риск памяти при fork в managed-поде
**Sonnet: допущение.**
fork → COW. pdfplumber в дочернем создаёт новые объекты (запись → COW). 500МБ session files в родителе — COW, читаются, не копируются. Пик: pdfplumber на 14.8МБ PDF ~150300МБ на воркер. 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. Разброс 70470с на 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+0400U+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.