Improve LLM and CSV processing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Repinoid
2026-10-23 10:09:34 +03:00
co-authored by Copilot
parent aa1d15509b
commit 49171c93f9
14 changed files with 315 additions and 92 deletions
+87 -8
View File
@@ -1,10 +1,11 @@
"""
Извлечение текста из документов — все форматы → Markdown.
Извлечение текста из документов и подготовка CSV к обфускации.
Поддерживает:
- .docx → MD (python-docx: стили, жирный/курсив, таблицы)
- .pdf → MD (pdfplumber: текст + таблицы, без форматирования)
- .doc → MD (LibreOffice headless: .doc → .docx → штатный docx_to_markdown)
- .doc → HTTP-сервис конвертации (CONVERT_SERVICE_URL) → .docx → Markdown
- .csv → текст ячеек для сканирования и обезличенный CSV на выходе
- .txt и прочие → как есть
Также содержит:
@@ -12,13 +13,16 @@
"""
import io
import csv
import os
import zipfile
import tempfile
import shutil
import subprocess
import logging
from typing import List, Tuple
from typing import Dict, List, Tuple
from . import replacer
log = logging.getLogger("drhider")
@@ -199,6 +203,77 @@ def doc_to_markdown(content: bytes) -> str:
# Единая точка входа
# ═══════════════════════════════════════════════════════════════════════════
def decode_text(content: bytes) -> str:
"""Декодировать текст как UTF-8, затем CP1251, с Latin-1 последним fallback."""
try:
return content.decode("utf-8")
except UnicodeDecodeError:
# Блокнот и выгрузки 1С часто сохраняют кириллицу в CP1251.
try:
return content.decode("cp1251")
except UnicodeDecodeError:
return content.decode("latin-1", errors="replace")
def detect_delimiter(text: str) -> str:
"""Определить CSV-разделитель, используя Sniffer и детерминированный fallback."""
delimiters = [",", ";", "\t"]
nonempty_lines = [line for line in text.splitlines() if line.strip()]
if nonempty_lines:
sample = "\n".join(nonempty_lines[:10])
try:
return csv.Sniffer().sniff(sample, delimiters="".join(delimiters)).delimiter
except csv.Error:
first_line = nonempty_lines[0]
return max(delimiters, key=first_line.count)
return ","
def csv_to_text(content: bytes) -> str:
"""Распарсить CSV и склеить значения ячеек построчно для сканирования."""
text = decode_text(content).removeprefix("\ufeff")
delimiter = detect_delimiter(text)
reader = csv.reader(
io.StringIO(text, newline=""),
delimiter=delimiter,
quotechar='"',
strict=True,
)
# Каждая ячейка отделена переводом строки, чтобы сущности из соседних ячеек
# не объединялись в строку, по которой работают regex и LLM.
return "\n".join(cell for row in reader for cell in row)
def obfuscate_csv(content: bytes, mapping: Dict[str, str],
sorted_keys: List[str], compiled_re) -> bytes:
"""Заменить сущности в CSV-ячейках, сохранив порядок строк и столбцов."""
text = decode_text(content).removeprefix("\ufeff")
delimiter = detect_delimiter(text)
reader = csv.reader(
io.StringIO(text, newline=""),
delimiter=delimiter,
quotechar='"',
strict=True,
)
output = io.StringIO(newline="")
writer = csv.writer(
output,
delimiter=delimiter,
quotechar='"',
lineterminator="\n",
strict=True,
)
for row in reader:
writer.writerow([
replacer.apply_replacements(cell, mapping, sorted_keys, compiled_re)
for cell in row
])
# csv.writer нормализует кавычки/переводы строк; побайтовое сохранение
# исходного формата не является целью. BOM помогает Excel открыть кириллицу.
return ("\ufeff" + output.getvalue()).encode("utf-8")
def extract_text(fname: str, content: bytes, ctype: str = "") -> str:
"""Извлечь текст из одного файла → Markdown.
@@ -217,22 +292,26 @@ def extract_text(fname: str, content: bytes, ctype: str = "") -> str:
try:
return docx_to_markdown(content)
except ImportError:
return content.decode('utf-8', errors='replace')
return decode_text(content)
# ── .pdf: текст + таблицы ──
elif ext == '.pdf':
try:
return pdf_to_markdown(content)
except ImportError:
return content.decode('utf-8', errors='replace')
return decode_text(content)
# ── .doc: бинарный → LibreOffice → DOCX → Markdown ──
# ── .doc: бинарный → HTTP-сервис конвертации → DOCX → Markdown ──
elif ext == '.doc':
return doc_to_markdown(content)
# ── .csv: ячейки для обнаружения сущностей ──
elif ext == '.csv':
return csv_to_text(content)
# ── .txt и прочие ──
else:
return content.decode('utf-8', errors='replace')
return decode_text(content)
# ═══════════════════════════════════════════════════════════════════════════
@@ -338,7 +417,7 @@ def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, s
continue
# Синхронизация с фронтом (listZipFiles): из zip берём ТОЛЬКО документы
low = name.lower()
if not (low.endswith('.zip') or low.endswith(('.pdf', '.doc', '.docx', '.txt', '.md'))):
if not (low.endswith('.zip') or low.endswith(('.pdf', '.doc', '.docx', '.txt', '.md', '.csv'))):
continue
inner_data = zf.read(info)