fix(indexer): понятная ошибка, если загруженный "PDF" на самом деле не PDF
Живой репорт: проверка плагиата упала с "Не удалось открыть 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
41
services/worker-indexer/tests/test_extractors_pdf.py
Normal file
41
services/worker-indexer/tests/test_extractors_pdf.py
Normal file
@@ -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"<!DOCTYPE html><html><body>not a pdf</body></html>"
|
||||
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)
|
||||
Reference in New Issue
Block a user