Skip to content

Сервис анализа документов docreader ​

Анализ документов преобразует загруженные файлы в доступный для поиска текст и извлекает ссылки на исходные изображения. Платформа поддерживает множество форматов с возможностью выбора движка анализа для каждого типа файлов; для сканированных копий, изображений и аудио также требуются соответствующие визуальные или голосовые модели.

Поддерживаемые форматы:

КатегорияФорматы
ДокументыPDF, Word (doc/docx), PPT (ppt/pptx), Excel (xls/xlsx), EPUB, XMind
Текстtxt, Markdown, CSV, JSON
Веб-страницыЗахват по онлайн-URL, локальные архивы HTML / MHTML
Изображенияjpg, png, gif, bmp, tiff, webp (для понимания содержимого требуется визуальная модель)
Аудиоmp3, wav, m4a, flac, ogg (требуется модель распознавания речи)

Если результат анализа неудовлетворителен, можно внести следующие корректировки:

  • Плохое сохранение макета PDF, смещение таблиц: в настройках анализа базы знаний укажите другой движок для pdf (MarkItDown / OpenDataLoader / MinerU);
  • На скане не распознан текст: убедитесь, что настроена визуальная модель, при необходимости принудительно включите режим сканирования;
  • Первая строка Excel содержит заголовки столбцов, но распознается как данные: включите опцию «Первая строка как заголовок» для xlsx/xls;
  • Требуется исправление содержимого блоков: отредактируйте текст в списке блоков, см. Базы знаний и управление знаниями.

Справочник механизмов анализа и конфигурации ​

docreader — это отдельный gRPC-сервис на Python, отвечающий за преобразование файлов или URL в Markdown и ссылки на исходные изображения. Основной сервис на Go затем выполняет разбиение на блоки, сохранение изображений, OCR, описание изображений и векторизацию.

Разделение обязанностей между сервисом анализа и последующей обработкой определено в docreader/parser/base_parser.py:

python
class BaseParser(ABC):
    """Base parser interface.

    After the lightweight refactoring, BaseParser only extracts markdown text
    and raw image references from documents. Chunking, image storage, OCR,
    and VLM caption are handled by the Go App module.
    """

Позиционирование сервиса и внешние интерфейсы ​

Протокол интерфейса: чистый gRPC (без HTTP) ​

Точкой входа сервиса является docreader/main.py, который запускает только gRPC-сервер (grpc.server + ThreadPoolExecutor), по умолчанию слушающий порт 50051. Также регистрируется стандартный сервис проверки работоспособности gRPC (grpc_health.v1) для проб K8s / Docker (в сочетании с бинарным файлом grpc_health_probe внутри образа). Никаких интерфейсов HTTP нет.

Определение Proto находится в docreader/proto/docreader.proto, всего 3 RPC:

protobuf
service DocReader {
  rpc Read(ReadRequest) returns (ReadResponse) {}
  // Потоковая версия: сначала отправляет 1 кадр meta (markdown/metadata/error), затем по 1 кадру на каждое изображение.
  // Предотвращает превышение лимита размера унарного сообщения (RESOURCE_EXHAUSTED) при обработке больших сканов PDF (сотни страниц).
  rpc ReadStream(ReadRequest) returns (stream ReadStreamResponse) {}
  rpc ListEngines(ListEnginesRequest) returns (ListEnginesResponse) {}
}

ReadRequest — единый запрос: для режима файлов задаются file_content/file_name/file_type, для режима URL — url/title; config.parser_engine задает движок (builtin / markitdown / opendataloader), config.parser_engine_overrides передает параметры переопределения уровня движка (например, pdf_force_scanned, odl_hybrid).

ReadResponse возвращает markdown_content + repeated ImageRef image_refs (изображения возвращаются встроенными байтами inline bytes, image_dir_path всегда пустая строка — сохранение изображений полностью возложено на Go App, прежнее поле 3 image_storage в proto помечено как reserved), а также опциональный repeated SourceBlock source_blocks: сопоставляет фрагмент markdown_content (смещение кодовых точек Unicode start/end) с позицией в исходном файле (locator_json) для точного позиционирования цитат, см. Позиция в исходном документе. ReadStreamMeta содержит те же поля.

Преимущество ReadStream (см. main.py::ReadStream и _iter_image_refs): каждый кадр независим и имеет малый объем; сервер по мере декодирования base64 выполняет images.pop(ref_path) для освобождения исходных данных, обеим сторонам не нужно одновременно держать все изображения в памяти, что решает проблему пикового потребления памяти и размера сообщений для больших сканированных PDF. Клиент на стороне Go internal/infrastructure/docparser/grpc_parser.go отдает приоритет вызову ReadStream, автоматически откатываясь к унарному Read при получении Unimplemented от старых версий docreader.

ListEngines сохранен для обратной совместимости — комментарии явно указывают, что список движков теперь управляется на стороне Go internal/infrastructure/docparser/engine_registry.go (docparser.ListAllEngines), Go App больше не вызывает этот RPC, а удаленные движки вроде MinerU обрабатываются Go нативно.

Аутентификация и TLS (auth.py) ​

docreader/auth.py предоставляет два уровня механизмов безопасности, включаемых через переменные окружения:

Аутентификация по токену (AuthInterceptor): активируется при установке GRPC_AUTH_TOKEN. Клиент должен передать в metadata authorization: Bearer <token> (или токен без префикса). Проверка выполняется через hmac.compare_digest для защиты от атак по времени; при длине токена менее 16 байт выводится предупреждение. Два метода проверки работоспособности (/grpc.health.v1.Health/Check, /Watch) пропускаются без аутентификации, гарантируя корректную работу проб. При сбое аутентификации через _make_abort_handler формируется обработчик прерывания, соответствующий исходному типу RPC (unary/stream), возвращая UNAUTHENTICATED вместо необработанного исключения INTERNAL.

TLS / mTLS (load_tls_credentials): при GRPC_TLS_ENABLED=true обязательны GRPC_TLS_CERT / GRPC_TLS_KEY, опционально GRPC_TLS_CA; GRPC_MTLS_REQUIRE_CLIENT_CERT=true требует сертификат клиента (при отсутствии настройки определяется автоматически по наличию GRPC_TLS_CA). Любая ошибка загрузки или отсутствие конфигурации TLS вызывает исключение TLSConfigError, которое в main() приводит к sys.exit(1) — fail-fast, отказ от тихого отката к открытому тексту.

Клиент со стороны Go находится в docreader/client/auth.go (LoadAuthConfigFromEnv считывает одноименные переменные окружения GRPC_TLS_ENABLED/CERT/KEY/CA/SERVER_NAME и GRPC_AUTH_TOKEN), NewClient в docreader/client/client.go создает подключение с балансировкой нагрузки round_robin и ограничением размера сообщений MAX_FILE_SIZE_MB.

Временная диаграмма взаимодействия с основным сервисом ​

В Go App internal/application/service/knowledge_process.go вызывает анализ на этапе docreader конвейера добавления документа (тайм-аут управляется конфигурацией docreader_call_timeout, чтобы зависший docreader не блокировал worker надолго). Обратите внимание: форматы md/markdown/txt/csv/json/изображения/аудио обрабатываются встроенным SimpleFormatReader на стороне Go и не проходят через docreader (см. simpleFormats в internal/infrastructure/docparser/builtin_converter.go).


Регистрация и диспетчеризация анализаторов ​

Реестр движков (parser/registry.py) ​

ParserEngineRegistry поддерживает двухуровневое сопоставление имя движка → {расширение файла → класс парсера} и позволяет регистрировать пробу check_available для каждого движка (используется ListEngines для отчета о доступности и причинах недоступности).

_build_default_registry() регистрирует три движка:

ДвижокТипы файловОписание
builtindocx(Docx2Parser), doc(DocParser), pdf(PDFParser), md/markdown(MarkdownParser), xlsx/xls(ExcelParser), pptx/ppt(MarkitdownParser), epub(EPUBParser), html/htm(HTMLParser), mhtml(MHTMLParser), xmind(XMindParser), jpg/jpeg/png/gif/bmp/tiff/webp(ImageParser)Встроенный движок анализа; PPT/PPTX используют парсер MarkItDown
markitdownmd, markdown, pdf, docx, doc, pptx, ppt, xlsx, xls, csv (все через MarkitdownParser)Библиотека Microsoft MarkItDown
opendataloaderpdf(OpenDataLoaderParser)Анализ макета PDF OpenDataLoader, требуется Java 11+; check_available проверяет java, пакеты Python и доступность гибридного сервиса

Правило диспетчеризации (get_parser_class): если запрошенный движок не поддерживает данный тип файла, происходит автоматический откат к движку builtin; если и в builtin поддержки нет, выбрасывается ValueError("Unsupported file type").

Фасад и исправление сигнатур файлов (parser/parser.py) ​

Parser является классом-фасадом: parse_file() работает через реестр, parse_url() жестко использует WebParser. Важным защитным механизмом является detect_effective_file_type(): формат OOXML .docx представляет собой ZIP-контейнер, тогда как старый .doc является составным файлом OLE Compound File. Поскольку WPS/Word допускают переименование .doc в .docx, при обнаружении сигнатуры OLE (b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1") в начале файла с расширением "docx" запрос принудительно перенаправляется парсеру DOC, предотвращая передачу бинарных данных OLE в анализатор DOCX.

Параметры переопределения движка engine_overrides (из parser_engine_overrides в proto) передаются как **kwargs в конструктор анализатора, например pdf_force_scanned перехватывается в PDFParser.__init__.

Цепочки анализаторов (parser/chain_parser.py) ​

Два комбинатора «цепочки обязанностей», динамически создающие подклассы через фабрику create(*parser_classes):

  • FirstParser: последовательно пробует несколько анализаторов, возвращает результат первого же, у которого document.is_valid() (т.е. content != ""); при исключении переходит к следующему. Типичный пример: Docx2Parser = FirstParser.create(MarkitdownParser, DocxParser).
  • PipelineParser: конвейер, где выходной текст каждого анализатора (заново закодированный в bytes) становится входом для следующего, а полученные images/metadata накапливаются и объединяются. Типичные примеры: MarkdownParser = PipelineParser.create(MarkdownTableFormatter, MarkdownImageBase64), WebParser = PipelineParser.create(StdWebParser, MarkdownParser), MarkitdownParser = PipelineParser.create(StdMarkitdownParser, MarkdownParser).

Модель конкурентности (parser/concurrency.py и связанные компоненты) ​

Контроль конкурентности разделен на четыре уровня:

  1. Пул потоков gRPC: ThreadPoolExecutor(max_workers=CONFIG.grpc_max_workers) (по умолчанию 4), то есть одновременно обрабатывается не более 4 запросов.
  2. Ограничение именованными семафорами (parser_worker_limit(name, max_workers)): процессный threading.BoundedSemaphore, повторно используемый по имени для ограничения параллелизма тяжелых бэкендов. Текущие точки ограничения: "markitdown" (по умолчанию 1), "opendataloader" (по умолчанию 1, каждый convert запускает JVM), "pdf_render" (по умолчанию 1). При max_workers <= 0 ограничение снято.
  3. Глобальная блокировка pdfium (pdf_parser.py::_PDFIUM_LOCK): библиотека C pdfium является глобальной для процесса и не потокобезопасна. Одновременный анализ PDF двумя рабочими потоками gRPC нарушает общее состояние или приводит к взаимоблокировке всего процесса (наблюдалось постоянное зависание запроса на "Parsing document with PDFParser"). Поэтому все операции pdfium (извлечение текста, рендеринг страниц, извлечение картинок) сериализованы за этой глобальной блокировкой, конкурентные загрузки PDF выстраиваются в очередь; парсеры других форматов не затрагиваются.
  4. Параллелизм на уровне процессов:
  • Рендеринг сканированных страниц PDF: _render_pages_parallel использует ProcessPoolExecutor (предпочтителен метод запуска forkserver, исключающий риски fork в многопоточном процессе), распределяя сканированные страницы одного PDF по нескольким процессам-воркерам (каждый процесс независимо открывает PdfDocument из временного файла) с параллелизмом DOCREADER_PDF_RENDER_PARALLELISM (по умолчанию min(4, cpu)). Это главное средство сокращения времени рендеринга больших сканов на CPU-ограниченных контейнерах; при сбое прозрачно откатывается к последовательному выполнению.
  • Постраничный параллелизм DOCX: Docx в docx_parser.py распределяет задачи страниц в ProcessPoolExecutor + общий список Manager, изображения передаются между процессами через временные файлы /tmp/docx_img_* и окончательно кодируются в основном процессе.
  • Конвертация LibreOffice (doc→docx, ppt→pptx, xls→xlsx): вызов subprocess с soffice --headless, каждая попытка использует отдельный -env:UserInstallation=<временный профиль>, предотвращая тихие сбои из-за конкуренции за блокировку профиля пользователя при одновременных вызовах soffice, с 3 попытками повтора с задержкой.

Подробный разбор анализаторов ​

pdf_parser.py — PDFParser / PDFScannedParser (PDF движка builtin) ​

Зависимости: pypdfium2 (+ Pillow). Не требует внешних сервисов (MinerU / Docling и др.), сам docreader не выполняет OCR.

Ключевой дизайн: постраничная маршрутизация (per-page routing). Каждая страница независимо классифицируется как "text" или "scanned" (_classify_page): главный сигнал — коэффициент покрытия изображениями (площадь ограничивающих рамок объектов image на странице / общая площадь страницы, порог DOCREADER_PDF_SCAN_IMAGE_RATIO=0.5): сканированная страница по сути представляет собой одно большое изображение на весь лист, даже при наличии слоя OCR-текста (зачастую низкого качества); вторичный сигнал — число символов текстового слоя < DOCREADER_PDF_SCAN_MIN_CHARS (10) при наличии некоторого графического содержимого. Этот подход соотносится с архитектурой MinerU / Docling / DeepDoc и предотвращает попадание искаженного текста в контекст RAG.

Процесс обработки (_route_locked, три прохода):

  1. Проход 1: Извлечение текста + Классификация: текстовые страницы обрабатываются по текстовому слою. Если DOCREADER_PDF_LAYOUT_ORDERING=true (по умолчанию) и чистый текст pdfium не является «хорошо сформированным» (_plain_is_well_formed), выполняется геометрическая реконструкция макета: извлечение на уровне глифов (фильтрация скрытого текста render-mode 3 и глифов вне страницы — защита от prompt injection через скрытый текст), рекурсивное разделение на колонки методом XY-cut (линеаризация многоколоночного текста), удаление водяных знаков и боковых колонок (например, боковых колонок arXiv), определение пробелов между словами по расстоянию между символами (WORD_GAP_WIDTH_RATIO), повышение строк с крупным шрифтом до заголовков Markdown по отношению к медианной высоте строки страницы (DETECT_HEADINGS). Если результат реконструкции выглядит фрагментированным (эвристика _should_prefer_plain), происходит откат к чистому тексту. Далее _postprocess_pdf_text производит очистку: удаление заполнителей вроде U+FFFE, строк водяных знаков arXiv, номеров страниц, мусора осей/легенд векторных графиков, попавших в текстовый слой (STRIP_CHART_TEXT_DEBRIS). При обнаружении подписи Figure N на текстовой странице область векторной диаграммы над подписью рендерится в JPEG (RENDER_VECTOR_FIGURES) и вставляется перед подписью в виде ![...](images/...).
  2. Проход 2: Рендеринг сканированных страниц: только scanned страницы рендерятся в JPEG (DPI DOCREADER_PDF_RENDER_DPI=200, качество DOCREADER_PDF_JPEG_QUALITY=85, ограничение длинной стороны DOCREADER_PDF_RENDER_MAX_EDGE=2000 px — защита от формирования изображений на 100+ Мп из PDF с огромными размерами страниц), в markdown вставляется заполнитель ![xxx_page_N.jpg](images/...), в metadata выставляется флаг image_source_type=scanned_pdf, и Go App запускает OCR для этих страниц (компонент image_multimodal.go на стороне Go использует специальный ocr_prompt для источников scanned_pdf).
  3. Проход 3: Извлечение встроенных изображений: из текстовых страниц извлекаются встроенные иллюстрации/диаграммы (EXTRACT_EMBEDDED_IMAGES) с фильтрацией по минимальному размеру (80 px), минимальной доле площади страницы (1%), частоте повторения по страницам (одинаковый MD5 на ≥50% текстовых страниц считается логотипом/водяным знаком и отсеивается) и лимиту на документ (50 изображений), после чего они вставляются в markdown сверху вниз по странице.

Также _strip_repeating_lines аккуратно удаляет повторяющиеся колонтитулы (кандидатами являются только первая и последняя строки страницы, они должны быть короткими и встречаться на ≥60% текстовых страниц).

Любое исключение приводит к откату к PDFScannedParser — запасному анализатору, который рендерит каждую страницу в JPEG (также используется при принудительном режиме сканирования pdf_force_scanned, включаемом через override при загрузке или DOCREADER_PDF_FORCE_SCANNED).

Результат: Markdown (текст страниц text + заполнители изображений), словарь images, метаданные (page_count/scanned_page_count/text_page_count/embedded_image_count/vector_figure_count/image_source_type).

Ограничения: нет распознавания структуры таблиц (таблицы текстового слоя выводятся построчно); распознавание заголовков основано на эвристике размера шрифта; текст сканированных страниц полностью зависит от OCR на стороне Go.

doc_parser.py — DocParser (устаревший Word.doc) ​

Наследует Docx2Parser, цепочка обработки (попытки по порядку):

  1. _parse_with_docx: конвертация DOC в DOCX с помощью LibreOffice (soffice --headless --convert-to docx, отдельный профиль + 3 попытки) с последующим анализом цепочкой родительского класса DOCX (единственный путь, извлекающий изображения);
  2. _parse_with_antiword: извлечение чистого текста утилитой командной строки antiword (выполняется через SandboxExecutor с принудительными переменными прокси, по умолчанию используется «черная дыра» http://128.0.0.1:1, предотвращающая нежелательные внешние соединения);
  3. _parse_with_textract: отключено (в textract есть уязвимость SSRF, код сохранен в закомментированном виде).

Зависимости: LibreOffice (soffice), antiword (установлены в образе); пути поиска настраиваются через переменные LIBREOFFICE_PATH/ANTIWORD_PATH. Для внешних команд установлен тайм-аут по умолчанию 60 секунд; при превышении завершается вся группа процессов (включая дочерние процессы soffice), что исключает зависание рабочих воркеров docreader. Ограничение: при отсутствии LibreOffice происходит деградация до чистого текста antiword (без изображений и структуры таблиц).

Различия docx2_parser.py и docx_parser.py ​

  • Docx2Parser (фактическая точка входа для docx в реестре) содержит всего 3 строки логики: FirstParser.create(MarkitdownParser, DocxParser) — сначала пробует MarkItDown (быстрый, качественная конвертация таблиц в Markdown), а при сбое или пустом выводе откатывается к собственной разработке DocxParser.
  • DocxParser (docx_parser.py, более 1500 строк) — собственный анализатор на базе python-docx:
  • Содержит патч для python-docx load_from_xml_v2 (пропуск поврежденных связей с target_ref="../NULL", проблема python-docx #1105);
  • Класс Docx распознает разрывы страниц (lastRenderedPageBreak / w:br type="page" / sectPr; для больших документов >1000 абзацев используется эвристическое сопоставление «~25 абзацев на страницу»), выполняя многопроцессную параллельную обработку страниц;
  • Поабзацное извлечение текста и встроенных картинок (a:blip/@r:embed → blob related_part → PIL, пропуск декоративных картинок <50px, масштабирование >1920px), с сохранением исходного порядка текста/изображений (content_sequence), изображения через колбэк _inline_upload встраиваются base64 в виде images/<uuid>.<ext>;
  • Таблицы конвертируются в HTML <table> (соседние ячейки с одинаковым текстом объединяются через colspan);
  • При общем сбое откатывается к _parse_using_simple_method (последовательное извлечение абзацев и строк таблиц через базовый python-docx без картинок);
  • Лимит страниц задается через DOCREADER_DOCX_MAX_PAGES (по умолчанию 0 = без ограничений).

excel_parser.py и три вспомогательных модуля (.xlsx /.xls) ​

ExcelParser основан на pandas: последовательно считывает листы DataFrame, удаляет полностью пустые строки и преобразует каждую строку в пары ключ-значение вида Заголовок: Значение,Заголовок: Значение, формируя Chunk на каждую строку (со смещениями start/end). Отсекает встроенные формулы изображений вроде WPS =DISPIMG("ID",mode) и Office 365 =_xlfn.IMAGE(...) (_IMAGE_FUNC_RE). Изображения не извлекаются.

Режим заголовка: поведение для XLSX и старого XLS унифицировано — по умолчанию строка 1 обрабатывается как данные, а столбцы именуются буквами A/B/C (чтобы случайно не отбросить строку данных как заголовок). Если таблица гарантированно содержит заголовки столбцов в первой строке, через правила движка анализа можно включить xlsx_first_row_as_header: тогда первая строка станет заголовками столбцов, а текст пар превратится в семантический вид: Имя: Иван,Отдел: Разработка. Пустые ячейки или значения функций изображений откатываются к буквам столбцов, а дублирующиеся заголовки получают суффиксы _2, _3 для уникальности.

Этот переключатель настраивается в базе знаний через parser_engine_rules[].xlsx_first_row_as_header (а также может быть переопределен разово в диалоге подтверждения загрузки), бэкенд в applyParserRuleOverrides() применяет его только для правил с xlsx/xls и движком builtin (или пустым), передавая в docreader как parser_engine_overrides. Тип поля — *bool: null означает значение по умолчанию анализатора, явный false — отключение.

Три вспомогательных модуля для работы с некорректными файлами:

  • excel_convert.py: определение реального формата (xlsx/xls/xlsb/ods) по сигнатуре/inspect_excel_format с выбором движка pandas (xlrd/openpyxl/odf); при невозможности распознавания (например, WPS .et, переименованный csv) выполняется нормализация через LibreOffice convert-to xlsx (normalize_excel_bytes последовательно пробует расширения .xlsx/.xls/.et/.csv).
  • xlsx_merge.py: fill_merged_cells_xlsx разъединяет объединенные ячейки и копирует значение из верхнего левого угла во все ячейки диапазона: openpyxl хранит значение только в левом верхнем углу, а pandas считывает остальные как NaN; после заполнения построчные блоки RAG сохраняют полный контекст.
  • xlsx_repair.py: устранение распространенных ошибок сборки пакета XLSX: исправление регистра и путей для sharedStrings.xml; если манифест ссылается на sharedStrings, но в пакете его нет, а листы используют только inline string, ссылки вырезаются из [Content_Types].xml и связей книги, позволяя openpyxl успешно прочитать файл.

Чтение XLSX предварительно проходит цикл repair → fill_merged_cells с использованием header=None и букв A/B/C как стабильных имен (для xls сначала пробуется первая строка как заголовок, при столбцах Unnamed: происходит откат к буквам).

ppt_convert.py / pptx_media.py (.ppt /.pptx для движка markitdown) ​

Для файлов серии PPT нет отдельного анализатора, они обрабатываются MarkitdownParser, а указанные модули выступают пре- и пост-обработчиками:

  • ppt_convert.py: normalize_ppt_bytes проверяет сигнатуру (ZIP = прямой пропуск pptx; OLE = старый ppt конвертируется через LibreOffice convert-to pptx, отдельный профиль + 3 попытки). При отсутствии LibreOffice для.ppt сразу выдается ошибка с предложением установки.
  • pptx_media.py: обработка медиафайлов PPTX, которые MarkItDown не может встроить (особенно векторных WMF/EMF/SVG): распаковывает ресурсы из ppt/media/, последовательно растеризует их в PNG с помощью Pillow (для растровых) или ImageMagick convert (для векторных), затем заменяет неразрешенные ссылки ![](...) в markdown на images/<uuid>.png со встроенными данными картинок.

image_parser.py — ImageParser (изолированные файлы изображений) ​

Простейший анализатор (29 строк): не производит OCR. Встраивает изображение base64 в Document.images, а в текст помещает одну строку ![имя_файла](images/имя_файла). Движок OCR расположен на стороне Go: комментарии в Dockerfile docreader подчеркивают, что зависимости OCR/PaddleOCR удалены; сторона Go выполняет OCR и генерацию описаний через internal/infrastructure/docparser/paddleocr_vl_converter.go / paddleocr_vl_cloud_converter.go (PaddleOCR-VL) и image_multimodal.go. Кроме того, simpleFormats в Go уже перевел обработку картинок на нативный уровень Go, поэтому ImageParser в docreader нужен в основном для прямых вызовов gRPC из SDK.

markdown_parser.py — MarkdownParser (.md /.markdown) ​

PipelineParser.create(MarkdownTableFormatter, MarkdownImageBase64):

  • MarkdownTableFormatter: автоопределение кодировки (endecode.decode_bytes: utf-8 → gb18030 → gb2312 → gbk → big5 → ascii → latin-1) с последующей нормализацией таблиц — выравнивание отступов и маркеров | cell |, normalize_spurious_table_prefixes для исправления лишних префиксов пустых/разделительных строк от MarkItDown, а также добавление разделителей | --- | GFM для таблиц Word без явных заголовков.
  • MarkdownImageBase64: извлечение встроенных изображений ![alt](data:image/xxx;base64,...) в ссылки images/<uuid>.<ext> + структуру Document.images (поддерживаются подтипы MIME с дефисами вроде x-emf). Изображения с ошибками декодирования пропускаются с записью в лог, не вызывая сбоя анализа всего документа.

Этот анализатор также служит общей стадией постобработки в конвейерах MarkitdownParser и WebParser.

web_parser.py — WebParser (режим URL) ​

PipelineParser.create(StdWebParser, MarkdownParser). StdWebParser использует Playwright (движок WebKit) для рендеринга страницы + trafilatura для извлечения основного контента в Markdown:

  • Двойная защита от SSRF: проверка is_ssrf_safe_url(url) перед переходом; затем установка обработчика маршрутов через page.route("**/*") с аналогичной проверкой каждого подзапроса и цели редиректа (utils/ssrf.py зеркалирует правила безопасности Go internal/utils/security.go: блокируются внутренние, loopback, link-local адреса, домены облачных метаданных, суффиксы вроде .local/.internal, прямые IP, IP-подобные имена хостов, запрещенные IP из DNS и опасные порты; исключения настраиваются через SSRF_WHITELIST / SSRF_WHITELIST_EXTRA).
  • Поддержка SPA: после события domcontentloaded ожидание networkidle (10 с) + ожидание видимого текста ≥80 символов в #app/main/body (15 с), что обеспечивает адаптацию к JS-рендерингу.
  • Адаптация к WeChat Official Accounts: monkey-patch для внутреннего utils.IMAGE_EXTENSION в trafilatura (распознавание картинок без расширения вида mmbiz.qpic.cn/...wx_fmt=) и xpaths.BODY_XPATH (приоритет #js_content / .rich_media_content).
  • Откат: если trafilatura не смогла извлечь текст, используется видимый текст Playwright (≥50 символов) + title страницы.
  • Прокси настраивается через DOCREADER_EXTERNAL_HTTPS_PROXY. Метаданные содержат title.

mhtml_parser.py — MHTMLParser (веб-архивы.mhtml) ​

Использует стандартную библиотеку email для разбора структуры MIME: собирает все части text/html, выбирает наибольшую нерекламную часть в качестве основного текста (с фильтрацией по черному списку доменов вроде googleads/doubleclick); части image/* извлекаются как images/... (приоритет имени из Content-Location, при конфликтах добавляется суффикс _2), создается таблица псевдонимов различных вариантов написания Content-Location/Content-ID(cid:)/X-Attachment-Id (экранирование HTML, URL-encode, basename, относительный urljoin) для переписывания <img src>. Конвертация HTML → Markdown выполняется через BeautifulSoup (удаление script/style/noscript/iframe, разворачивание внутренних ссылок) + markdownify, с нормализацией пустых строк с учетом блоков кода. При полном сбое преобразуется в блок кода ```html. Метаданные: source_format=mhtml, file_size, image_count.

html_parser.py — HTMLParser (статические файлы .html / .htm) ​

Файлы HTML, загруженные пользователем напрямую, обрабатываются по этому пути, отдельно от онлайн-захвата parse_url(): HTMLParser = PipelineParser.create(HTMLToMarkdownParser, MarkdownParser).

  • HTMLToMarkdownParser сначала декодирует исходные байты через BeautifulSoup(content, "lxml"), проверяя BOM и метатег charset;
  • Конвертация HTML → Markdown переиспользует MHTMLParser.html_to_markdown(), но с параметрами extract_images=False (в локальном HTML нет вложений MIME), strip_internal_links=False (сохранение внутренних ссылок) и fallback_to_raw_html=False (при невозможности конвертации возвращается пустая строка вместо блока ```html);
  • Удаленные изображения, на которые ведут ссылки <img src="http://...">, загружаются на стороне Go: internal/infrastructure/docparser/image_resolver.go скачивает их с проверкой SSRF и сохраняет в объектное хранилище, переписывая ссылки для прохождения общего процесса OCR / описания.

xmind_parser.py — XMindParser (интеллект-карты .xmind) ​

Считывает content.json (в новых версиях) или content.xml (в старых версиях) из архива XMind с ограничением размера одного файла контента в 32 МиБ. Каждый лист оформляется подразделом # Заголовок листа, иерархия тем преобразуется в Markdown-список с отступами, текстовые заметки добавляются блоками цитат под соответствующей темой, листы разделяются чертой ---. Изображения не извлекаются; при отсутствии тем для отображения парсинг завершается ошибкой.

epub_parser.py — EPUBParser (электронные книги .epub) ​

Основной путь использует ebooklib (чтение через временный файл): извлекает метаданные DC (title/author/publisher/language/description/date/isbn), последовательно обрабатывает главы по оглавлению TOC (берется первый h1/h2 как заголовок главы, формируется ## Заголовок + основной текст через markdownify), все ITEM_IMAGE извлекаются как images/<uuid>.<ext> с перезаписью <img src> по псевдонимам; внутренние ссылки книги разворачиваются с сохранением только текста. При сбое ebooklib происходит откат к прямому чтению ZIP: файлы html/xhtml сортируются по маске chapter(\d+) и конвертируются по очереди. Метаданные содержат chapter_count/image_count.

markitdown_parser.py — MarkitdownParser (движок markitdown) ​

PipelineParser.create(StdMarkitdownParser, MarkdownParser). StdMarkitdownParser является оберткой над библиотекой Microsoft MarkItDown (markitdown[docx,pdf,xls,xlsx]): ppt/pptx нормализуются через normalize_ppt_bytes; сначала выполняется конвертация с keep_data_uris=True (изображения остаются data URI для извлечения последующим MarkdownImageBase64), при сбое повторяется с keep_data_uris=False; если после конвертации pptx в markdown остались неразрешенные ссылки на изображения, вызывается attach_pptx_media_to_markdown. Параллелизм ограничен через parser_worker_limit("markitdown", DOCREADER_MARKITDOWN_MAX_WORKERS=1). Ограничения: в MarkItDown обработка PDF идет через извлечение текста pdfminer, бессильное перед сканами (в комментариях parse_local.py --scanned также отмечено, что pdfminer может намертво зависать); восстановление таблиц и макета уступает встроенной маршрутизации PDF builtin.

opendataloader_parser.py — OpenDataLoaderParser (движок opendataloader, только PDF) ​

Обертка над библиотекой Apache-2.0 opendataloader-pdf (анализ макета на Java): каждый вызов convert() поднимает JVM (ограничение parser_worker_limit("opendataloader", 1)), формируя markdown и каталог изображений; затем сканируются изображения, строится таблица псевдонимов (угловые скобки <images/foo.png>, сущности HTML, basename, нумерация imageFileN) и переписываются ссылки в markdown. Поддерживается гибридный режим (DOCREADER_ODL_HYBRID=docling-fast и др.): обращение к отдельно развернутому HTTP-сервису opendataloader-pdf-hybrid (DOCREADER_ODL_HYBRID_URL, по умолчанию http://127.0.0.1:5002, в Docker соответствует docker/Dockerfile.odl-hybrid), проверка доступности выполняется с повторами (быстрая проба 2 с × 1 раз; перед анализом 5 с × 6 раз на случай холодного старта). Если объем полученного текста менее 20 символов, результат считается неудачным с откатом к PDFScannedParser движка builtin. Требования доступности: наличие java в PATH (Java 11+, в образе установлен openjdk-17-jre-headless) + установленные пакеты Python + доступность сервиса hybrid.

Схема выбора анализатора ​


Обработка изображений и мультимодальное разделение труда ​

Контракт по изображениям на стороне docreader предельно прост: каждый анализатор возвращает изображения в виде словаря Document.images = {"images/<имя_файла>": "<base64>"}, а в тексте markdown на них формируются относительные ссылки ![...](images/<имя_файла>).

Два пути возврата данных в main.py:

  • Унарный Read: _resolve_images() декодирует base64 всех картинок во встроенные байты inline bytes ImageRef.image_data и возвращает их разом (image_dir_path всегда пустой — запись в общий том устарела, комментарий гласит: "The Go App is solely responsible for persisting images to the configured storage backend (local/minio/cos/tos)");
  • Потоковый ReadStream: _iter_image_refs() отдает изображения по одному через yield, освобождая память с помощью pop прямо во время отправки.

После приема на стороне Go (internal/infrastructure/docparser/image_resolver.go): inline bytes загружаются в объектное хранилище, а ссылки images/... в markdown переписываются на целевые URL хранилища. Затем internal/application/service/image_multimodal.go принимает решение на основе image_source_type в метаданных: полные страницы scanned_pdf отправляются на OCR (со специальным ocr_prompt), а обычные иллюстрации — на генерацию описаний VLM caption. Сервис имеет два конвейера (в трейсе обозначаются как caption_ocr / observation_driven): исторический конвейер (если в базе знаний не включен image_attrs_enabled) отправляет для каждого изображения отдельный запрос описания и отдельный запрос OCR; конвейер наблюдения атрибутов (при включении) в первом раунде определяет атрибуты по результату «наблюдение атрибутов + описание» (contain.text / contain.data_visual), после чего чистая функция DecideOCR на основе настроек image_actions решает, требуется ли OCR. Если contain.text ∈ {sparse, none}, наблюдение подтверждено и data_visual == false, по умолчанию формируется только описание без затрат на OCR. Понимание изображений в обоих конвейерах выполняется на стороне Go. Пользовательские инструкции анализа изображений (vlm_config.custom_instructions) добавляются только к промпту описания (или совмещенному промпту наблюдения и описания) и не попадают в промпт OCR, чтобы не нарушать формат его вывода (например, требование отвечать No text content при отсутствии текста).

Встроенные HTML-таблицы <table> в выводах всех движков анализа (характерно для MinerU, PaddleOCR-VL и VLM OCR) перед разбиением на блоки приводятся к таблицам Markdown; таблицы с объединенными ячейками, которые невозможно конвертировать, остаются в HTML, но каждая строка размещается отдельно, чтобы обеспечить корректные границы разбиения. Внутри docreader нет никаких вызовов VLM; поля vlm_config/storage_config в models/read_config.py сохранены лишь для совместимости со старыми сигнатурами конструкторов ("Legacy config kept for backward compatibility").


Позиция в исходном документе (source locators) ​

Каждый блок при сохранении получает набор source_locators, фиксирующий его положение в исходном файле, что позволяет подсвечивать цитаты в исходном тексте при клике в чате (см. Сессии и диалоговый интерфейс). Сведения о позиции формируются индивидуально каждым движком анализа в виде отображения фрагментов Markdown на координаты исходного файла (source block), а после разбиения на блоки пересекаются с диапазонами блоков для формирования индивидуальных локаторов:

ИсточникСпособ формированияlocator
builtin PDF (pdf_parser.py)Текстовые страницы разбиваются на строки по координатам глифов pdfium, затем объединяются в абзацы по интервалам, спискам и коротким строкам, формируя прямоугольник на каждый абзац; сканированные страницы и иллюстрации фиксируют только номер страницыpdf: page + bbox
MinerU (локальный / облачный / V1)Считывание content_list из ответа (page_idx + нормализованный bbox 0–1000) с текстовым выравниванием относительно Markdown; только для PDF и изображенийpdf: page + bbox
PaddleOCR-VL (локальный / облачный)Блоки макета постраничного вывода (prunedResult.parsing_res_list), при их отсутствии — постраничноpdf: page (+ bbox)
Word / PPT / Excel / CSV / EPUB (любой движок)Считывание структуры исходного файла на стороне Go (абзацы и таблицы docx, текст слайдов pptx, строки листов, разделы EPUB) с выравниванием по тексту Markdown без привязки к конкретному движкуdocx: block; slide: slide; sheet: sheet + row_start/row_end; section: section
builtin Excel (excel_parser.py)Прямая запись имени листа и номера строки для каждого рядаsheet
Markdown / TXT (прямая передача)Фиксация диапазона символов исходного текста по абзацамtext: start/end
АудиоПри наличии временных меток в ответе распознавания речи транскрипция выводится построчно с фиксацией времениtime: start_ms/end_ms

Выравнивание сравнивает только буквы и цифры без учета синтаксиса Markdown, пробелов и знаков препинания; в Go результат дополнительно проходит нормализацию переносов строк, конвертацию таблиц HTML и переписывание адресов изображений, поэтому диапазоны блоков заново проецируются на итоговый текст построчным сопоставлением. Каждый локатор содержит quote (цитируемый фрагмент до 300 символов), по которому фронтенд производит точный поиск в отрендеренном исходном файле и подбирает абзац, наиболее соответствующий предложению в ответе. Для старых версий doc/ppt/xls и веб-страниц структурные позиции отсутствуют, поэтому интерфейс откатывается к поиску по тексту или открытию исходной страницы.

Взаимосвязь разделителя splitter/ и chunker на Go ​

TextSplitter в docreader/splitter/splitter.py представляет собой рекурсивный разделитель с защитой блоков:

  • Значения по умолчанию согласованы с Go: chunk_size=512, chunk_overlap=80, комментарий в коде прямо гласит: "Aligned with internal/infrastructure/chunker/splitter.go (DefaultChunkOverlap = 80, DefaultChunkSize = 512). The Go splitter is now the production path; this Python splitter is kept for the docreader sidecar where it's still used." — то есть разбиение на блоки в рабочем конвейере выполняется на стороне Go (internal/infrastructure/chunker/, включая heading_splitter, heuristic_splitter, header_tracker и т.д.), а версия на Python оставлена для сценариев sidecar и локальной отладки с сохранением единых алгоритмов и значений по умолчанию.
  • Алгоритм разделения: рекурсивный разрез по приоритету разделителей (\n, 。, пробел, посимвольно в крайнем случае) → извлечение неделимых фрагментов через protected_regex (формулы $$...$$, картинки ![](...), ссылки [](...), строки таблиц Markdown, заголовки блоков кода) → сохранение целостности защищенных участков через _join → объединение по chunk_size/overlap через _merge с получением троек (start, end, text) (исходный текст может быть восстановлен без потерь через restore_text).
  • HeaderTracker в splitter/header_hook.py отслеживает заголовки таблиц Markdown при объединении: если новый блок начинается в середине таблицы, заголовок с разделителем автоматически добавляется в начало блока (при несовпадении колонок добавление отменяется, header_column_mismatch; пустые заголовки дополняются из первой строки данных, что соответствует поведению header_tracker в Go), благодаря чему найденные блоки таблиц сохраняют имена колонок.

В ответах gRPC блоки больше не возвращаются (в ReadResponse нет поля chunk); хотя ExcelParser помещает построчные блоки в Document.chunks, основной конвейер обрабатывает только content.


Полный список параметров конфигурации ​

config.py (DocReaderConfig, выводит действующие значения при старте) ​

Переменная окружения (псевдоним)По умолчаниюОписание
DOCREADER_GRPC_MAX_WORKERS (GRPC_MAX_WORKERS)4Количество потоков в пуле gRPC
DOCREADER_GRPC_MAX_FILE_SIZE_MB (MAX_FILE_SIZE_MB)50 (МБ)Лимит размера сообщений gRPC (пересчитывается в байты)
DOCREADER_GRPC_PORT (PORT)50051Порт прослушивания gRPC
DOCREADER_DOCX_MAX_PAGES0 (без лимита)Максимальное число страниц при обработке DOCX
DOCREADER_MARKITDOWN_MAX_WORKERS1Лимит параллелизма MarkItDown (≤0 отключает лимит)
DOCREADER_ODL_MAX_WORKERS1Лимит параллелизма OpenDataLoader (JVM)
DOCREADER_ODL_HYBRIDoffГибридный режим ODL (например, docling-fast)
DOCREADER_ODL_HYBRID_URLhttp://127.0.0.1:5002Адрес гибридного сервиса
DOCREADER_ODL_HYBRID_MODEautoПараметры гибридного режима
DOCREADER_ODL_HYBRID_FALLBACKfalseОткат при сбое гибридного режима
DOCREADER_ODL_MARKDOWN_WITH_HTMLfalseРазрешить HTML в markdown от ODL
DOCREADER_PDF_RENDER_MAX_WORKERS1Лимит воркеров на этапе рендеринга PDF (между запросами)
DOCREADER_PDF_RENDER_PARALLELISMmin(4, cpu)Число процессов-воркеров для рендеринга сканированных страниц одного PDF
DOCREADER_PDF_RENDER_DPI200DPI рендеринга сканированных страниц
DOCREADER_PDF_JPEG_QUALITY85Качество JPEG для изображений страниц
DOCREADER_PDF_RENDER_MAX_EDGE2000Лимит длинной стороны извлекаемых/рендеримых картинок в пикселях (0 = без лимита)
DOCREADER_EXTERNAL_HTTP_PROXY / DOCREADER_EXTERNAL_HTTPS_PROXY (EXTERNAL_HTTP_PROXY/EXTERNAL_HTTPS_PROXY)ПустоПрокси для внешних соединений (WebParser, конвертация DOC)
DOCREADER_IMAGE_OUTPUT_DIR (IMAGE_OUTPUT_DIR)/tmp/docreaderКаталог временных изображений (для локального отката, в основном конвейере диск не задействован)

Параметры маршрутизации PDF (модульные переменные pdf_parser.py, основные) ​

Переменная окруженияПо умолчаниюОписание
DOCREADER_PDF_SCAN_IMAGE_RATIO0.5Коэффициент площади картинок ≥ порога определяет сканированную страницу
DOCREADER_PDF_SCAN_MIN_CHARS10Меньшее число символов считается отсутствием полезного текстового слоя
DOCREADER_PDF_FORCE_SCANNEDfalseОбрабатывать все страницы как сканы (также задается через per-upload override pdf_force_scanned)
DOCREADER_PDF_EXTRACT_EMBEDDED_IMAGEStrueИзвлекать встроенные иллюстрации из текстовых страниц
DOCREADER_PDF_EMBED_MIN_PIXELS / _EMBED_MIN_AREA_RATIO / _EMBED_REPEAT_PAGE_FRAC / _EMBED_MAX_IMAGES80 / 0.01 / 0.5 / 50Фильтрация иллюстраций: минимальная сторона / доля листа / порог повторов для логотипов / лимит на документ
DOCREADER_PDF_LAYOUT_ORDERINGtrueГеометрическая реконструкция макета (порядок чтения колонок)
DOCREADER_PDF_DETECT_HEADINGStrueЭвристическое определение заголовков по размеру шрифта
DOCREADER_PDF_FILTER_HIDDEN_TEXTtrueФильтрация невидимого текста и текста за пределами листа (защита от prompt injection)
DOCREADER_PDF_SANITIZE_TEXT / _STRIP_CHART_DEBRIStrueОчистка заполнителей и обрывков диаграмм
DOCREADER_PDF_RENDER_VECTOR_FIGUREStrueРендеринг областей векторных графиков в JPEG
DOCREADER_PDF_SOURCE_BOXEStrueФиксация рамок абзацев на текстовых страницах (для подсветки цитат); при выключении сохраняются только номера страниц
DOCREADER_PDF_WORD_GAP_WIDTH_RATIO / _MARGIN_COL_WIDTH_RATIO / _MIN_HEADING_LINE_CHARS и др.0.4 / 0.12 / 8Тонкая настройка реконструкции макета (см. константы в коде)

Тайм-ауты анализа на стороне Go ​

Следующие переменные окружения относятся к основному сервису Go, а не к контейнеру docreader:

Переменная окруженияТипПо умолчаниюОписание
WEKNORA_DOCUMENT_PROCESS_TIMEOUTGo duration2hОбщий тайм-аут задачи обработки одного документа
WEKNORA_DOCREADER_CALL_TIMEOUTGo duration30mТайм-аут одного вызова docreader, должен быть меньше общего тайм-аута документа
WEKNORA_PADDLEOCR_VL_TIMEOUTGo duration1000sТайм-аут HTTP-запросов к локальному движку PaddleOCR-VL; пустые, некорректные или неположительные значения заменяются значением по умолчанию
WEKNORA_MINERU_TIMEOUTGo duration1000sТайм-аут разового анализа локального сервиса MinerU (API V1 покрывает весь цикл от загрузки до скачивания, в старых версиях — запрос /file_parse); пустые, некорректные или неположительные значения заменяются значением по умолчанию
WEKNORA_MINERU_CLOUD_TIMEOUTGo duration600sМаксимальное время ожидания результатов в облаке MinerU (mineru.net); пустые, некорректные или неположительные значения заменяются значением по умолчанию
WEKNORA_PADDLEOCR_VL_CLOUD_TIMEOUTGo duration600sМаксимальное время ожидания результатов в облачном сервисе PaddleOCR-VL (AI Studio); пустые, некорректные или неположительные значения заменяются значением по умолчанию
WEKNORA_WEKNORACLOUD_TIMEOUTGo duration20mМаксимальное время ожидания результатов управляемого docreader в aihelpme.ruCloud; должно быть меньше WEKNORA_DOCREADER_CALL_TIMEOUT; пустые, некорректные или неположительные значения заменяются значением по умолчанию

При обработке больших документов тайм-ауты следует настраивать с запасом от внутренних к внешним, например: PaddleOCR-VL или MinerU 90m, docreader 100m, обработка документа 2h.

Безопасность и прочие параметры ​

Переменная окруженияОписание
GRPC_AUTH_TOKENВключает аутентификацию по токену (metadata authorization: Bearer <token>)
GRPC_TLS_ENABLED / GRPC_TLS_CERT / GRPC_TLS_KEY / GRPC_TLS_CA / GRPC_MTLS_REQUIRE_CLIENT_CERTTLS / mTLS, сервис не запустится при некорректной конфигурации
SSRF_WHITELIST / SSRF_WHITELIST_EXTRAБелый список SSRF (через запятую, поддерживает *.suffix и CIDR)
SSRF_DNS_WHITELIST_ONLYПри установке в true разрешает доступ только к именам хостов из белого списка, остальные отклоняются до разрешения DNS; синхронизировано с сервисом Go
LOG_LEVELУровень логирования (по умолчанию INFO; формат логов включает request_id и длительность, см. utils/request.py)
LIBREOFFICE_PATH / ANTIWORD_PATHПереопределение путей к исполняемым файлам soffice / antiword

Рекомендации по развертыванию и масштабированию ​

Образ и системные зависимости (docker/Dockerfile.docreader) ​

Базовый образ python:3.10.18-bookworm, двухэтапная сборка (builder устанавливает зависимости через uv sync --locked и генерирует код protobuf через docreader/scripts/generate_proto.sh; runner копирует venv), порт EXPOSE 50051, запуск через CMD ["uv", "run", "-m", "docreader.main"]. Системные зависимости этапа выполнения:

  • LibreOffice (конвертация doc→docx, ppt→pptx, нестандартных таблиц в xlsx) + набор библиотек X11/шрифтов (libxinerama1, libfontconfig1, libcairo2, libcups2 и др.);
  • antiword (извлечение чистого текста из.doc);
  • openjdk-17-jre-headless (для OpenDataLoader PDF требуется Java 11+);
  • Playwright WebKit: python -m playwright install webkit + install-deps webkit — единственный шаг загрузки браузерных бинарников (после оптимизации модели OCR не скачиваются, в Dockerfile прямо указано: «удалены зависимости OCR/PaddleOCR»);
  • grpc_health_probe (проба проверки работоспособности gRPC для оркестраторов контейнеров);
  • ImageMagick convert (при наличии задействуется модулем pptx_media.py для растеризации WMF/EMF).

В каталоге scripts/ предусмотрены две утилиты: generate_proto.sh (генерация кода Python/Go через grpc_tools.protoc с исправлением путей импорта) и parse_local.py (локальная отладка результатов работы Parser в обход gRPC с поддержкой выгрузки markdown и картинок через --engine, --scanned, --out).

Зависимости Python (зафиксированы в pyproject.toml + uv.lock): grpcio, pypdfium2, markitdown[docx,pdf,xls,xlsx], opendataloader-pdf, python-docx, pandas/openpyxl/xlrd, playwright, trafilatura, beautifulsoup4/markdownify/lxml, ebooklib, pillow, pydantic, textract (отключен) и др.

Масштабирование и оптимизация ​

  • Горизонтальное масштабирование в приоритете: глобальная блокировка pdfium делает анализ PDF внутри одного экземпляра последовательным, поэтому пропускная способность PDF масштабируется количеством реплик. Go-клиент использует строку подключения dns:/// + round_robin, в K8s headless-сервис обеспечивает равномерное распределение нагрузки между репликами.
  • Вертикальная оптимизация экземпляра: при избытке CPU увеличьте DOCREADER_PDF_RENDER_PARALLELISM (ускорение рендеринга одного документа почти линейное) и DOCREADER_GRPC_MAX_WORKERS (форматы, отличные от PDF, обрабатываются параллельно); при нехватке памяти убедитесь, что Go задействует ReadStream (поведение по умолчанию).
  • Большие файлы: параметр MAX_FILE_SIZE_MB необходимо корректировать синхронно на обеих сторонах — в Go-клиенте и в docreader; размер изображений страниц сканов регулируется регуляторами DOCREADER_PDF_RENDER_MAX_EDGE/_DPI/_JPEG_QUALITY.
  • Изоляция нагрузок JVM и браузеров: каждый вызов OpenDataLoader запускает JVM, а WebParser запускает WebKit, что создает тяжелые процессы; значения по умолчанию 1 для DOCREADER_ODL_MAX_WORKERS и DOCREADER_MARKITDOWN_MAX_WORKERS консервативны, при достаточности ресурсов лимиты можно увеличить или снять (≤0). Гибридный сервис ODL (Dockerfile.odl-hybrid) рекомендуется развертывать отдельно и указывать через DOCREADER_ODL_HYBRID_URL.
  • Защита по тайм-ауту: параметр Go WEKNORA_DOCREADER_CALL_TIMEOUT (в конфигурации docreader_call_timeout, по умолчанию 30 минут) ограничивает одиночный вызов docreader, защищая воркеры от зависания; при необходимости анализа объемных документов соответственно увеличьте общий тайм-аут обработки (см. Тайм-ауты анализа на стороне Go).
  • Базовый уровень безопасности: в производственной среде задайте GRPC_AUTH_TOKEN (≥16 байт) + GRPC_TLS_ENABLED; без этих настроек сервис запустится в открытом режиме без аутентификации с выводом предупреждения WARNING.

Движок anydoc (анализ внутри процесса Go, минуя docreader) ​

anydoc — это опциональный движок анализа на стороне Go: библиотека anydoc (конвертер документов на Rust) линкуется через cgo напрямую в основной процесс платформы, конвертируя офисные документы в Markdown на месте. В отличие от docreader, он не использует сервис Python, не создает межпроцессных вызовов и не вызывает внешние бинарники, что делает его оптимальным для легковесных сред или задач с высокими требованиями к задержке.

Поддерживаемые форматы: doc, docx, docm, odt, rtf, ppt, pptx, pptm, odp, xls, xlsx, xlsm, ods, epub, csv, pdf.

Способ включения ​

Библиотека парсера является статической библиотекой Rust и требует цепочки инструментов Rust для сборки. Официальный Docker-образ и docker compose build по умолчанию линкуют anydoc, движок сразу доступен в настройках. Локальная сборка go build по умолчанию его не включает: без флага -tags anydoc движок будет помечен в списке как недоступный, остальные движки продолжат работать штатно.

bash
make build-anydoc                  # сборка статической библиотеки + бинарника с тегом anydoc
# эквивалентно:
scripts/build-anydoc-lib.sh && go build -tags anydoc ./cmd/server

В Docker-образе по умолчанию установлено WITH_ANYDOC=1. Чтобы пропустить инструментарий Rust и ускорить сборку:

bash
docker build -f docker/Dockerfile.app --build-arg WITH_ANYDOC=0 -t weknora-app .
# или укажите WITH_ANYDOC=0 в .env перед выполнением docker compose build

Явные правила анализа имеют наивысший приоритет. Если правила не заданы, при наличии anydoc поддерживаемые им сложные форматы направляются в него по умолчанию; исключение составляет PDF, который по умолчанию отправляется в builtin (anydoc извлекает только текстовый слой PDF с потерей картинок, таблиц и структуры, тогда как builtin распознает сканированные страницы и отдает их в OCR). Простые форматы по-прежнему обрабатываются встроенным Go SimpleFormatReader. Для сред без anydoc обработка PPT/PPTX по умолчанию откатывается к markitdown. При необходимости движок можно зафиксировать в настройках базы знаний для конкретных форматов (включая назначение pdf на anydoc) независимо от параметров сборки.

Границы возможностей ​

  • Сканированные PDF: anydoc извлекает только текстовый слой PDF. Для сканов без текстового слоя будет выдано сообщение о необходимости OCR; если DocReader (builtin) подключен, AnydocReader автоматически передаст файл в builtin для постраничного рендеринга JPEG с меткой image_source_type=scanned_pdf и последующим OCR на стороне Go. Если DocReader не подключен, конвертация завершится ошибкой — в этом случае используйте builtin, mineru или paddleocr_vl.
  • Отсутствие обратного заполнения вертикально объединенных ячеек: Docx2Parser в docreader дублирует объединенное значение во все строки диапазона, anydoc выводит его только в начальной строке, оставляя остальные пустыми. Для баз знаний, опирающихся на построчную семантику таблиц, надежнее использовать builtin.
  • Позиционирование картинок: при включенном извлечении картинок встроенные иллюстрации документа преобразуются в ссылки images/image-N.ext, после чего передаются стандартному сериализатору GFM anydoc, сохраняя исходное положение в абзацах/таблицах/списках. Параметр переопределения anydoc_extract_images=false отключает извлечение картинок для ускоренного рендеринга чистого текста (картинки заменяются alt-текстом).
  • Не обрабатывает URL, изображения и аудио: эти форматы остаются в ведении WebParser, SimpleFormatReader и конвейера ASR.

Расположение в исходном коде ​

ПутьНазначение
internal/infrastructure/docparser/anydoc/Слой адаптера: маппинг форматов, проверка доступности и два бэкенда (cgo / stub)
internal/infrastructure/docparser/anydoc_reader.goРеализация DocReader: сборка результатов конвертации и ссылок на картинки
internal/infrastructure/docparser/engines.goРегистрация движков (метаданные + фабрика Reader)
third_party/anydoc-go/Включенные в репозиторий привязки Go и шим C ABI (источник и изменения описаны в README каталога)

Движок MinerU для локального развертывания (прямое подключение из Go без docreader) ​

Движок mineru вызывается из Go App напрямую к сервису MinerU (internal/infrastructure/docparser/mineru_converter.go). В MinerU 4.0 устаревший эндпоинт /file_parse был удален и заменен на API V1, поэтому система перед каждым анализом запрашивает GET {mineru_endpoint}/v1/health, выбирая протокол по результату:

Результат пробыПротоколПроцесс
200 и status=okV1 (MinerU ≥ 4.0, mineru_v1_client.go)POST /v1/uploads → загрузка байтов по полученному upload_url → POST /v1/uploads/{id}/complete → POST /v1/parse/jobs (запрос только zip) → опрос GET /v1/parse/jobs/{id} → скачивание zip через GET /v1/files/{id}/content с извлечением markdown.md и images/
404 / 405Старая версия (MinerU ≤ 3.x)POST /file_parse, синхронный возврат Markdown и изображений в base64
503 со структурой ошибкиV1, но сервис не готовПрямой возврат ошибки (часто при сбое предзагрузки моделей) без отката к старому протоколу

Обновление MinerU не требует менять конфигурацию платформы. Соответствие параметров двух протоколов:

Параметр (ParserEngineConfig)MinerU ≥ 4.0MinerU ≤ 3.x
mineru_endpointАдрес сервиса V1 (например, http://mineru:8000)То же самое
mineru_server_api_keyПередается как Authorization: Bearer, если сервис запущен с --api-keyНе используется
mineru_tiertier: flash / basic / standard / advanced; при пустом значении сервер выбирает уровень по умолчанию (приоритет standard)Не используется
mineru_parse_methodocr_mode (auto / txt / ocr)parse_method
mineru_model, mineru_vlm_server_url, mineru_enable_formula, mineru_enable_table, mineru_languageИгнорируются (параметры удалены в 4.0; адрес VLM настраивается в config.yaml MinerU)Передаются без изменений

Особенности протокола V1:

  • При загрузке передается sha256sum: если файл уже есть на сервере, он используется повторно без повторной отправки байтов.
  • API Key передается, только если upload_url совпадает по источнику с mineru_endpoint (совпадают схема, хост и порт); сторонние адреса (например, предписанные URL объектных хранилищ от официального API) вызываются без ключа и проверяются на SSRF.
  • Опрос статуса выполняется с экспоненциальной задержкой от 2 до 30 секунд; общее время ожидания по умолчанию составляет 1000 секунд (настраивается через WEKNORA_MINERU_TIMEOUT). При тайм-ауте или отмене клиентом отправляется DELETE /v1/parse/jobs/{id} для отмены задачи на сервере.
  • Состояние задач и загрузок сервиса MinerU V1 хранится в оперативной памяти процесса; после перезапуска сервиса опрашиваемые задачи вернут 404 с ошибкой анализа.
  • Кнопка «Проверить подключение» на сервисе V1 отправляет дополнительный авторизованный запрос GET /v1/parse/jobs?limit=1 для проверки корректности API Key (/v1/health не проверяет ключи).

Облачный сервис mineru_cloud (mineru.net) продолжает использовать пакетный интерфейс /api/v4/file-urls/batch и не затронут изменениями версии 4.0.


Приложение: Сводка ключевых фактов ​

  • Внешние интерфейсы: только gRPC, порт 50051 (DOCREADER_GRPC_PORT/PORT), методы RPC: Read / ReadStream / ListEngines + стандартный сервис Health.
  • Полный список форматов, поддерживаемых напрямую docreader: pdf, docx, doc, xlsx, xls, pptx, ppt, xmind (движок markitdown дополнительно поддерживает csv), md/markdown, epub, html/htm, mhtml, изображения jpg/jpeg/png/gif/bmp/tiff/webp, а также захват веб-страниц по URL; форматы txt/csv/json/изображения/аудио в основном потоке обрабатываются SimpleFormatReader в Go и не поступают в сервис.
  • OCR / VLM: внутри docreader нет моделей OCR и VLM; страницы сканов и иллюстрации передаются в виде изображений, а OCR (PaddleOCR-VL) и генерация описаний выполняются в Go App.
  • Возврат изображений: во встроенных байтах (ImageRef.image_data), сохранение в local/minio/cos/tos выполняется силами Go.
  • Разбиение на блоки: боевой конвейер реализован в Go chunker; модуль Python TextSplitter (512/80) оставлен исключительно для sidecar и синхронизирован с реализацией Go.

Версия v0.8.3 · MIT License