v0.0.55: A1 threading для .doc (liberta), A2 кэш compiled regex — безопасное ускорение
Deploy drhider / validate (push) Canceled after 0s

This commit is contained in:
“Naeel”
2026-08-20 09:20:30 +03:00
parent 59a1924bd0
commit dfa325a5c5
5 changed files with 139 additions and 18 deletions
+25 -3
View File
@@ -11,6 +11,7 @@ import io
import os
import time
import logging
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, List, Tuple, Callable, Optional
from . import extractor
@@ -78,6 +79,7 @@ class TwoPassObfuscator:
"""
self._mapping: Dict[str, str] = {}
self._sorted_keys: List[str] = []
self._compiled_re = None # кэш скомпилированного regex (A2)
self._counters: Dict[str, int] = {} # Глобальные счётчики токенов
self._llm_client = llm_client
@@ -116,6 +118,20 @@ class TwoPassObfuscator:
file_times: List[float] = [0.0] * total
skipped: set = set() # имена файлов, которые не удалось обработать (битые)
# A1: .doc конвертируются через HTTP-сервис liberta (IO-bound).
# Запускаем их в потоках — GIL не мешает (ожидание сети), а основное
# время (PDF/docx, CPU-bound) перекрывается с HTTP-конвертацией .doc.
doc_futures: Dict[int, object] = {}
doc_indices = [i for i, (f, _, _) in enumerate(files)
if f.lower().endswith('.doc')]
if doc_indices:
with ThreadPoolExecutor(max_workers=min(4, len(doc_indices))) as _ex:
for i in doc_indices:
fname, content, ctype = files[i]
doc_futures[i] = _ex.submit(
extractor.extract_text, fname, content, ctype
)
for i, (fname, content, ctype) in enumerate(files):
display_name = os.path.basename(fname) or fname
if progress_cb:
@@ -123,7 +139,10 @@ class TwoPassObfuscator:
t0 = time.time()
try:
text = extractor.extract_text(fname, content, ctype)
if i in doc_futures:
text = doc_futures[i].result()
else:
text = extractor.extract_text(fname, content, ctype)
except Exception as e:
log.warning("obfuscate: skip file %r: %r", fname, e)
skipped.add(fname)
@@ -145,6 +164,8 @@ class TwoPassObfuscator:
self._sorted_keys = sorted(
self._mapping.keys(), key=len, reverse=True
)
# A2: кэш скомпилированного regex — один на все файлы прохода 2
self._compiled_re = replacer._build_combined_re(self._sorted_keys)
# ── Проход 2: замена сущностей ──
results: List[Tuple[str, bytes]] = []
@@ -166,7 +187,7 @@ class TwoPassObfuscator:
# .doc — конвертируется через liberta, обрабатывается как .docx
md_text = all_texts.get(fname, '')
replaced = replacer.apply_replacements(
md_text, self._mapping, self._sorted_keys
md_text, self._mapping, self._sorted_keys, self._compiled_re
)
md_name = os.path.splitext(fname)[0] + '.md'
obf_content = replaced.encode('utf-8')
@@ -175,7 +196,7 @@ class TwoPassObfuscator:
# Все форматы → Markdown → замена в тексте
md_text = all_texts.get(fname, '')
replaced = replacer.apply_replacements(
md_text, self._mapping, self._sorted_keys
md_text, self._mapping, self._sorted_keys, self._compiled_re
)
# Имя файла: заменить расширение на .md
md_name = os.path.splitext(fname)[0] + '.md'
@@ -197,6 +218,7 @@ class TwoPassObfuscator:
# Очистка состояния (обфускатор может использоваться повторно)
self._mapping.clear()
self._sorted_keys.clear()
self._compiled_re = None
self._counters.clear()
+27 -14
View File
@@ -35,7 +35,29 @@ def _md_tolerant_pattern(original: str) -> str:
return MD + MD.join(re.escape(p) for p in parts) + MD
def apply_replacements(text: str, mapping: Dict[str, str], sorted_keys: List[str]) -> str:
def _build_combined_re(sorted_keys: List[str]):
"""Построить единый regex из всех ключей (убывание длины, границы слова).
Возвращает None, если нет ни одного применимого ключа.
"""
alternatives = []
for original in sorted_keys:
if not original:
continue
esc = re.escape(original)
# Границы слова — только если сущность обрамлена буквами/цифрами
if original[0].isalnum() and original[-1].isalnum():
alternatives.append(r'(?<!\w)' + esc + r'(?!\w)')
else:
alternatives.append(esc)
if not alternatives:
return None
return re.compile('|'.join(alternatives))
def apply_replacements(text: str, mapping: Dict[str, str], sorted_keys: List[str],
compiled_re=None) -> str:
"""Применить все замены из словаря mapping к строке.
Ключи применяются в порядке убывания длины (sorted_keys).
@@ -49,6 +71,8 @@ def apply_replacements(text: str, mapping: Dict[str, str], sorted_keys: List[str
text: Исходный текст
mapping: Словарь {оригинал: замена}
sorted_keys: Ключи mapping, отсортированные по длине (убывание)
compiled_re: Опционально, заранее скомпилированный regex из _build_combined_re
(кэш между файлами). Если None — компилируется здесь.
Returns:
Текст с заменами
@@ -61,21 +85,10 @@ def apply_replacements(text: str, mapping: Dict[str, str], sorted_keys: List[str
if not sorted_keys:
return result
alternatives = []
for original in sorted_keys:
if not original:
continue
esc = re.escape(original)
# Границы слова — только если сущность обрамлена буквами/цифрами
if original[0].isalnum() and original[-1].isalnum():
alternatives.append(r'(?<!\w)' + esc + r'(?!\w)')
else:
alternatives.append(esc)
if not alternatives:
combined = compiled_re if compiled_re is not None else _build_combined_re(sorted_keys)
if combined is None:
return result
combined = re.compile('|'.join(alternatives))
matched_keys = set()
def _replace(match):