Improve LLM and CSV processing
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+87
-8
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user