Image: AI generated
Одной команды
pip install markitdownдостаточно, чтобы превратить PDF, документы Word, таблицы Excel и презентации PowerPoint в markdown. Но дайте ему PDF, состоящий только из отсканированных изображений — скажем, отсканированный договор — и на выходе получится пустой документ. Эта статья идёт по двум линиям: зачем и как использовать MarkItDown, и как подключить OCR именно там, где это доверие рушится — в PDF, состоящих только из изображений.
Зачем использовать MarkItDown — условие agent-operable
MarkItDown — это open-source библиотека Microsoft для CLI/Python, которая конвертирует файлы PDF, Word, Excel, PowerPoint, HTML, изображения и аудио в markdown.
Начнём с понятия agent-operable (состояние, с которым может работать агент). Чтобы агент мог автономно обрабатывать документ, этот документ должен находиться в состоянии, которое машина может структурно разобрать — а не просто в состоянии, которое человек считает читаемым. Это условие применимо не только к коду. Бизнес-правило, погребённое внутри PDF или таблицы Excel, для агента практически не существует, поскольку человеку нужно открыть его, чтобы узнать содержимое. Это первое из трёх условий — читаемость, проверяемость, устойчивость — изложенных в статье «Построение agent-operable систем»: «читаемость без шума».
Так почему же именно MarkItDown? Такую же конвертацию можно построить самостоятельно, собрав сторонние парсеры (PyPDF2, python-docx). Разница в том, кто это построил. docx, xlsx и pptx — всё это OOXML, спецификация, которой владеет сама Microsoft. Когда компания-владелец спецификации сама создаёт конвертер для своего формата, крайние случаи (объединённые ячейки, вложенные таблицы, сноски) структурно с большей вероятностью работают корректно, чем со сторонним парсером. Если сама конвертация ненадёжна, всё, что строится поверх неё в направлении agent-operable состояния, теряет смысл. Именно в этом причина использовать MarkItDown — для собственных форматов Microsoft это доверие обеспечено по умолчанию.
Проблема в том, что это доверие не переносится на PDF, особенно на PDF, состоящие только из изображений. Вторая половина статьи посвящена закрытию этого пробела с помощью OCR.
Установка
Базовая установка
pip install markitdown
Только базовая установка обрабатывает лишь лёгкие форматы вроде обычного текста и HTML. Чтобы конвертировать PDF и документы Office, нужны дополнительные пакеты для конкретных форматов.
Дополнительные пакеты для конкретных форматов
pip install 'markitdown[pdf]' # PDF (pdfminer.six + pdfplumber)
pip install 'markitdown[docx]' # Word
pip install 'markitdown[pptx]' # PowerPoint
pip install 'markitdown[xlsx]' # Excel
pip install 'markitdown[all]' # everything above, plus audio transcription, YouTube captions, Azure Document Intelligence, etc.
Если для этой статьи нужен только PDF, достаточно markitdown[pdf].
Базовое использование
CLI
markitdown example.pdf # print markdown to stdout
markitdown example.pdf -o example.md # save to a file
cat example.pdf | markitdown # read from stdin (pipe)
Python API
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("example.pdf")
print(result.markdown)
Оба способа автоматически выбирают конвертер на основе расширения файла или MIME-типа. Когда информации о расширении нет — например, при чтении из stdin — можно передать подсказку через -x (--extension) или -m (--mime-type).
Что на самом деле делает конвертация PDF — читает только текстовый слой
Конвертер PDF в MarkItDown (packages/markitdown/src/markitdown/converters/_pdf_converter.py) сначала использует pdfplumber для обнаружения структуры таблиц/форм на каждой странице, а если страница не является таблицей — переходит на page.extract_text(). Если pdfplumber не срабатывает, происходит откат на pdfminer.six.
Обе библиотеки — это инструменты, читающие текстовый слой, уже существующий внутри PDF. Если страница состоит только из отсканированного изображения без текстового слоя, extract_text() возвращает пустую строку, и вывод MarkItDown фактически оказывается пустым. В базовом пакете вообще нет кода, который рендерил бы изображение или распознавал в нём символы.
Иными словами, это две разные проблемы:
- PDF с текстовым слоем (экспортированный из Word, большинство электронных договоров) → базовая установка работает хорошо. То же доверие, которое обеспечивает собственные форматы Microsoft, распространяется и сюда.
- PDF без текстового слоя (документ, пропущенный через сканер, фото, встроенное как изображение) → базовая установка возвращает пустой результат. Именно здесь обрывается условие «читаемости» из agent-operable.
Настройка OCR — где восстанавливается разорванное доверие
Именно отсюда начинается вторая линия статьи. Доверие, которое MarkItDown обеспечивает для собственных форматов Microsoft, для PDF из одних изображений нужно выстраивать отдельно.
1. Установите плагин
Поддержка OCR находится в отдельном пакете markitdown-ocr. Он лежит в том же репозитории (microsoft/markitdown), но не включён в pip install markitdown.
pip install markitdown-ocr
pip install openai # an OpenAI-compatible client — this plugin has no OCR model of its own
После установки убедитесь, что плагин распознан.
markitdown --list-plugins
# * ocr (package: markitdown_ocr)
2. Что обязательно нужно — клиент LLM Vision
markitdown-ocr не содержит встроенного традиционного OCR-движка вроде Tesseract или PaddleOCR. Всё, что он реально делает — кодирует изображение в base64 и просит совместимый с OpenAI API chat.completions (например, gpt-4o) «извлечь текст из этого изображения» (LLMVisionOCRService в _ocr_service.py). Поэтому ключ API, способный вызывать модель зрения, — обязательное требование, без которого OCR вообще не работает.
from markitdown import MarkItDown
from openai import OpenAI
md = MarkItDown(
enable_plugins=True,
llm_client=OpenAI(), # reads OPENAI_API_KEY from the environment
llm_model="gpt-4o",
)
result = md.convert("scanned_contract.pdf")
print(result.markdown)
Любой совместимый с OpenAI клиент — включая Azure OpenAI — можно передать точно так же.
from openai import AzureOpenAI
md = MarkItDown(
enable_plugins=True,
llm_client=AzureOpenAI(
api_key="...",
azure_endpoint="https://<resource>.openai.azure.com/",
api_version="2024-02-01",
),
llm_model="gpt-4o",
)
3. Через CLI это не работает — проверено по реальному коду
В README markitdown-ocr приведена такая команда в качестве примера использования:
markitdown document.pdf --use-plugins --llm-client openai --llm-model gpt-4o
Но прямая проверка определений аргументов CLI ядра MarkItDown (packages/markitdown/src/markitdown/__main__.py) показывает, что таких флагов не существует — нет ни --llm-client, ни --llm-model. CLI поддерживает -o (выходной файл), -x (подсказка расширения), -m (подсказка MIME), -c (кодировка), -d/--use-docintel, --use-cu, -p/--use-plugins, --list-plugins и --keep-data-uris, и это всё — конструктор MarkItDown получает только enable_plugins=args.use_plugins. Иными словами, пример CLI в README — это ошибка в документации; он реально не работает.
OCR работает только через Python API — llm_client/llm_model нужно передавать напрямую. Передать API-ключ через CLI невозможно.
4. Как это работает на самом деле — OCR встроенных изображений и запасной вариант для целой страницы
PdfConverterWithOCR (_pdf_converter_with_ocr.py) работает в два этапа.
- Сначала встроенные изображения: когда страница смешивает текст с картинками (скажем, отсканированная подпись посреди основного текста), только изображение вырезается, отправляется в LLM Vision, а результат встраивается обратно вместе с окружающим текстом по исходной Y-позиции, сохраняя порядок чтения.
- Запасной вариант OCR для целой страницы: если извлечение текста возвращает полную пустоту (полностью отсканированный PDF), вся страница рендерится как PNG с разрешением 300dpi и отправляется в LLM Vision по одной странице за раз (
_ocr_full_pages).
Извлечённый текст всегда оборачивается в следующий формат:
*[Image OCR]
<extracted text>
[End OCR]*
5. Настройка точности с помощью пользовательского промпта
Промпт по умолчанию — «извлеки весь текст с этого изображения, сохраняя исходный макет и порядок». Для документов с большим количеством таблиц, вертикальным текстом или печатями и подписями переопределение через llm_prompt может повысить точность.
md = MarkItDown(
enable_plugins=True,
llm_client=OpenAI(),
llm_model="gpt-4o",
llm_prompt="Extract all text from this image, but preserve table structure as markdown tables.",
)
6. Пакетная обработка нескольких файлов
При прогоне сразу целой папки отсканированных PDF каждый файл вызывает собственный API-запрос — каждую конвертацию нужно оборачивать так, чтобы одна ошибка не остановила всю пачку.
from pathlib import Path
from markitdown import MarkItDown
from openai import OpenAI
md = MarkItDown(enable_plugins=True, llm_client=OpenAI(), llm_model="gpt-4o")
for pdf_path in Path("scans/").glob("*.pdf"):
try:
result = md.convert(str(pdf_path))
pdf_path.with_suffix(".md").write_text(result.markdown, encoding="utf-8")
except Exception as e:
print(f"failed: {pdf_path.name} — {e}")
7. Что если установить плагин без llm_client?
Плагин загружается, но OCR тихо отключается. register_converters() в _plugin.py создаёт LLMVisionOCRService только тогда, когда одновременно присутствуют llm_client и llm_model; в противном случае конвертеры регистрируются с ocr_service=None. В этом состоянии система автоматически и молча откатывается к извлечению только текстового слоя — без ошибки, без предупреждения. Большинство проблем вида «плагин установлен, но OCR не работает» упираются именно в эту точку.
8. Даже пытается восстановить повреждённые PDF
Когда pdfplumber и pdfminer вообще не могут открыть PDF — например, при обрезанном EOF — markitdown-ocr переходит к прямому рендерингу страниц через PyMuPDF (fitz) и повторяет попытку. Если и этот путь не срабатывает, в результате остаётся *[Error: Could not process scanned PDF]*.
9. Стоимость и скорость
Поскольку в модель зрения отправляются целые страницы, стоимость и задержка накапливаются пропорционально числу страниц отсканированного документа. Рендеринг при 300dpi обходится не дёшево по токенам на изображение, поэтому безопаснее сначала прогнать выборку из нескольких страниц, чтобы оценить качество и стоимость, прежде чем запускать весь документ на сотни страниц целиком.
Проверка результата OCR — отдельно, позже
Всё вышесказанное — про то, как «включить» OCR. Включить его — не то же самое, что доверять результату: при нынешнем устройстве, когда всё проходит молча вне зависимости от того, задан ли llm_client, и вне зависимости от того, пуста ли страница, детерминированной процедуры для оценки того, действительно ли результат OCR заслуживает доверия, не существует.
Эта процедура здесь не рассматривается. Как применить принцип Symbolic Feedback Loop — LLM генерирует → детерминированный инструмент оценивает → обратная связь → повтор — к результату OCR, будет темой отдельной статьи/инструмента.
Практический чек-лист
- Если вы работаете только с обычными PDF с текстовым слоем, базовой установки
markitdown[pdf]достаточно. Плагин OCR не требуется. - Если приходится обрабатывать сканы или фотографии PDF, обязателен
markitdown-ocrплюс ключ API OpenAI (или совместимого сервиса). Интеграции с бесплатным локальным традиционным OCR-движком вроде Tesseract нет. - Если результат OCR возвращается пустым, первым делом стоит проверить, действительно ли были переданы
llm_client/llm_model. Отказ происходит молча, так что в логах ничего не появится. - Включить OCR только через CLI нельзя. Нужен Python-скрипт, вызывающий
MarkItDown(enable_plugins=True, llm_client=..., llm_model=...). - Механического способа оценить надёжность результата пока не существует — сейчас человеку нужно вручную проверять выборку.
Сводная таблица
| Ситуация | Что нужно | Примечания |
|---|---|---|
| PDF с текстовым слоем | pip install markitdown[pdf] | pdfplumber/pdfminer извлекают текст |
| Некоторым встроенным изображениям нужен OCR | markitdown-ocr + клиент LLM Vision | вырезает только изображения, встраивает вместе с текстом |
| Полностью отсканирован (вся страница — изображение) | markitdown-ocr + клиент LLM Vision | рендерит всю страницу при 300dpi, затем применяет OCR |
| Повреждённый PDF | то же самое (автоматический откат) | повторяет попытку через рендеринг PyMuPDF |
llm_client не задан | — | OCR молча пропускается, без ошибки |
Передача --llm-client в CLI | невозможно (такого флага не существует) | только через Python API |
| Оценка надёжности результата OCR | (запланировано) | будет рассмотрено отдельным verifier |
Связанные статьи
- «Построение agent-operable систем» — три условия (читаемость, проверяемость, устойчивость), и место MarkItDown среди них
- Agent Operable Codebase — тот же принцип, применённый к коду
Источники
- microsoft/markitdown — корень репозитория
_pdf_converter.py— базовый конвертер PDF (только извлечение текстового слоя)__main__.py— определения аргументов CLI (подтверждает отсутствие--llm-client/--llm-model)markitdown-ocrREADME_pdf_converter_with_ocr.py_ocr_service.py_plugin.py- markitdown pyproject.toml — список дополнительных пакетов
История изменений
- 2026-07-09: Первая версия — добавлена рамка agent-operable, настройка OCR расширена как вторая половина статьи, добавлен раздел-заглушка для verifier