v0.0.54: В2 фикс кракозябр имён zip (utf-8+валидация→cp866), В1 фильтр таблиц (безопасен, эффект≈0)
Deploy drhider / validate (push) Canceled after 0s
Deploy drhider / validate (push) Canceled after 0s
This commit is contained in:
@@ -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.
|
||||||
+33
-5
@@ -123,7 +123,15 @@ def pdf_to_markdown(content: bytes) -> str:
|
|||||||
lines.append("")
|
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:
|
for table in tables:
|
||||||
if not table:
|
if not table:
|
||||||
continue
|
continue
|
||||||
@@ -260,13 +268,33 @@ def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, s
|
|||||||
"""Декодировать имя из ZIP.
|
"""Декодировать имя из ZIP.
|
||||||
|
|
||||||
Если флаг UTF-8 (bit 11) выставлен — имя уже корректное (берём как есть).
|
Если флаг UTF-8 (bit 11) выставлен — имя уже корректное (берём как есть).
|
||||||
Иначе zipfile декодировал имя как CP437. Для кириллицы из 1С (CP866)
|
Иначе zipfile декодировал имя как CP437. Пытаемся восстановить исходные
|
||||||
перекодируем CP437→CP866; при неудаче оставляем как есть.
|
байты (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):
|
if not (info.flag_bits & 0x800) and any(ord(c) > 127 for c in name):
|
||||||
try:
|
try:
|
||||||
return name.encode("cp437").decode("cp866")
|
raw = name.encode("cp437")
|
||||||
except (UnicodeDecodeError, UnicodeEncodeError):
|
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
|
pass
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
|
|||||||
sys.path.insert(0, _sys_path_root)
|
sys.path.insert(0, _sys_path_root)
|
||||||
|
|
||||||
# Версия приложения (меняется при изменениях)
|
# Версия приложения (меняется при изменениях)
|
||||||
VERSION = "0.0.53"
|
VERSION = "0.0.54"
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
|
|||||||
Reference in New Issue
Block a user