文档解析服务 docreader
上传一个 PDF 之后,系统要先把它变成能被切分和索引的文本——复杂格式由独立的解析服务 docreader 完成,纯文本、图片和音频由 Go 主服务直接处理。作为使用者,你通常只需要知道两件事:支持哪些格式,以及解析不理想时能调什么。
可以上传的格式(上传白名单见 internal/application/service/knowledge_util.go):
| 类别 | 格式 |
|---|---|
| 文档 | PDF、Word(doc/docx)、PPT(ppt/pptx)、Excel(xls/xlsx)、EPUB、XMind 思维导图(xmind) |
| 文本 | txt、Markdown(md/markdown)、CSV、JSON |
| 网页 | 在线 URL 抓取、本地 HTML(html/htm)/ MHTML 归档 |
| 图片 | jpg、jpeg、png、gif、webp(需要在知识库里开启图像处理并配置视觉模型才能理解内容) |
| 音频 | mp3、wav、m4a、flac、ogg(需要配置语音识别模型) |
| 视频 | mp4、mov、avi、mkv、webm、wmv、flv、m4v(需要语音识别模型转写音轨,或开启图像处理描述关键帧,至少其一;docreader 需装有 ffmpeg) |
单个文件的大小上限由系统管理员在运行时设置(file.max_size_mb,默认取 MAX_FILE_SIZE_MB=50;视频单独用 file.video_max_size_mb,默认取 MAX_VIDEO_FILE_SIZE_MB=2048)。
解析结果不理想时可以调整:
- PDF 版式还原差、表格错位:在知识库编辑弹窗的「解析引擎」里为
pdf换一个引擎(MarkItDown、OpenDataLoader、anydoc,或配置好的 MinerU / PaddleOCR-VL 服务); - 扫描件、网页打印的 PDF 没识别出文字:确认知识库开启了图像处理(视觉模型),上传时在确认对话框里勾选「按扫描件解析 PDF」,逐页渲染后由视觉模型 OCR;
- Excel 首行是列名却被当成数据:为
xlsx/xls打开「首行作为表头」; - 个别段落切错:不必重传整个文档,直接在分块列表里改,见知识库与知识管理的分块编辑一节。
以下是 docreader 的完整实现说明,供二次开发与排障参考。
docreader/ 是 Yuheng 中独立的 Python 文档解析微服务(gRPC sidecar)。它的唯一职责是:把各种格式的文件 / URL 转换为 Markdown 文本 + 原始图片引用,供 Go 主服务(App)完成后续的分块(chunking)、图片持久化、OCR、VLM caption、向量化等流程。
经过"轻量化重构"后,docreader 本身不做 OCR、不做 VLM caption、不做分块、不做对象存储上传——这些全部在 Go 侧完成。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.
"""1. 服务定位与对外接口
1.1 接口协议:纯 gRPC(无 HTTP)
服务入口是 docreader/main.py,只启动一个 gRPC server(grpc.server + ThreadPoolExecutor),默认监听 50051 端口,同时注册标准的 gRPC Health 服务(grpc_health.v1)供 K8s / Docker 探活(配合镜像内的 grpc_health_probe 二进制)。没有任何 HTTP 接口。
Go 侧客户端还支持 DOCREADER_TRANSPORT=http,按 /read、/list-engines 的 JSON 协议调用一个兼容的 HTTP 服务(internal/infrastructure/docparser/http_parser.go);本仓库的 docreader 本身只提供 gRPC,默认 DOCREADER_TRANSPORT=grpc、DOCREADER_ADDR=docreader:50051。DOCREADER_ADDR 为空时主服务以「未连接」状态启动,此时只能处理 Go 原生支持的格式。
Proto 定义在 docreader/proto/docreader.proto,共 2 个 RPC:
service DocReader {
// 唯一的解析入口,流式返回:先发 1 帧 meta(markdown/metadata/error),
// 然后是视频的音轨分段,最后每帧 1 张图片。每帧都很小,大扫描件 PDF
// (数百页图片)也不会撞上单条消息的大小上限(RESOURCE_EXHAUSTED)。
rpc ReadStream(ReadRequest) returns (stream ReadStreamResponse) {}
rpc ListEngines(ListEnginesRequest) returns (ListEnginesResponse) {}
}ReadRequest 是统一请求:设置 file_content/file_name/file_type 为文件模式,设置 url/title 为 URL 模式;另有 file_path,供大体积媒体(多 GB 的视频)走共享卷——Go 传两个容器都挂载的路径而不是把字节塞进 gRPC,docreader 校验路径必须落在 DOCREADER_SHARED_DATA_DIR(默认 /data/files)之内。config.parser_engine 指定引擎(builtin / markitdown / opendataloader),config.parser_engine_overrides 传递引擎级覆盖参数(如 pdf_force_scanned、xlsx_first_row_as_header、odl_hybrid)。
ReadStream 的每一帧(ReadStreamResponse)是 oneof:
- 首帧
ReadStreamMeta:markdown_content、metadata、error、image_count(尽力而为的图片总数,未知时为 0);解析视频时还带MediaInfo(时长、分辨率、是否有音轨)。 - 随后是
AudioChunk(仅视频):音轨按固定时长切成段,每段再按传输大小切片,同一段的切片共享segment_index/start_ms/end_ms,Go 侧按到达顺序拼回。 - 最后每张图片一帧
ImageRef:filename、original_ref、mime_type、image_data(inline bytes),视频关键帧另带timestamp_ms。图片持久化完全由 Go App 负责,docreader 不写任何共享目录或存储。
流式的价值(见 main.py::ReadStream 与 _iter_image_refs):每帧独立、体积小;服务端边解码 base64 边 images.pop(ref_path) 释放源数据,双方都不必同时持有全部图片,解决大扫描件 PDF 的峰值内存和消息尺寸问题。Go 侧 internal/infrastructure/docparser/grpc_parser.go 只调用 ReadStream,没有 unary 回退。
proto 里被删掉的字段,若所在消息仍在使用,就把编号和名字一起 reserved(如 ImageRef 的 storage_key = 4、ReadStreamMeta 的 image_dir_path = 2),防止以后新字段复用同一编号,被仍按旧 schema 编解码的一端误读;整条消息删掉的(如原来的 ReadResponse)无需保留。
ListEngines 返回 docreader 注册的引擎及其可用性。设置页的引擎列表由 Go 侧 docparser.ListAllEngines(internal/infrastructure/docparser/engine_registry.go)合并而成:Go 本地注册的引擎总在列表里;docreader 已连接时再调用 ListEngines,同名引擎以 docreader 报告的文件类型为准,Go 不认识的引擎(markitdown、opendataloader)原样追加,并在解析时路由给 docreader。docreader/parser/registry.py 里「Go 已不再调用该 RPC」的注释已经过时。
1.2 认证与 TLS(auth.py)
docreader/auth.py 提供两层安全机制,均通过环境变量开启:
Token 认证(AuthInterceptor):设置 GRPC_AUTH_TOKEN 后启用。客户端需在 metadata 里携带 authorization: Bearer <token>(或裸 token)。校验用 hmac.compare_digest 防时序攻击;token 短于 16 字节会打警告。两个健康检查方法(/grpc.health.v1.Health/Check、/Watch)在鉴权前放行,保证探活不受影响。鉴权失败时通过 _make_abort_handler 构造与原 RPC kind(unary/stream)匹配的 abort handler,返回 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,BuildDialOptions 生成带 round_robin 负载均衡、消息上限、TLS 与 token 的 dial options。连接本身由 internal/infrastructure/docparser/grpc_parser.go 建立(消息上限取 MAX_FILE_SIZE_MB)。同目录的 client_test.go 是对真实 docreader 的 ReadStream / ListEngines 集成测试,CI(.github/workflows/docreader.yml)会先拉起服务再跑;本地没有服务时自动跳过。
1.3 与主服务的交互时序
Go App 中 internal/application/service/knowledge_process.go 在文档入库流水线的 docreader stage 调用解析(单次调用超时 docreader_call_timeout / YUHENG_DOCREADER_CALL_TIMEOUT,默认 30 分钟,须小于整个文档处理任务的超时 YUHENG_DOCUMENT_PROCESS_TIMEOUT,默认 2 小时)。注意:知识库没有为该文件类型指定引擎时,md/markdown/txt/csv/json/图片/音频由 Go 侧 SimpleFormatReader 原生处理,不经过 docreader(见 internal/infrastructure/docparser/builtin_converter.go 的 simpleFormats);显式选了 builtin 则仍交给 docreader。视频总是交给 docreader。
2. 解析器注册与调度机制
2.1 引擎全景:Go 侧注册表与 docreader 注册表
知识库「解析引擎」设置里可选的引擎来自两处。Go 侧 internal/infrastructure/docparser/engines.go 注册的本地引擎:
| 引擎 | 在哪里运行 | 文件类型 | 可用条件 |
|---|---|---|---|
builtin | docreader | 见下表 | docreader 已连接 |
simple | Go 进程内 | md、markdown、txt、csv、json、图片、音频 | 总是可用 |
anydoc | Go 进程内(cgo) | doc、docx、docm、odt、rtf、ppt、pptx、pptm、odp、xls、xlsx、xlsm、ods、epub、csv、pdf | 二进制链接了 anydoc(见 §8) |
mineru / mineru_cloud | 自托管 MinerU 服务 / MinerU 云 API | pdf、图片、doc、docx、ppt、pptx | 工作区设置里配置了服务地址 / API Key 且探测可达 |
mineru_tianshu | 自托管 MinerU 天枢任务队列 | 另含 xls、xlsx、html、htm | 配置了服务地址(及所需凭据)且探测可达 |
paddleocr_vl / paddleocr_vl_cloud | 自托管 PaddleOCR-VL / AI Studio 云 API | pdf、jpg、jpeg、png、bmp、tiff | 配置了服务地址 / Token 且探测可达 |
MinerU、PaddleOCR-VL 这类远程引擎的地址与凭据是工作区级配置(Tenant.ParserEngineConfig),由 Go 直接调用,不经 docreader。上表的文件类型是引擎能处理的范围,最终能否上传仍受上传白名单约束(例如 bmp、tiff、odt 不在白名单内:图片以按字节嗅探出的 MIME 类型送给视觉模型,TIFF 嗅探不出、会被当作 PNG 发送,BMP 常见的视觉模型接口不收,浏览器也不显示 TIFF,放行只会得到一条没有内容可检索的知识)。
docreader 侧 ParserEngineRegistry(docreader/parser/registry.py)维护 引擎名 → {文件扩展名 → 解析器类} 的两级映射,并支持每个引擎注册 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)、mp4/mov/avi/mkv/webm/wmv/flv/m4v(VideoParser,仅在 PATH 上有 ffmpeg 与 ffprobe 时注册) | 内置解析引擎 |
markitdown | md、markdown、pdf、docx、doc、pptx、ppt、xlsx、xls、csv(全部 MarkitdownParser) | 微软 MarkItDown 库 |
opendataloader | pdf(OpenDataLoaderParser) | OpenDataLoader PDF 版面分析,需 Java 11+;check_available 探测 java、Python 包及 hybrid 服务健康 |
调度规则(get_parser_class):请求指定的引擎若不支持该文件类型,自动回退 builtin 引擎;builtin 也没有则抛 ValueError("Unsupported file type")。
2.2 门面与文件魔数纠偏(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(来自 proto 的 parser_engine_overrides)作为 **kwargs 传入解析器构造函数,例如 pdf_force_scanned 由 PDFParser.__init__ 捕获。
2.3 链式解析器(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)。
2.4 并发模型(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):pdfium C 库是进程全局且非线程安全的,两个 gRPC worker 同时解析 PDF 会破坏共享状态甚至死锁整个进程(曾观测到请求永久卡在 "Parsing document with PDFParser")。因此所有 pdfium 操作(文本抽取、页面渲染、图片抽取)都串行化在这把全局锁后,并发 PDF 上传排队处理;非 PDF 解析器不受影响。 - 进程级并行:
- PDF 扫描页渲染:
_render_pages_parallel用ProcessPoolExecutor(优先forkserver启动方式,规避多线程进程 fork 的风险)把单个 PDF 的扫描页分片给多个 worker 进程渲染(每个进程从临时文件独立打开PdfDocument),并行度DOCREADER_PDF_RENDER_PARALLELISM(默认min(4, cpu))。这是把 CPU 受限容器上"大扫描件渲染 1 小时+"压下来的主要手段;失败时透明回退串行。 - DOCX 按页并行:
docx_parser.py::Docx把页面任务分发给ProcessPoolExecutor+Manager共享列表,图片通过/tmp/docx_img_*临时文件跨进程传递,最后在主进程统一编码上传。 - LibreOffice 转换(doc→docx、ppt→pptx、xls→xlsx):
subprocess调用soffice --headless,每次尝试用独立的-env:UserInstallation=<临时 profile>规避并发 soffice 争抢用户 profile 锁导致的静默失败,失败重试 3 次退避。
- PDF 扫描页渲染:
3. 解析器逐一详解
3.1 pdf_parser.py — PDFParser / PDFScannedParser(builtin 引擎的 PDF)
依赖: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,三个 Pass):
- Pass 1 文本抽取 + 分类:text 页走文本层。若
DOCREADER_PDF_LAYOUT_ORDERING=true(默认)且 pdfium 纯文本不"良构"(_plain_is_well_formed),则做几何版面重建:glyph 级抽取(过滤隐藏文本 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)。text 页上检测到Figure Ncaption 时,还会把 caption 上方的矢量图区域渲染成 JPEG(RENDER_VECTOR_FIGURES)并以注入 caption 前。 - Pass 2 扫描页渲染:仅渲染 scanned 页为 JPEG(DPI
DOCREADER_PDF_RENDER_DPI=200,质量DOCREADER_PDF_JPEG_QUALITY=85,长边钳制DOCREADER_PDF_RENDER_MAX_EDGE=2000px——防止声明超大页框的 PDF 渲染出 100+ MP 图撞 gRPC 上限),markdown 中以占位,metadata 标记image_source_type=scanned_pdf,由 Go App 对这些页面图执行 OCR(Go 侧image_multimodal.go对scanned_pdf来源使用专门的ocr_prompt)。 - Pass 3 嵌入图抽取:从 text 页抽取嵌入的插图/图表(
EXTRACT_EMBEDDED_IMAGES),按最小像素(80)、最小页面积占比(1%)、跨页重复率(同一 MD5 出现在 ≥50% text 页视为 logo/水印剔除)、每文档上限(50 张)过滤,按页内自上而下顺序插入 markdown。
另有 _strip_repeating_lines 保守剔除跨页重复的页眉页脚(候选仅每页首尾行、须短、须出现在 ≥60% text 页)。
任何异常都会回退到 PDFScannedParser:把每一页渲染成 JPEG 的兜底解析器(也用于 pdf_force_scanned 强制扫描模式,可通过 per-upload override 或 DOCREADER_PDF_FORCE_SCANNED 开启)。
产物:Markdown(text 页文本 + 图片占位)、images dict、metadata(page_count/scanned_page_count/text_page_count/embedded_image_count/vector_figure_count/image_source_type)。
局限:不做表格结构识别(文本层表格按行输出);标题识别是字号启发式;扫描页文本完全依赖 Go 侧 OCR。
3.2 doc_parser.py — DocParser(.doc 老式 Word)
继承 Docx2Parser,处理链(依次尝试):
_parse_with_docx:用 LibreOffice(soffice --headless --convert-to docx,独立 profile + 3 次重试)把 DOC 转成 DOCX,再用父类 DOCX 链解析(唯一能提取图片的路径);_parse_with_antiword:antiword命令行纯文本提取(通过SandboxExecutor执行,强制注入代理环境变量,默认http://128.0.0.1:1的"黑洞代理"阻断子进程意外外联);_parse_with_textract:已禁用(textract 存在 SSRF 漏洞,代码保留但注释掉)。
依赖:LibreOffice(soffice)、antiword(镜像内已装);查找路径支持 LIBREOFFICE_PATH/ANTIWORD_PATH 环境变量。局限:无 LibreOffice 时退化为 antiword 纯文本(无图片、无表格结构)。
3.3 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 issue #1105); Docx处理类识别分页(lastRenderedPageBreak/w:br type="page"/sectPr;>1000 段落的大文档改用"每页约 25 段"启发式映射),按页多进程并行处理;- 逐段提取文本 + 内嵌图片(
a:blip/@r:embed→ related_part blob → 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 = 不限制)。
- 打了 python-docx 的补丁
3.4 excel_parser.py 及三个辅助模块(.xlsx / .xls)
ExcelParser 基于 pandas:逐 sheet 读取 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 后缀保证唯一。
该开关通过 KB 的 parser_engine_rules[].xlsx_first_row_as_header 配置(也可在上传确认对话框中按次覆盖),后端 applyParserRuleOverrides() 只对 xlsx/xls 且引擎为 builtin(或留空)的规则生效,最终作为 parser_engine_overrides 传给 docreader。字段类型是 *bool:null 表示沿用解析器默认值,显式 false 表示关闭。
三个辅助模块解决现实中的脏文件:
excel_convert.py:魔数/inspect_excel_format检测真实格式(xlsx/xls/xlsb/ods),为每种格式选 pandas engine(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 chunk 才能保留上下文。xlsx_repair.py:修复常见 XLSX 打包问题——sharedStrings.xml路径大小写/位置不规范时重命名归位;manifest 引用了 sharedStrings 但包内缺失且工作表只用 inline string 时,从[Content_Types].xml与 workbook rels 中剥掉引用,使 openpyxl 能读。
XLSX 读取前统一走 repair → fill_merged_cells 预处理,并用 header=None + A/B/C 列字母作为稳定列名(xls 则先尝试首行做表头,遇 Unnamed: 列回退列字母)。
3.5 ppt_convert.py / pptx_media.py(.ppt / .pptx)
PPT 系列没有独立解析器,builtin 与 markitdown 两个引擎都把它交给 MarkitdownParser,这两个模块是它的前后置助手:
ppt_convert.py:normalize_ppt_bytes按魔数判断(ZIP=pptx 直通;OLE=老式 ppt 则 LibreOfficeconvert-to pptx,独立 profile + 3 次重试)。无 LibreOffice 时对 .ppt 直接抛错并提示安装。pptx_media.py:MarkItDown 无法内联的 PPTX 媒体(尤其 WMF/EMF/SVG 矢量图)的补救——解包ppt/media/下所有资源,按顺序用 Pillow(位图)或 ImageMagickconvert(矢量,兜底一切格式)栅格化为 PNG,然后把 markdown 里未解析的引用按顺序替换为images/<uuid>.png并内联图片数据。
3.6 image_parser.py — ImageParser(独立图片文件)
最简单的解析器:不做任何 OCR。把整张图 base64 内联进 Document.images,正文只有一行 。图片的 OCR 与描述在 Go 侧由知识库配置的视觉模型完成(internal/application/service/image_multimodal.go,见 §4)。另外注意:Go 的 simpleFormats 已把图片格式收编为 Go 原生处理,只有知识库为图片显式选了 builtin 引擎时才会走到 docreader 的 ImageParser。
PaddleOCR-VL(paddleocr_vl_converter.go / paddleocr_vl_cloud_converter.go)与 MinerU 是另一回事:它们是可选的整文档解析引擎,为某类文件选中后替代 docreader 产出 Markdown,而不是给 builtin 路径做 OCR。
3.7 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 产出的假空行/分隔行前缀,并给无表头的 Word 表格补| --- |GFM 分隔行。MarkdownImageBase64:把内嵌图抽出为images/<uuid>.<ext>引用 +Document.images数据(MIME 子类型支持x-emf这类带连字符的格式)。
该解析器也是 MarkitdownParser / WebParser 流水线的公共后处理阶段。
3.8 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策略:内网/环回/link-local/云 metadata 域名、.local/.internal等后缀、直连 IP、IP-like 主机名、DNS 解析出的受限 IP、危险端口全部拦截;SSRF_WHITELIST/SSRF_WHITELIST_EXTRA环境变量放行)。 - SPA 支持:
domcontentloaded后等 networkidle(10s)+ 等#app/main/body 可见文本 ≥80 字符(15s),适配 JS 渲染页面。 - 微信公众号适配:monkey-patch trafilatura 内部
utils.IMAGE_EXTENSION(识别mmbiz.qpic.cn/...wx_fmt=无扩展名图片)与xpaths.BODY_XPATH(优先#js_content/.rich_media_content)。 - 回退:trafilatura 抽不出正文时用 Playwright 可见文本(≥50 字符)+ 页面 title 兜底。
- 代理走
DOCREADER_EXTERNAL_HTTPS_PROXY。metadata 提取title。
3.9 mhtml_parser.py — MHTMLParser(.mhtml 网页归档)
用标准库 email 解析 MIME 结构:收集全部 text/html part,选最大的非广告 part 作为正文(按 googleads/doubleclick 等域名黑名单过滤);image/* part 抽出为 images/...(优先用 Content-Location 文件名,冲突加 _2 后缀),并按 Content-Location/Content-ID(cid:)/X-Attachment-Id 的多种拼写(HTML 转义、URL 编码、basename、相对路径 urljoin)建立别名表回写 <img src>。HTML → Markdown 用 BeautifulSoup(去 script/style/noscript/iframe、unwrap 站内链接)+ markdownify,再做代码围栏感知的空行规范化。全部失败时退化为 ```html 代码块。metadata:source_format=mhtml、file_size、image_count。
3.10 html_parser.py — HTMLParser(.html / .htm 静态网页文件)
用户直接上传的 HTML 文件走这条链路,与 parse_url() 的在线抓取分开:HTMLParser = PipelineParser.create(HTMLToMarkdownParser, MarkdownParser)。
HTMLToMarkdownParser先用BeautifulSoup(content, "lxml")解码原始字节——先看 BOM 与 HTML 内的 charset 声明,再交给统一的 Markdown 转换;- 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 / caption 流程。
3.11 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>;EPUB 内部链接(章节间跳转、#fragment)unwrap 只留文本。ebooklib 失败时回退 ZIP 直读:按 chapter(\d+) 排序 html/xhtml 文件逐个转换。metadata 含 chapter_count/image_count。
3.12 markitdown_parser.py — MarkitdownParser(markitdown 引擎)
PipelineParser.create(StdMarkitdownParser, MarkdownParser)。StdMarkitdownParser 包装微软 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 可能卡死);表格/版面还原弱于 builtin PDF 路由。
3.13 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 图片引用。支持 hybrid 模式(DOCREADER_ODL_HYBRID=docling-fast 等):调用独立部署的 opendataloader-pdf-hybrid HTTP 服务(DOCREADER_ODL_HYBRID_URL,默认 http://127.0.0.1:5002,Docker 侧对应 docker/Dockerfile.odl-hybrid),可用性探针带重试(快速探测 2s×1 次;解析前探测 5s×6 次容忍服务冷启动)。产出文本 <20 字符时判定失败,回退 builtin 的 PDFScannedParser。可用性检查:java 在 PATH(需 Java 11+,镜像装的是 openjdk-17-jre-headless)+ Python 包已装 + hybrid 健康。
3.14 video_parser.py — VideoParser(视频)与 xmind_parser.py — XMindParser
VideoParser 只做媒体层面的重活,模型调用都留在 Go 侧:
- 用 ffprobe 读时长、分辨率、是否有音轨;抽出音轨(码率
DOCREADER_VIDEO_AUDIO_BITRATE,默认 64k),按DOCREADER_VIDEO_AUDIO_SEGMENT_SEC(默认 170 秒)切段回传,Go 侧用 ASR 模型逐段转写,每段自带起止时间,不支持时间戳的 ASR 厂商也能得到分段级时间线; - 先用 ffmpeg 场景检测(阈值
DOCREADER_VIDEO_SCENE_THRESHOLD=0.3)选关键帧,场景变化太少(例如静态的讲座录像)时退回固定间隔;帧间隔下限DOCREADER_VIDEO_MIN_FRAME_INTERVAL_SEC=5 秒、总数上限DOCREADER_VIDEO_MAX_FRAMES=200、长边DOCREADER_VIDEO_FRAME_MAX_EDGE=1280 px,防止超长视频产生上千个视觉模型任务;关键帧带毫秒时间戳,由 Go 侧视觉模型描述; - 正文只返回占位 Markdown,Go 侧用转写文本与关键帧描述重建带时间线的正文。
Go 侧对视频的前置检查:知识库既没有配置 ASR 模型也没有开启图像处理时,直接判解析失败并提示。本地存储部署下,视频经共享卷把路径交给 docreader,ffmpeg 原地读取,不经 gRPC 传字节。
XMindParser 把 XMind 归档(ZIP)里的画布与主题树展开成 Markdown 大纲,主题备注随主题输出;单个内容文件上限 32 MB。
3.15 解析器选择决策流程
4. 图片处理与多模态分工
docreader 侧的图片契约非常简单:每个解析器把图片以 Document.images = {"images/<文件名>": "<base64>"} 返回,markdown 正文中以  相对引用。
main.py 只有一条回传路径:ReadStream 里 _iter_image_refs() 把图片逐张 base64 解码成 ImageRef.image_data 内联字节、一帧一张 yield,边发边 pop 释放内存。图片持久化完全由 Go App 负责,写入配置的存储后端(本地或 S3 兼容存储)。
Go 侧接手后(internal/infrastructure/docparser/image_resolver.go):将 inline bytes 上传对象存储、把 markdown 中的 images/... 引用重写为存储 URL;随后 internal/application/service/image_multimodal.go 用知识库配置的视觉模型(VLMConfig)处理每张图:OCR 时 image_source_type=scanned_pdf 的整页图用专门的扫描件提示词、其余图用通用 OCR 提示词,并为图片生成描述;结果成为 image_ocr / image_caption 子分块。知识库没有开启图像处理时,这一步不执行,扫描页也就没有文字。docreader 内没有任何模型调用。
5. 分块不在 docreader
docreader 只把文件转成 Markdown 与图片引用,gRPC 响应里没有分块。分块全部在 Go 侧的 internal/infrastructure/chunker/ 完成(标题、启发式、递归三级策略与表头前置),见分块策略。
6. 配置项全表
6.1 config.py(DocReaderConfig,启动时打印生效值)
| 环境变量(别名) | 默认值 | 说明 |
|---|---|---|
DOCREADER_GRPC_MAX_WORKERS(GRPC_MAX_WORKERS) | 4 | gRPC 线程池并发数 |
DOCREADER_GRPC_MAX_FILE_SIZE_MB | max(MAX_FILE_SIZE_MB, 512)(MB) | gRPC 收发消息的传输硬上限。它不是上传限额,Go 客户端用同一规则,两端须一致 |
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 hybrid 模式(如 docling-fast) |
DOCREADER_ODL_HYBRID_URL | http://127.0.0.1:5002 | hybrid 服务地址 |
DOCREADER_ODL_HYBRID_MODE | auto | hybrid 模式参数 |
DOCREADER_ODL_HYBRID_FALLBACK | false | hybrid 失败是否回退 |
DOCREADER_ODL_MARKDOWN_WITH_HTML | false | ODL markdown 允许 HTML |
DOCREADER_PDF_RENDER_MAX_WORKERS | 1 | PDF 渲染阶段限流(跨请求) |
DOCREADER_PDF_RENDER_PARALLELISM | min(4, cpu) | 单个 PDF 内扫描页渲染的 worker 进程数 |
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 | 临时图片目录(local 模式回退用,当前主链路不写盘) |
DOCREADER_SHARED_DATA_DIR(LOCAL_STORAGE_BASE_DIR) | /data/files | 与 app 共享的存储卷挂载点;file_path 请求必须落在其下(两容器路径须一致) |
DOCREADER_VIDEO_SCENE_THRESHOLD | 0.3 | 关键帧场景检测阈值 |
DOCREADER_VIDEO_MIN_FRAME_INTERVAL_SEC / _MAX_FRAMES / _FRAME_MAX_EDGE | 5 / 200 / 1280 | 关键帧最小间隔(秒)/ 上限张数 / 长边像素 |
DOCREADER_VIDEO_MAX_DURATION_SEC | 0(不限) | 视频时长上限,超过则解析失败 |
DOCREADER_VIDEO_FFMPEG_TIMEOUT_SEC | 1800(compose 设为 3600) | 单次 ffmpeg 调用超时 |
DOCREADER_VIDEO_AUDIO_BITRATE / _AUDIO_SEGMENT_SEC | 64k / 170 | 抽出音轨的码率 / 分段时长(秒) |
6.2 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 | 从 text 页抽取嵌入插图 |
DOCREADER_PDF_EMBED_MIN_PIXELS / _EMBED_MIN_AREA_RATIO / _EMBED_REPEAT_PAGE_FRAC / _EMBED_MAX_IMAGES | 80 / 0.01 / 0.5 / 50 | 嵌入图过滤:最小边长 / 页面积占比 / logo 判定重复率 / 每文档上限 |
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_WORD_GAP_WIDTH_RATIO / _MARGIN_COL_WIDTH_RATIO / _MIN_HEADING_LINE_CHARS 等 | 0.4 / 0.12 / 8 | 版面重建微调参数(详见源码常量区) |
6.3 安全与其他
| 环境变量 | 说明 |
|---|---|
GRPC_AUTH_TOKEN | 设置后启用 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) |
LOG_LEVEL | 日志级别(默认 INFO;日志格式含 request_id 与耗时,见 utils/request.py) |
LIBREOFFICE_PATH / ANTIWORD_PATH | soffice / antiword 可执行文件路径覆盖 |
7. 部署与扩容建议
7.1 镜像与系统依赖(docker/Dockerfile.docreader)
基础镜像 python:3.10.18-bookworm,双阶段构建(builder 用 uv sync --locked 装依赖 + scripts/generate_proto.sh 生成 pb 代码;runner 拷贝 venv),EXPOSE 50051,CMD ["uv", "run", "-m", "docreader.main"]。运行阶段系统依赖:
- LibreOffice(doc→docx、ppt→pptx、异常表格→xlsx 转换)+ 一串 X/字体库(libxinerama1、libfontconfig1、libcairo2、libcups2 等);
- antiword(.doc 纯文本兜底);
- openjdk-17-jre-headless(OpenDataLoader PDF 需要 Java 11+);
- ffmpeg(视频:抽音轨、抽关键帧);
- 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 栅格化(属可选增强)。
服务以非 root 用户 docreader(uid 10001)运行,代码与依赖只读:它解析的是不可信的用户文档,LibreOffice、ffmpeg、WebKit、PDF 库出漏洞时不该拿到 root。
scripts/ 下另有两个工具:generate_proto.sh(grpc_tools.protoc 生成 Python/Go 代码并修复 import 路径)与 parse_local.py(本地直接调 Parser 调试解析结果,不经 gRPC,支持 --engine、--scanned、--out 导出 markdown 与图片)。
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(已禁用路径)等。
7.2 扩容与调优
- 水平扩展优先:pdfium 全局锁使单实例内 PDF 解析串行,PDF 吞吐主要靠多副本扩展。Go 客户端 dial
dns:///+round_robin,K8s 下用 headless service 即可让多副本均衡分流。 - 单实例纵向调优: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,均为重进程;
DOCREADER_ODL_MAX_WORKERS、DOCREADER_MARKITDOWN_MAX_WORKERS默认 1 是保守值,资源充足可放宽或设 ≤0 关闭限流。ODL hybrid 服务(Dockerfile.odl-hybrid)应独立部署并配置DOCREADER_ODL_HYBRID_URL。 - 超时保护:单次调用超时
YUHENG_DOCREADER_CALL_TIMEOUT(默认 30 分钟)防止挂死的 docreader 长时间占用入库 worker;处理超大扫描件或长视频时,它和YUHENG_DOCUMENT_PROCESS_TIMEOUT(默认 2 小时)可能需要一起调大,前者须小于后者。 - 安全基线:
docker-compose.yml不把 50051 端口发布到宿主机,只在内部网络供 app 调用,默认不鉴权。docreader 需要从其他机器访问时,开启GRPC_AUTH_TOKEN(≥16 字节,app 与 docreader 两边设同一个值;设成示例值或过短时 app 拒绝启动)+GRPC_TLS_ENABLED;不设置时服务会以明文 + 无鉴权模式启动并打印 WARNING。
8. anydoc 引擎(Go 进程内解析,不经 docreader)
anydoc 是一个 Go 侧的可选解析引擎:它把 anydoc(Rust 编写的文档转换库)通过 cgo 链接进 Yuheng 主进程,直接把 office 文档转成 Markdown。与本文其余部分描述的 docreader 不同,它不经过 Python 服务、不跨进程、也不调用外部二进制——适合不想部署 docreader 的轻量部署,或对解析延迟敏感的场景。
支持的文件类型:doc、docx、docm、odt、rtf、ppt、pptx、pptm、odp、xls、xlsx、xlsm、ods、epub、csv、pdf。
8.1 启用方式
解析库是 Rust 静态库,需要 Rust 工具链构建。用本仓库的 docker/Dockerfile.app 构建镜像(docker compose build、make 的镜像目标)默认链接 anydoc,设置页可直接选用。本地 go build 默认不链接:未加 -tags anydoc 时该引擎在「解析引擎」列表里显示为不可用,其它引擎不受影响。
make build-anydoc # 构建静态库 + 带 anydoc 标签的二进制
# 等价于:
scripts/build-anydoc-lib.sh && go build -tags anydoc ./cmd/serverDocker 镜像默认 WITH_ANYDOC=1。若要跳过 Rust 工具链、缩短构建:
docker build -f docker/Dockerfile.app --build-arg WITH_ANYDOC=0 -t yuheng-app .
# 或在 .env 里设 WITH_ANYDOC=0 再 docker compose build启用后在知识库的解析设置里把对应文件类型指向 anydoc 引擎即可。
8.2 能力边界
- 扫描件 PDF:anydoc 只抽取 PDF 的文字层。没有文字层的扫描件会报「需要 OCR」;若 DocReader(builtin)已连接,AnydocReader 会自动把该文件交给 builtin,按页渲染 JPEG 并标记
image_source_type=scanned_pdf,后续仍走 Go 侧 OCR。未连接 DocReader 时转换失败,请改用builtin、mineru或paddleocr_vl。 - 纵向合并单元格不回填:docreader 的
Docx2Parser会把纵向合并的值复制到每一行,anydoc 只在起始行输出该值,后续行留空。对依赖表格逐行语义的知识库,builtin仍然更稳。 - 图片位置:开启图片抽取时,先把文档模型里的嵌入图改写成
images/image-N.ext链接,再交给 anydoc 官方 GFM 序列化,因此图片会留在原段落/表格/列表位置。设置引擎覆盖参数anydoc_extract_images=false可关闭图片抽取,走更快的纯文本渲染(嵌入图会退化成 alt 文本)。 - 不处理 URL、图片、音频:这些仍由
WebParser、SimpleFormatReader与 ASR 链路负责。
8.3 代码位置
| 路径 | 作用 |
|---|---|
internal/infrastructure/docparser/anydoc/ | 适配层:格式映射、可用性判断,以及 cgo / stub 两个后端 |
internal/infrastructure/docparser/anydoc_reader.go | DocReader 实现:转换结果与图片引用组装 |
internal/infrastructure/docparser/engines.go | 引擎注册(元数据 + Reader 工厂) |
third_party/anydoc-go/ | vendored 的上游 Go 绑定与 C ABI shim(来源与本地改动见该目录 README) |
附:关键事实速查
- 对外接口:仅 gRPC,端口
50051(DOCREADER_GRPC_PORT/PORT),RPC:Read/ReadStream/ListEngines+ 标准 Health 服务。 - docreader 直接支持的文件格式全集:
pdf、docx、doc、xlsx、xls、pptx、ppt、md/markdown、epub、html/htm、mhtml、xmind、图片jpg/jpeg/png/gif/bmp/tiff/webp、视频(需 ffmpeg),以及 URL 网页抓取(markitdown 引擎额外含csv);txt/csv/json/图片/音频在未指定引擎时由 Go 侧SimpleFormatReader原生处理,不经过本服务。 - OCR / VLM / ASR:docreader 内部不调用任何模型;扫描页、插图、视频关键帧作为图片回传,由 Go 侧视觉模型做 OCR 与描述,音频与视频音轨由 Go 侧 ASR 模型转写。MinerU、PaddleOCR-VL 是可选的整文档解析引擎,不是 builtin 路径的 OCR。
- 图片回传:inline bytes(
ImageRef.image_data),持久化到 local/s3 由 Go 负责。 - 分块:全部在 Go 侧 chunker,docreader 不分块。
实现参考
想读源码时按下表定位(路径相对仓库根目录):
| 层 | 文件 |
|---|---|
| gRPC 服务入口 | docreader/main.py |
| 协议定义 | docreader/proto/docreader.proto |
| docreader 配置 | docreader/config.py |
| 认证与 TLS | docreader/auth.py(服务端)、docreader/client/auth.go(Go 客户端) |
| 解析器注册表与门面 | docreader/parser/registry.py、docreader/parser/parser.py |
| 各解析器 | docreader/parser/*_parser.py |
| Go 侧引擎注册与路由 | internal/infrastructure/docparser/engines.go、engine_registry.go |
| Go 侧 docreader 客户端 | internal/infrastructure/docparser/grpc_parser.go、http_parser.go |
| Go 原生格式处理 | internal/infrastructure/docparser/builtin_converter.go |
| 远程解析引擎 | internal/infrastructure/docparser/mineru_*.go、paddleocr_vl_*.go |
| 图片持久化与引用重写 | internal/infrastructure/docparser/image_resolver.go |
| 图片 OCR 与描述 | internal/application/service/image_multimodal.go |
| 上传白名单 | internal/application/service/knowledge_util.go |
| 入库流水线调用点 | internal/application/service/knowledge_process.go、knowledge_process_config.go |
| 镜像 | docker/Dockerfile.docreader、docker/Dockerfile.odl-hybrid |