diff --git a/Dockerfile b/Dockerfile index fd17096..30915ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM python:3.12-slim WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends \ + libreoffice-writer \ + && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY site /app/site diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fa14ece..0958521 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -45,7 +45,7 @@ site/app.py ──→ Flask (create_app) | **`.pdf`** | → Markdown | Текст + таблицы (`pdfplumber`), без форматирования | | **`.txt`** и прочие текстовые | → как есть | Декодируется UTF-8, ошибки заменяются `�` | | **`.zip`** | → распаковка | Файлы внутри обрабатываются рекурсивно. Защита от ZIP-бомб: ≤500 файлов, ratio ≤100:1, ≤500 MB | -| **`.doc`** (бинарный) | ❌ не поддерживается | Возвращает заглушку `[DOC binary — not supported. Convert to .docx first.]` | +| **`.doc`** (бинарный) | → Docx → Markdown | LibreOffice headless: .doc → .docx → штатный `docx_to_markdown()` (таблицы, форматирование). Требует `libreoffice-writer` в контейнере | Ограничение на размер загрузки: **200 MB** (`MAX_CONTENT_LENGTH`). diff --git a/drhider/extractor.py b/drhider/extractor.py index 3c5b353..2c0d159 100644 --- a/drhider/extractor.py +++ b/drhider/extractor.py @@ -4,7 +4,7 @@ Поддерживает: - .docx → MD (python-docx: стили, жирный/курсив, таблицы) - .pdf → MD (pdfplumber: текст + таблицы, без форматирования) -- .doc (бинарный — не парсится, возвращает заглушку) +- .doc → MD (LibreOffice headless: .doc → .docx → штатный docx_to_markdown) - .txt и прочие → как есть Также содержит: @@ -14,6 +14,9 @@ import io import os import zipfile +import tempfile +import shutil +import subprocess import logging from typing import List, Tuple @@ -145,6 +148,51 @@ def pdf_to_markdown(content: bytes) -> str: return "\n".join(lines) +# ═══════════════════════════════════════════════════════════════════════════ +# DOC → Markdown (через LibreOffice) +# ═══════════════════════════════════════════════════════════════════════════ + +def doc_to_markdown(content: bytes) -> str: + """Конвертировать бинарный .doc → .docx (LibreOffice) → Markdown. + + Использует soffice --headless --convert-to docx, затем прогоняет + через штатный docx_to_markdown() с полным форматированием и таблицами. + + Args: + content: Бинарное содержимое .doc файла + + Returns: + Строка в формате Markdown (или сообщение об ошибке) + """ + tmpdir = tempfile.mkdtemp() + doc_path = os.path.join(tmpdir, "input.doc") + try: + with open(doc_path, 'wb') as f: + f.write(content) + + result = subprocess.run( + ["soffice", "--headless", "--convert-to", "docx", + "--outdir", tmpdir, doc_path], + timeout=60, capture_output=True + ) + + docx_path = os.path.join(tmpdir, "input.docx") + if os.path.exists(docx_path): + with open(docx_path, 'rb') as f: + return docx_to_markdown(f.read()) + + stderr = result.stderr.decode('utf-8', errors='replace').strip() + log.warning("LibreOffice conversion failed: %s", stderr) + return f"[DOC — LibreOffice conversion failed: {stderr}]" + + except FileNotFoundError: + return "[DOC — LibreOffice not installed. Install: apt install libreoffice-writer]" + except subprocess.TimeoutExpired: + return "[DOC — conversion timed out (>60s)]" + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + + # ═══════════════════════════════════════════════════════════════════════════ # Единая точка входа # ═══════════════════════════════════════════════════════════════════════════ @@ -176,9 +224,9 @@ def extract_text(fname: str, content: bytes, ctype: str = "") -> str: except ImportError: return content.decode('utf-8', errors='replace') - # ── .doc: бинарный — пока не поддерживается ── + # ── .doc: бинарный → LibreOffice → DOCX → Markdown ── elif ext == '.doc': - return "[DOC binary — not supported. Convert to .docx first.]" + return doc_to_markdown(content) # ── .txt и прочие ── else: diff --git a/site/app.py b/site/app.py index e0f6ec2..0053bd0 100644 --- a/site/app.py +++ b/site/app.py @@ -20,7 +20,7 @@ if _sys_path_root not in sys.path: sys.path.insert(0, _sys_path_root) # Версия приложения (меняется при изменениях) -VERSION = "0.0.16" +VERSION = "0.0.17" def create_app():