v0.0.55: A1 threading для .doc (liberta), A2 кэш compiled regex — безопасное ускорение
Deploy drhider / validate (push) Canceled after 0s
Deploy drhider / validate (push) Canceled after 0s
This commit is contained in:
+25
-3
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user