From df2dfcc456e031ec712891d5e9c92cfcb89b264a Mon Sep 17 00:00:00 2001 From: jze9 Date: Mon, 24 Aug 2026 18:55:24 +0500 Subject: [PATCH] =?UTF-8?q?fix(indexer):=20=D0=BF=D0=BE=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0,=20?= =?UTF-8?q?=D0=B5=D1=81=D0=BB=D0=B8=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20"PDF"=20=D0=BD=D0=B0=20=D1=81?= =?UTF-8?q?=D0=B0=D0=BC=D0=BE=D0=BC=20=D0=B4=D0=B5=D0=BB=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B5=20PDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Живой репорт: проверка плагиата упала с "Не удалось открыть PDF: code=7: no objects found" — сырая внутренняя ошибка MuPDF. Разобрал файл из MinIO: это HTML-страница (веб-интерфейс роутера D-Link DAP-400P), сохранённая с расширением .pdf — 2049 байт, не PDF вообще. Система корректно отказалась парсить мусор (это не баг пайплайна — реальный битый/не-PDF файл юзера), но сообщение об ошибке было нечитаемым. extract_text_from_pdf теперь проверяет magic-байты (%PDF-) ДО попытки fitz.open() и даёт понятное "Файл повреждён или не является PDF-документом" для этого частого случая; для настоящих PDF с внутренней порчей — прежнее поведение (сырая ошибка MuPDF как detail, для диагностики). 4 юнит-теста (валидный PDF, HTML под видом PDF, пустые байты, битый заголовок PDF). Тестов всего: 122 (было 118). Co-Authored-By: Claude Opus 4.8 --- README.md | 3 +- services/worker-indexer/app/extractors/pdf.py | 6 +++ services/worker-indexer/requirements-test.txt | 1 + .../tests/test_extractors_pdf.py | 41 +++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 services/worker-indexer/tests/test_extractors_pdf.py diff --git a/README.md b/README.md index 15d2edb..1083653 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ docker compose -f docker-compose.prod.yml --profile observability up -d promethe 1. **Линт** — `ruff` (весь Python) + `mypy` (чистая доменная логика). Конфиги: [`ruff.toml`](ruff.toml), [`mypy.ini`](mypy.ini). -2. **Юнит-тесты** — `pytest` по сервисам: 118 тестов на ядро детекции, скоринга, +2. **Юнит-тесты** — `pytest` по сервисам: 122 теста на ядро детекции, скоринга, парсеров, форматирования и OAuth, без внешней инфры (БД/Redis/GPU/Ollama замоканы либо не нужны). @@ -231,6 +231,7 @@ make test-one SVC=worker-gost # тесты одного сервиса | L2 — нечёткие (MinHash LSH) | `worker-indexer/app/algorithms/minhash.py` | 6 | | Разбиение на фрагменты | `worker-indexer/app/fragments.py` | 5 | | Автопополнение корпуса | `worker-indexer/app/staging.py` | 6 | +| Извлечение текста из PDF | `worker-indexer/app/extractors/pdf.py` | 4 | | L3 — семантический индекс (FAISS) | `worker-gpu/app/faiss_manager.py` | 5 | | L3 — векторный бэкенд (Qdrant + выбор) | `worker-gpu/app/qdrant_manager.py`, `vector_store.py` | 9 | | L4 — LLM-парафраз | `worker-gpu/app/ollama_client.py` | 12 | diff --git a/services/worker-indexer/app/extractors/pdf.py b/services/worker-indexer/app/extractors/pdf.py index 2dbbbe3..fa54d58 100644 --- a/services/worker-indexer/app/extractors/pdf.py +++ b/services/worker-indexer/app/extractors/pdf.py @@ -23,6 +23,12 @@ def extract_text_from_pdf(data: bytes) -> str: """ import fitz # PyMuPDF + # Частый случай: файл с расширением .pdf, но внутри — HTML/другое (например, + # сохранённая веб-страница). Даём понятное сообщение, а не сырую ошибку MuPDF + # вида "code=7: no objects found", которая ничего не скажет пользователю. + if not data.startswith(b"%PDF-"): + raise ValueError("Файл повреждён или не является PDF-документом") + try: doc = fitz.open(stream=data, filetype="pdf") except Exception as e: diff --git a/services/worker-indexer/requirements-test.txt b/services/worker-indexer/requirements-test.txt index bdcbe3c..5a21bd3 100644 --- a/services/worker-indexer/requirements-test.txt +++ b/services/worker-indexer/requirements-test.txt @@ -6,3 +6,4 @@ xxhash==3.4.1 datasketch==1.6.5 numpy==1.26.4 pydantic-settings==2.2.1 +PyMuPDF==1.24.0 diff --git a/services/worker-indexer/tests/test_extractors_pdf.py b/services/worker-indexer/tests/test_extractors_pdf.py new file mode 100644 index 0000000..5671f1b --- /dev/null +++ b/services/worker-indexer/tests/test_extractors_pdf.py @@ -0,0 +1,41 @@ +"""Юнит-тесты извлечения текста из PDF (app.extractors.pdf).""" + +import pytest +from app.extractors.pdf import extract_text_from_pdf + + +def _minimal_pdf_bytes(text: str) -> bytes: + """Собрать валидный однострочный PDF с заданным текстом через сам PyMuPDF.""" + import fitz + + doc = fitz.open() + page = doc.new_page() + page.insert_text((72, 72), text) + data = doc.tobytes() + doc.close() + return data + + +def test_extracts_text_from_valid_pdf(): + # ASCII — встроенный шрифт PyMuPDF не рендерит кириллицу без явного указания + # шрифта с поддержкой юникода; для проверки самой логики извлечения не важно. + text = extract_text_from_pdf(_minimal_pdf_bytes("Hello, world")) + assert "Hello, world" in text + + +def test_non_pdf_content_raises_friendly_error(): + # Частый реальный случай: файл с расширением .pdf, а внутри — HTML + html = b"not a pdf" + with pytest.raises(ValueError, match="не является PDF"): + extract_text_from_pdf(html) + + +def test_empty_bytes_raises_friendly_error(): + with pytest.raises(ValueError, match="не является PDF"): + extract_text_from_pdf(b"") + + +def test_truncated_pdf_header_only_raises(): + # Начинается как PDF (проходит magic-byte проверку), но структуры внутри нет + with pytest.raises(ValueError): + extract_text_from_pdf(b"%PDF-1.4\n" + b"garbage" * 20)