Сервис анализа документов 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:
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:
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() регистрирует три движка:
| Движок | Типы файлов | Описание |
|---|---|---|
builtin | docx(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 |
markitdown | md, markdown, pdf, docx, doc, pptx, ppt, xlsx, xls, csv (все через MarkitdownParser) | Библиотека Microsoft MarkItDown |
opendataloader | pdf(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 и связанные компоненты)
Контроль конкурентности разделен на четыре уровня:
- Пул потоков gRPC:
ThreadPoolExecutor(max_workers=CONFIG.grpc_max_workers)(по умолчанию 4), то есть одновременно обрабатывается не более 4 запросов. - Ограничение именованными семафорами (
parser_worker_limit(name, max_workers)): процессныйthreading.BoundedSemaphore, повторно используемый по имени для ограничения параллелизма тяжелых бэкендов. Текущие точки ограничения:"markitdown"(по умолчанию 1),"opendataloader"(по умолчанию 1, каждый convert запускает JVM),"pdf_render"(по умолчанию 1). Приmax_workers <= 0ограничение снято. - Глобальная блокировка pdfium (
pdf_parser.py::_PDFIUM_LOCK): библиотека C pdfium является глобальной для процесса и не потокобезопасна. Одновременный анализ PDF двумя рабочими потоками gRPC нарушает общее состояние или приводит к взаимоблокировке всего процесса (наблюдалось постоянное зависание запроса на "Parsing document with PDFParser"). Поэтому все операции pdfium (извлечение текста, рендеринг страниц, извлечение картинок) сериализованы за этой глобальной блокировкой, конкурентные загрузки PDF выстраиваются в очередь; парсеры других форматов не затрагиваются. - Параллелизм на уровне процессов:
- Рендеринг сканированных страниц 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: Извлечение текста + Классификация: текстовые страницы обрабатываются по текстовому слою. Если
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) и вставляется перед подписью в виде. - Проход 2: Рендеринг сканированных страниц: только scanned страницы рендерятся в JPEG (DPI
DOCREADER_PDF_RENDER_DPI=200, качествоDOCREADER_PDF_JPEG_QUALITY=85, ограничение длинной стороныDOCREADER_PDF_RENDER_MAX_EDGE=2000px — защита от формирования изображений на 100+ Мп из PDF с огромными размерами страниц), в markdown вставляется заполнитель, в metadata выставляется флагimage_source_type=scanned_pdf, и Go App запускает OCR для этих страниц (компонентimage_multimodal.goна стороне Go использует специальныйocr_promptдля источниковscanned_pdf). - Проход 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, цепочка обработки (попытки по порядку):
_parse_with_docx: конвертация DOC в DOCX с помощью LibreOffice (soffice --headless --convert-to docx, отдельный профиль + 3 попытки) с последующим анализом цепочкой родительского класса DOCX (единственный путь, извлекающий изображения);_parse_with_antiword: извлечение чистого текста утилитой командной строкиantiword(выполняется черезSandboxExecutorс принудительными переменными прокси, по умолчанию используется «черная дыра»http://128.0.0.1:1, предотвращающая нежелательные внешние соединения);_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) выполняется нормализация через LibreOfficeconvert-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 конвертируется через LibreOfficeconvert-to pptx, отдельный профиль + 3 попытки). При отсутствии LibreOffice для.ppt сразу выдается ошибка с предложением установки.pptx_media.py: обработка медиафайлов PPTX, которые MarkItDown не может встроить (особенно векторных WMF/EMF/SVG): распаковывает ресурсы изppt/media/, последовательно растеризует их в PNG с помощью Pillow (для растровых) или ImageMagickconvert(для векторных), затем заменяет неразрешенные ссылкив markdown наimages/<uuid>.pngсо встроенными данными картинок.
image_parser.py — ImageParser (изолированные файлы изображений)
Простейший анализатор (29 строк): не производит OCR. Встраивает изображение base64 в Document.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: извлечение встроенных изображенийв ссылки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зеркалирует правила безопасности Gointernal/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 на них формируются относительные ссылки .
Два пути возврата данных в main.py:
- Унарный
Read:_resolve_images()декодирует base64 всех картинок во встроенные байты inline bytesImageRef.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_PAGES | 0 (без лимита) | Максимальное число страниц при обработке DOCX |
DOCREADER_MARKITDOWN_MAX_WORKERS | 1 | Лимит параллелизма MarkItDown (≤0 отключает лимит) |
DOCREADER_ODL_MAX_WORKERS | 1 | Лимит параллелизма OpenDataLoader (JVM) |
DOCREADER_ODL_HYBRID | off | Гибридный режим ODL (например, docling-fast) |
DOCREADER_ODL_HYBRID_URL | http://127.0.0.1:5002 | Адрес гибридного сервиса |
DOCREADER_ODL_HYBRID_MODE | auto | Параметры гибридного режима |
DOCREADER_ODL_HYBRID_FALLBACK | false | Откат при сбое гибридного режима |
DOCREADER_ODL_MARKDOWN_WITH_HTML | false | Разрешить HTML в markdown от ODL |
DOCREADER_PDF_RENDER_MAX_WORKERS | 1 | Лимит воркеров на этапе рендеринга PDF (между запросами) |
DOCREADER_PDF_RENDER_PARALLELISM | min(4, cpu) | Число процессов-воркеров для рендеринга сканированных страниц одного PDF |
DOCREADER_PDF_RENDER_DPI | 200 | DPI рендеринга сканированных страниц |
DOCREADER_PDF_JPEG_QUALITY | 85 | Качество JPEG для изображений страниц |
DOCREADER_PDF_RENDER_MAX_EDGE | 2000 | Лимит длинной стороны извлекаемых/рендеримых картинок в пикселях (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_RATIO | 0.5 | Коэффициент площади картинок ≥ порога определяет сканированную страницу |
DOCREADER_PDF_SCAN_MIN_CHARS | 10 | Меньшее число символов считается отсутствием полезного текстового слоя |
DOCREADER_PDF_FORCE_SCANNED | false | Обрабатывать все страницы как сканы (также задается через per-upload override pdf_force_scanned) |
DOCREADER_PDF_EXTRACT_EMBEDDED_IMAGES | true | Извлекать встроенные иллюстрации из текстовых страниц |
DOCREADER_PDF_EMBED_MIN_PIXELS / _EMBED_MIN_AREA_RATIO / _EMBED_REPEAT_PAGE_FRAC / _EMBED_MAX_IMAGES | 80 / 0.01 / 0.5 / 50 | Фильтрация иллюстраций: минимальная сторона / доля листа / порог повторов для логотипов / лимит на документ |
DOCREADER_PDF_LAYOUT_ORDERING | true | Геометрическая реконструкция макета (порядок чтения колонок) |
DOCREADER_PDF_DETECT_HEADINGS | true | Эвристическое определение заголовков по размеру шрифта |
DOCREADER_PDF_FILTER_HIDDEN_TEXT | true | Фильтрация невидимого текста и текста за пределами листа (защита от prompt injection) |
DOCREADER_PDF_SANITIZE_TEXT / _STRIP_CHART_DEBRIS | true | Очистка заполнителей и обрывков диаграмм |
DOCREADER_PDF_RENDER_VECTOR_FIGURES | true | Рендеринг областей векторных графиков в JPEG |
DOCREADER_PDF_SOURCE_BOXES | true | Фиксация рамок абзацев на текстовых страницах (для подсветки цитат); при выключении сохраняются только номера страниц |
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_TIMEOUT | Go duration | 2h | Общий тайм-аут задачи обработки одного документа |
WEKNORA_DOCREADER_CALL_TIMEOUT | Go duration | 30m | Тайм-аут одного вызова docreader, должен быть меньше общего тайм-аута документа |
WEKNORA_PADDLEOCR_VL_TIMEOUT | Go duration | 1000s | Тайм-аут HTTP-запросов к локальному движку PaddleOCR-VL; пустые, некорректные или неположительные значения заменяются значением по умолчанию |
WEKNORA_MINERU_TIMEOUT | Go duration | 1000s | Тайм-аут разового анализа локального сервиса MinerU (API V1 покрывает весь цикл от загрузки до скачивания, в старых версиях — запрос /file_parse); пустые, некорректные или неположительные значения заменяются значением по умолчанию |
WEKNORA_MINERU_CLOUD_TIMEOUT | Go duration | 600s | Максимальное время ожидания результатов в облаке MinerU (mineru.net); пустые, некорректные или неположительные значения заменяются значением по умолчанию |
WEKNORA_PADDLEOCR_VL_CLOUD_TIMEOUT | Go duration | 600s | Максимальное время ожидания результатов в облачном сервисе PaddleOCR-VL (AI Studio); пустые, некорректные или неположительные значения заменяются значением по умолчанию |
WEKNORA_WEKNORACLOUD_TIMEOUT | Go duration | 20m | Максимальное время ожидания результатов управляемого 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_CERT | TLS / 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 движок будет помечен в списке как недоступный, остальные движки продолжат работать штатно.
make build-anydoc # сборка статической библиотеки + бинарника с тегом anydoc
# эквивалентно:
scripts/build-anydoc-lib.sh && go build -tags anydoc ./cmd/serverВ Docker-образе по умолчанию установлено WITH_ANYDOC=1. Чтобы пропустить инструментарий Rust и ускорить сборку:
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=ok | V1 (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.0 | MinerU ≤ 3.x |
|---|---|---|
mineru_endpoint | Адрес сервиса V1 (например, http://mineru:8000) | То же самое |
mineru_server_api_key | Передается как Authorization: Bearer, если сервис запущен с --api-key | Не используется |
mineru_tier | tier: flash / basic / standard / advanced; при пустом значении сервер выбирает уровень по умолчанию (приоритет standard) | Не используется |
mineru_parse_method | ocr_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.
