From 59a1924bd05ab016d6c236e200155118122bee30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 20 Aug 2026 09:10:16 +0300 Subject: [PATCH] =?UTF-8?q?v0.0.54:=20=D0=922=20=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=20=D0=BA=D1=80=D0=B0=D0=BA=D0=BE=D0=B7=D1=8F=D0=B1=D1=80=20?= =?UTF-8?q?=D0=B8=D0=BC=D1=91=D0=BD=20zip=20(utf-8+=D0=B2=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D1=8F=E2=86=92cp866),=20=D0=921?= =?UTF-8?q?=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D1=86=20(=D0=B1=D0=B5=D0=B7=D0=BE=D0=BF=D0=B0?= =?UTF-8?q?=D1=81=D0=B5=D0=BD,=20=D1=8D=D1=84=D1=84=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=E2=89=880)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- History/2026-08-20-v1-v2-implemented.md | 37 ++++++++++++++++++++++++ drhider/extractor.py | 38 +++++++++++++++++++++---- site/app.py | 2 +- 3 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 History/2026-08-20-v1-v2-implemented.md diff --git a/History/2026-08-20-v1-v2-implemented.md b/History/2026-08-20-v1-v2-implemented.md new file mode 100644 index 0000000..63e5b69 --- /dev/null +++ b/History/2026-08-20-v1-v2-implemented.md @@ -0,0 +1,37 @@ +# 2026-08-20 — В1+В2 из ревью Sonnet (v0.0.54) + +## В2 — фикс кракозябр имён zip (extractor.py `_decode_name`) +Добавлена попытка decode("utf-8") ПЕРЕД decode("cp866"), с валидацией диапазона: +- результат UTF-8 принимается, только если все символы — ASCII или кириллица (U+0400–U+04FF) + (отсекает случайную коллизию CP866→UTF-8, напр. «Т»+«г» = U+04A3); +- при ошибке или выходе из диапазона — фоллбэк decode("cp866") (реальные 1С). + +Проверено: +- CP866-зип (zip_subfolders_cp866.zip) → имена корректны (1_Металлургия.pdf…) +- UTF-8 с флагом (zip_subfolders_utf8.zip) → корректны +- test_zip 28/28 OK + +## В1 — фильтр перед extract_tables() (extractor.py `pdf_to_markdown`) +Добавлен: `if page.lines or page.curves or page.rects:` перед `extract_tables()`. +Задумывалось как ускорение сканов (extract_tables на страницах без линий впустую). + +### ⚠️ ФАКТ замера (опровергает гипотезу Sonnet) +На Spartan10Manual.pdf (14.8 МБ, 619 стр): +- extract_text суммарно 64.4с (104мс/стр) — УЗКОЕ МЕСТО +- extract_tables суммарно 0.2с (0мс/стр) — ничтожно +- на 272 страницах без линий extract_tables суммарно 0.0с — почти мгновенно + +ВЫВОД: фильтр В1 НЕ даёт ускорения (extract_tables и так дешёвый). Реальное узкое +место — extract_text (pdfminer). Фильтр оставлен как безопасная защита (таблицы +не теряет: на 0144-03-2023_отчет об оценке.pdf 94=94 таблицы, потеряно страниц 0), +но эффект ускорения ≈ 0. + +### Реальный путь ускорения (отложено) +Узкое место — extract_text (104мс/стр × 619 стр = 64с). Ускорение только через: +- распараллеливание extract_text по страницам/файлам (ProcessPool) — отложено, + требует замера рисков (fork/память, CPU=2); +- или смену извлечения текста без потери таблиц (риск: таблицы критичны). + +## Тесты +Все 106 тестов OK (test_zip 28, test_extractor 15, test_builder 20, test_replacer 10, +test_scanner 20, test_upload 13). VERSION 0.0.54. diff --git a/drhider/extractor.py b/drhider/extractor.py index ee8d396..fe2402d 100644 --- a/drhider/extractor.py +++ b/drhider/extractor.py @@ -123,7 +123,15 @@ def pdf_to_markdown(content: bytes) -> str: lines.append("") # ── Таблицы ── - tables = page.extract_tables() + # Быстрый фильтр: если на странице нет линий/кривых/прямоугольников, + # таблиц нет (pdfplumber ищет таблицы только по векторным линиям). + # Включая page.rects — таблицы на цветном фоне (заливка). + # Пропускаем extract_tables() на «чистых» страницах (скан/текст) — это + # основная стоимость на сканах, где линий нет. + if page.lines or page.curves or page.rects: + tables = page.extract_tables() + else: + tables = [] for table in tables: if not table: continue @@ -260,13 +268,33 @@ def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, s """Декодировать имя из ZIP. Если флаг UTF-8 (bit 11) выставлен — имя уже корректное (берём как есть). - Иначе zipfile декодировал имя как CP437. Для кириллицы из 1С (CP866) - перекодируем CP437→CP866; при неудаче оставляем как есть. + Иначе zipfile декодировал имя как CP437. Пытаемся восстановить исходные + байты (encode cp437) и декодировать: + 1) как UTF-8 — если результат в кириллице/ASCII (Info-ZIP/Linux-зипы + пишут UTF-8 без флага; случайная коллизия CP866→UTF-8 отсекается + проверкой диапазона); + 2) как CP866 — реальные 1С-выгрузки. """ + def _cyr_ok(s: str) -> bool: + # Все символы — ASCII или кириллица (U+0400–U+04FF) + return all(ord(c) < 128 or 0x0400 <= ord(c) <= 0x04FF for c in s) + if not (info.flag_bits & 0x800) and any(ord(c) > 127 for c in name): try: - return name.encode("cp437").decode("cp866") - except (UnicodeDecodeError, UnicodeEncodeError): + raw = name.encode("cp437") + except UnicodeEncodeError: + return name + # 1) UTF-8 (Info-ZIP/Linux без флага) + try: + dec = raw.decode("utf-8") + if _cyr_ok(dec): + return dec + except UnicodeDecodeError: + pass + # 2) CP866 (1С) + try: + return raw.decode("cp866") + except UnicodeDecodeError: pass return name diff --git a/site/app.py b/site/app.py index e9be552..789f8ff 100644 --- a/site/app.py +++ b/site/app.py @@ -21,7 +21,7 @@ if _sys_path_root not in sys.path: sys.path.insert(0, _sys_path_root) # Версия приложения (меняется при изменениях) -VERSION = "0.0.53" +VERSION = "0.0.54" def setup_logging():