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

8.8 KiB
Raw Blame History

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:

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 всё равно инициализируется. Замер:

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:

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.