Aligns the Go EML parser (`internal/parser/parser/email_parser.go`) with the Python flow parser (`rag/flow/parser/parser.py:_email`) on two structural points so the Go path is a faithful drop-in for the Python path on `.eml` inputs.
27 KiB
Parser 层 Go ↔ Python 一致性对齐 Handoff 文档
用途:多会话并行处理「Go 与 Python 在 Parser 层是否对同一文件产出相同结构」的核对与收敛工作。 本文档自包含,新会话只需读本文即可独立开工,无需回看原对话。 范围:排除 PDF 解析器(PDF 走独立原生后端
internal/deepdoc/parser/+pdf_vision_dispatch.go,不在此 scope)。
0. 目标与验收标准
目标:确认 DSL File → Parser → Chunker → Extractor → Tokenizer 中「Parser」这一独立阶段,在 Go 与 Python 两端对相同输入文件是否产出相同结构。
验收标准("相同结构"的定义):对同一份输入文件,
- 两端 Parser 阶段的输出条目数量(item 数)应一致;
- 每条目的关键字段与取值应一致(至少
text/doc_type_kwd一致;若带ck_type/image等,约定以某一端为准对齐); - 文本切分边界(段落 / 块 / 表格)应一致。
重要:这里的「Parser 阶段输出」指
- Go:
ParseResultProducer.ParseWithResult(...)返回的ParseResult(字段JSON/Markdown/Text/HTML+File); - Python(flow):
rag/flow/parser/parser.py里Parser._invoke→set_output产出的sections列表(每项形如{text, doc_type_kwd, image?, ck_type?})。
1. 关键定位纠正(务必先读)
正确的 Python 对应物是 rag/flow/parser/parser.py,不是 deepdoc/parser/* + rag/app/naive.py。
- 原因:你给的 DSL 是分阶段的,Go 端
internal/parser/parser/*的移植注释明确引用rag/flow/parser/parser.py:_invoke与set_output(internal/ingestion/component/parser.go:392注释)。Go 的ParseResult契约移植自port-rag-flow-pipeline-to-go.md §4.2/§6.5。 deepdoc/parser/*+rag/app/naive.py是 naive 摄取路径,它把 parser+chunker+tokenizer 融合在一个chunk()里,不是独立 Parser 阶段的对应物。但注意:flow 解析器内部又委托了 naive 解析器(_markdown→naive.Markdown,_html→HtmlParser,_docx→naive.Docx),所以 naive 解析器是底层实现,阶段边界仍以rag/flow/parser/parser.py为对齐面。
架构根因(必须在任何收敛方案里承认):
- Go 把 Parser 当作干净的「未切块」阶段:
ParseResult→ 重塑成schema.Page(internal/ingestion/component/parser_dispatch.go的jsonItemsToPages/buildParserOutputs)→ 交给下游 Chunker 才做 token 切块。 - Python flow 解析器复用了 naive 解析器,而 naive 解析器(html/txt/epub 尤其)在 parser 内部就做了 token 切块。因此即便输出 key 名一致,parser 阶段的粒度已经不同。
2. 类型 ↔ 实现映射与一致性总览
| 类型 | Go 实现 | Python(flow) 实现 | 默认输出格式 | 一致性 | 风险等级 |
|---|---|---|---|---|---|
| markdown | internal/parser/parser/markdown_parser.go |
rag/flow/parser/parser.py:_markdown → naive.Markdown |
json | ❌ 不一致 | 高 |
| html | internal/parser/parser/html_parser.go |
rag/flow/parser/parser.py:_html → HtmlParser |
json | ❌ 不一致 | 高 |
| text&code | internal/parser/parser/text_parser.go |
rag/flow/parser/parser.py:_code → TxtParser |
json | ❌ 不一致 | 高 |
| docx | internal/parser/parser/docx_parser.go(office_oxide) |
parser.py:_docx → naive.Docx(python-docx) |
json | ⚠️ 结构类似,引擎不同 | 中 |
| xlsx/xls/csv | xlsx_parser.go / xls_parser.go / csv_parser.go |
parser.py:_spreadsheet → ExcelParser |
html | ⚠️ 默认 html,待细核 | 中 |
| pptx/ppt | parser/pptx_parser.go(office_oxide) |
parser.py:_slides → RAGFlowPptParser |
json | ⚠️ 引擎不同 | 中 |
| epub | parser/epub_parser.go |
parser.py:_epub → EpubParser |
json | ✅ 基本一致 | 低 |
parser/email_parser.go |
parser.py:_email |
json/text | ✅ 基本一致 | 低 | |
| doc | parser/doc_parser.go(office_oxide) |
parser.py:_doc(tika) |
json | ❌ 格式不同 | 中 |
| json | parser/json_parser.go |
flow 无对应分支 | json | — Go 独有 | — |
2.1 Go 端 Parser 的 item 模型(对拍基础)
对拍时反复提到的「item」在 Go 端有精确定义,先统一术语,避免与下游 schema.Page 混淆。
item 是什么
- item =
ParseResult.JSON里的一个map[string]any元素(internal/parser/parser/parse_result.go:48),代表从文件解析出的一个逻辑单元(块 / 段落 / 表格 / 图片 / 行 / 幻灯片…)。 - 关键字段:
text:该单元文本(表格单元里是 HTML 字符串)doc_type_kwd:类型,取值text/image/table- 可选
ck_type:更细分类(heading/paragraph/list/code/quote/table/image) - 可选
image:base64 图片;page_number:页码
- 仅当
OutputFormat=="json"时才存在 item 列表。markdown/text/html 输出格式的解析器产出的是单个字符串(ParseResult.Markdown/Text/HTML),reshape 后变成 1 个schema.Page,没有 JSON item。 - 下游 dispatch(
internal/ingestion/component/parser_dispatch.go):OutputFormat=="json"→ 每个 item 一个 page(jsonItemsToPages);markdown/text/html→ 整段作 1 个 page;未知 → 按\f切页。所以「item 数」对比只对 json 输出格式有意义。
item 数量(因格式与输入而异,无固定值)
| 格式 | OutputFormat | item 数量 |
|---|---|---|
| markdown | json | = 顶层块数;整篇含表时塌缩成 1 个(markdown_parser.go:99-107);空输入补 1 个占位 |
| html | json | = 有文本的块级元素数(walkHTMLBlocks 每块 1 项) |
| text&code | json | = 非空段数(按 \n\n 切;超 8192B 就近再切) |
| docx | json | = section/element 数(段落/标题/图/表各自成项) |
| xlsx/xls/csv | 默认 html | 0 个 JSON item(单个 HTML 字符串);走 json 路径才按行成项 |
| pptx/ppt | json | = 幻灯片数(按 \f 分页) |
| epub | json | = spine 条目数 |
| json/text | json 时 1 个(单个字典项,含 from/to/subject/body…) | |
| json | json | = 逻辑记录数(数组每元素 / 对象 / JSONL 每行) |
item 类型(按 doc_type_kwd,共 3 类;全 Go 解析器 JSON 输出一致)
text—— 绝大多数:段落、标题、列表、代码块、单元格文本、行文本、幻灯片文本、email 正文等。docx 标题项额外带ck_type:"heading"。image—— 图片,必带image(base64)。来源:markdown 的解析(markdown_parser.go:287-292,含 HTTP 抓取+SSRF 防护);docx 内嵌图。table—— 表格,text字段是 HTML 表格字符串。来源:docx 表格(docx_ir.go)、markdown 内联表(未整篇塌缩时)。
- 更细一层的
ck_type词汇(仅 markdown 等部分解析器输出):heading/paragraph/list/code/quote/table/image。Python flow 的set_output不带ck_type,是字段契约差异点(见 §3.1)。
对照 Python(可比对维度)
- Python flow
set_output的 json 路径产出同形状[{text, doc_type_kwd, image?, ...}],因此「item 数 + 每 item 的text/doc_type_kwd」是两端可直接对拍的维度(即 §5 逐项/计数比对层)。 - 例外:xlsx/csv 在 Go 默认 html(0 个 json item),Python
_spreadsheet默认也是 html(ExcelParser().html)→ 两端都是单字符串,item 维度均为 0,比对时按 HTML 字符串走。
2.2 Schema 模型(契约信封 / 数据结构)
§2.1 描述的是 item(JSON 列表里的元素)。本节描述「装着 item 的信封」与组件间契约——即 Parser 输入输出、配置、以及 Parser→Chunker 的桥接结构。两端刻意镜像(Go schema/parser.go 注释多处写明 "mirrors rag/flow/parser/schema.py")。
Go 端
ParseResult(internal/parser/parser/parse_result.go:48):解析器统一返回契约。- 字段:
OutputFormat(json/markdown/text/html)、File(map)、JSON([]map[string]any)、Markdown/Text/HTML(string)、Err。 - 仅
OutputFormat=="json"时JSON填充(即 §2.1 的 item 列表)。
- 字段:
schema.Page(internal/ingestion/component/schema/parser.go:58):Page = map[string]any,Parser 把JSON重塑后交给下游 Chunker 的载体(json→每 item 一 page)。注释明确:它故意保持 Pythondict形态,字段形如{text, doc_type_kwd, ck_type?, image?, page_number?, positions?}(parser.go:52-57)。ParserFromUpstream(parser.go:29-41):Parser 组件输入(name/file/abstract/author/created_time/elapsed_time)。注释写明 mirrors PythonParserFromUpstream。ParserOutputs(parser.go:110-136):Parser 组件输出契约。字段output_format+ 四选一(json/markdown/text/html)+file+_ERROR。直接镜像 Pythonset_output的写入面。ParserSetup/ParserParam(parser.go:64-94):每文件类型的配置块 + 静态配置(含AllowedOutputFormat白名单)。Defaults()注释写明逐字复制自 PythonParserParam.__init__。ChunkDoc(下游,internal/ingestion/component/schema/chunk_types.go):Chunker 产出结构(text/content_with_weight/doc_type_kwd/ck_type/image/page_number/positions…)。属 Parser 之后的阶段,仅作上下文,不在本 scope。
Python 端(flow)
ParserFromUpstream(rag/flow/parser/schema.py:18-26,Pydantic):输入契约。字段与 Go 完全一致(created_time/elapsed_time/name/file/abstract/author,populate_by_name、extra="forbid")。- Parser 输出面(
rag/flow/parser/parser.py内set_output):无单一命名 model,但写入键固定为output_format+ 四选一(json/text/markdown/html)+file+_ERROR→ 与 GoParserOutputs一一对应。json 路径写set_output("json", [...])(即 §2.1 的 item 列表)。 TokenChunkerFromUpstream(rag/flow/chunker/schema.py:20-35,Pydantic):Parser→Chunker 的桥接契约,Chunker 消费它。- 字段:
output_format(json/markdown/text/html/chunks)、json_result(别名json)、markdown_result/text_result/html_result、chunks、name/file/created_time/elapsed_time。 - 即 Chunker 读
from_upstream.output_format与from_upstream.json_result(见token_chunker.py:313-345)。这正是 GoParseResult.JSON/schema.Page在 Python 侧的对应物。
- 字段:
- Chunker 输出:
TokenChunker.set_output("chunks", [{"text", "doc_type_kwd", ...}])(token_chunker.py:440_finalize_json_chunks)→ 对应 Go 的ChunkDoc。
跨端结构映射表
| 概念 | Go | Python(flow) |
|---|---|---|
| 解析器返回 | ParseResult(parse_result.go:48) |
(隐式)set_output 写入面 |
| 组件输入 | ParserFromUpstream(schema/parser.go:29) |
ParserFromUpstream(parser/schema.py:18) |
| 组件输出 | ParserOutputs(schema/parser.go:110) |
set_output:output_format+json/text/markdown/html+file+_ERROR |
| item 载体 / 页 | schema.Page = map[string]any(schema/parser.go:58) |
TokenChunkerFromUpstream.json_result(chunker/schema.py:30) |
| 配置块 | ParserSetup / ParserParam(schema/parser.go:64-94) |
ParserParam.setups / allowed_output_format(parser.py:71-...) |
| Chunker 输入 | schema.Page 列表 |
TokenChunkerFromUpstream(chunker/schema.py:20) |
| Chunker 输出 | ChunkDoc(chunk_types.go) |
chunks: [{"text","doc_type_kwd",...}](token_chunker.py:440) |
对齐要点:结构信封两端已高度镜像(ParserFromUpstream/ParserOutputs/ParserParam 是逐字对应),所以「对拍」的重点不在信封,而在信封内的 item 内容/粒度/字段(§2.1 + §3)。唯一需留意的是 TokenChunkerFromUpstream 还有 chunks 字段——当上游已是 chunks 时 Chunker 直接透传,这是 Python 在 Parser 之后、Chunker 之前可能已被预切块的另一证据(关联 §3.2 的切块归属讨论)。
2.3 已确认的对齐决策(全局,2026-08-07)
以下为已拍板、跨类型生效的决策,各并行会话直接遵循,无需再议:
- 切块所有权:保留现状。不做 pipeline 重构——Go Parser 仍不切块、Python flow Parser 仍内嵌 token 切块(html/epub 等)。不在「把切块挪到 Chunker 统一」上做改动。
- 对比方法:采用拼接对比(stitch-compare)。以「拼接后内容等价」作为一级闸门(见 §5),item 数 / 字段比对作为二级。验收重点是「两端 Parser 提取出相同内容」,而非 item 边界一致。
- 块抽取粒度基准:以 Python 为准。逐块边界(段落 / 块 / 表 / 列表续行等)的对齐目标是让 Go 收敛到 Python 的块边界;Python 现状视为事实基准。
- FIXME 保留不动。
deepdoc/parser/html_parser.py:236处rag_tokenizer.tokenize身份未确认,暂不调查。 - doc / docx / pptx / xlsx 引擎差异:保留(接受差异)。Go 用
office_oxide、Python 用 python-docx / pptx / excelize / tika,文本与分段发散属可接受分歧,记录原因即可。 - json:Python flow 不补分支。Go
json_parser.go为 Go 独有,Python 侧声明 flow 不支持.json,接受此不对称。
3. 已核实的具体分歧(带 file:line,供新会话直接验证)
3.1 Markdown(最严重)
- 表格塌缩:Go
markdown_parser.go:99-107中renderMarkdownTablesInline只要文档出现任意一张 GFM 表格,就把整篇重写为单个 item{"text": rendered, "doc_type_kwd":"text"}(函数见:136-177,changed为真即整篇返回)。Pythonseparate_tables=False(parser.py:1080)只把表格内联成 HTML 块,但仍按块切成多个 section。→ 同输入,Go 可能 1 个大 item,Python 是 N 个。 - ck_type 字段:Go 输出带
ck_type(heading/paragraph/list/code,markdown_parser.go:278-280);Python_markdown的set_output输出不含ck_type(parser.py:1089-1101)。→ 字段形状不同。 - delimiter 切分:Go markdown 解析器不做 delimiter 切分;Python 把
conf["delimiter"]传入 naive 解析器做二次切分(parser.py:1081,底层deepdoc/parser/markdown_parser.py:167-174, 305-345)。→ 配置 delimiter 时行为不同。 - 图片:Go
resolveMarkdownImage把内联成 base64 +doc_type_kwd:"image"(:287-292,含 HTTP 抓取与 SSRF 防护:427-457);Python 走return_section_images=True+ vision 增强(parser.py:1081-1088附近),机制不同但目标类似。
3.2 HTML(粒度本质不同)
- 切块时机:Go
html_parser.go的walkHTMLBlocks(:109-136)每个块级元素产 1 个 item,不切块。Python_html调HtmlParser()(name, blob, int(conf.get("chunk_token_num", 512)))(parser.py:1154,阈值来自self._param.setups["html"]["chunk_token_num"],配置驱动、默认 512;html 族默认 setupparser.py:188-193未设该 key,默认部署下即 512)。HtmlParser.chunk_block用rag_tokenizer.tokenize的真实 token 数累加、超阈值即切(deepdoc/parser/html_parser.py:232-237, 287-313)。→ Go 是原始块,Python 已切成 token 块。注意text&code的同类阈值默认是 128(parser.py:1135),两族默认值不同。 - 表格表示:Go 把单元格折叠进文本并标
ck_type:"table"(html_parser.go:169-174);Python 把<table>原样抽成独立 item(html_parser.py:75-76)。→ 表示方式不同。 - 两者都做 CSS 空白折叠与
<br>硬换行(html_parser.go:179-300↔html_parser.py:169-178),这部分是对齐的。
Python flow 管线后续仍走独立 Chunker(TokenChunker,rag/flow/chunker/token_chunker.py),但默认不是二次切分:_html 产出的 sections 已是 512-token 块(parser.py:1157-1159 写入 json_result),Chunker 的 json 分支(token_chunker.py:344-442)默认 delimiter_pattern 为空(只认反引号自定义分隔符,:70-76),_build_json_chunks 不把 section 切更小,仅 _merge_text_chunks_by_token_size(:432-434,阈值默认 512)做相邻合并、绝不分裂 → 默认近似透传。两处隐患((a) 待核实 / FIXME):(a) Parser 与 Chunker 的 token 计数口径是否一致 —— 待核实:TokenChunker 用 num_tokens_from_string(common/token_utils.py:126-132),底层是 tiktoken.get_encoding("cl100k_base")(common/token_utils.py:45,OpenAI GPT BPE)—— 此点已确认。HtmlParser._token_count 用 rag_tokenizer.tokenize(html_parser.py:236),但 rag_tokenizer.tokenize 实际解析到哪个分词器尚未确认:原先假设为 infinity.rag_tokenizer.RagTokenizer 已确认不正确(见 html_parser.py 内 FIXME 注释)。因此「两套分词器不一致」这一结论目前不成立/待证,需先确认 rag_tokenizer.tokenize 的真实实现,再判断 Parser/Chunker 是否同一 tokenizer、以及「512 token」前后含义是否一致。(b) 若 Chunker 配了自定义分隔符,_build_json_chunks 会把已切好的 section 内部再切(:120-121)= 真正的二次切分。收敛建议:先确认 Parser 切块用的 rag_tokenizer.tokenize 真实身份(FIXME),再决定是否需要统一口径,并让 _html 在 Parser 阶段停止预切块、统一交给 Chunker,与 Go 对齐。收敛建议:让 Python _html 在 Parser 阶段不再 token 切块(只产出原始块,切块交给 Chunker 统一做),与 Go 对齐;注意 HtmlParser 被 naive 路径复用,应在 flow _html 调用处跳过 chunk_block 而非改 HtmlParser 本身。
3.3 Text / Code
- Go
text_parser.go:146-175仅按空行\n\n分段,超 8192 字节就近按\n再切,无 delimiter、无 token 统计。 - Python
_code调TxtParser(..., delimiter, keep_delimiters=True)再merge_paragraphs(OVER_CAP)token 合并(deepdoc/parser/txt_parser.py:46-63)。→ 段落切分完全不同。 - 注意:Go 文件头注释(
text_parser.go:17-31)自承比 Python 更简单,因为"无生产模板依赖更丰富结构"。
3.4 docx / pptx / xlsx / doc(引擎差异)
- 这四类 Go 用
office_oxide原生库;Python 用python-docx/python-pptx/excelize(等价物)/tika。引擎不同 → section 数量与文本本身就会不同,即便 JSON 包裹形状类似。这是"相同结构"的根本风险点。 - docx JSON 包裹形状两侧都是
{text, image?, doc_type_kwd}+ 表格单独{text, doc_type_kwd:"table"},且 Go 额外带ck_type:"heading"(docx_ir.go:buildDOCXJSONSections附近);结构基本对齐,但文本来源不同。 - doc 特例:Go
doc_parser.go输出OutputFormat:"text"(office_oxide.PlainText)单字符串;Python_doc配置output_format:"json",产出行列表{"text": line, "doc_type_kwd":"text"}(parser.py_doc分支)。→ 输出格式都不同。
3.5 epub / email
epub —— 不一致(中/高风险,与 HTML §3.2 同根因,非"基本一致")
- 切块粒度:Go
extractEPUBTextItems(internal/parser/parser/epub_parser.go:197-210)每个 spine 文档产 1 个 item,整篇纯文本,不切块。Python_epub(rag/flow/parser/parser.py:1374-1389)→EpubParser(=deepdoc/parser/epub_parser.py:RAGFlowEpubParser,deepdoc/parser/__init__.py:18别名)→ 对每个 spine 文档调用RAGFlowHtmlParser(item_path, binary=html_bytes, chunk_token_num=512)(epub_parser.py:66),按 512 token 切块。→ 同输入 item 数:Go = spine 数 N;Python = 数十~上百 token chunk。必然不一致(小文档每 spine <512 token 时可能巧合一致,但粒度能力已不同)。 - 文本提取机制:Go 用正则
stripHTMLTags(:245-277)crude strip;Python 用RAGFlowHtmlParser结构化解析(表格/块级)。→text内容本身也会不同。 - 降级方案:把 epub 并入会话 B(html)一并处理,根因都是"Python 在 Parser 阶段内嵌 token 切块"。
email —— 结构接近一致(单 item),但有 4 处分歧
-
.msg支持:Python 走extract_msg.Message(parser.py:1316-1349);Go 显式报错(email_parser.go:79-83)。→ 输入.msg时 Go 直接报错、Python 出内容。能力缺口。
-
- metadata 收集逻辑:Python 无条件建
metadata字典(:1253),非基本 6 字段的 header 全部进 metadata,且不受metadata是否在 fields 限制;基本 6 字段(from/to/cc/bcc/date/subject)若不在 target_fields 则被整体丢弃(:1255-1261)。Go 仅在target["metadata"]为真时才挂 metadata,且非基本 header 受其门控(:164-180)。→ 同 header、不同 fields 配置下 dict 形状不同。
- metadata 收集逻辑:Python 无条件建
-
- 附件 payload 解码:Python
payload.decode(part.get_content_charset())无 fallback 链(:1306),charset 缺失/失败即抛异常;Go 有完整 fallback 链 utf-8→gb2312→gbk→gb18030→latin1(:345-361)。→ 多编码附件下 Go 更稳健,结果可能不同。
- 附件 payload 解码:Python
-
text_html空值处理:Python 总设text_html(可空串,:1296);Go 仅非空时设(:192-194)。→ 字段存在性差异(JSON 形状)。
3.6 JSON(Go 独有)
- Go 有
json_parser.go(数组/对象/JSONL 判别,:85-200);Python flowfunction_map/setups无 json 分支,flow 不处理.json。→ 若需对称,Python 侧要么补分支,要么明确"flow 不支持 json",二选一并记录决策。
4. 建议的并行拆分(每个会话认领一块)
按风险等级与依赖关系,建议如下拆分(各会话独立、互不冲突,最后统一回本文档的"对齐决策表"):
| 会话 | 认领类型 | 任务 |
|---|---|---|
| A | markdown | 以 Python 为准对齐块边界(表格塌缩 / ck_type / delimiter);切块保留现状;用拼接对比验证内容等价;写对拍测试 |
| B | html(+epub) | 以 Python 为准对齐块边界与表格表示;切块保留现状(不重构);拼接对比验证内容等价;写对拍测试 |
| C | text&code | 以 Python 为准对齐 delimiter / 合并策略的块边界;切块保留现状;写对拍测试 |
| D | docx/pptx/xlsx/doc | 引擎差异保留(接受);记录文本/分段发散原因;补基线测试 |
| E | epub/email/json | epub 并入 B;email 小修(Go 对齐 Python);json 接受 Go 独有(Python 不补分支);补基线测试 |
5. 每个会话的标准开工步骤(复现法)
- 取样本:为该类型准备 2-3 份代表性样本(含边界:表格、长段落、嵌套列表、图片引用、多 sheet 等)。
- 跑 Go:构造调用
parser.GetParser(fileType).ParseWithResult(ctx, name, data),打印ParseResult.JSON/Text/HTML的 item 数与内容。 - 跑 Python:调用
rag/flow/parser/parser.py的Parser._invoke(或对应_xxx方法 +set_output),打印sections的 item 数与内容。 - 拼接对比(一级闸门,优先做):把两端 items 的
text按 item 顺序拼成一段,归一化(统一换行、折叠空白、strip;更稳可再转 token 多集合)后比较。相等即「内容等价」——这是核心验收(决策见 §2.3:切块保留现状,验收重点是内容一致而非 item 边界)。- 图片/表格项无
text(或表示不同),需单独比 item 数与存在性,不计入文本拼接。
- 图片/表格项无
- 逐项 / 计数比对(二级):在拼接等价基础上,再比 item 数、
text、doc_type_kwd、ck_type、image是否存在/取值,定位剩余边界差异。粒度基准以 Python 为准(§2.3):Go 的块边界应收敛到 Python。 - 记录决策:在本文档 §6「对齐决策记录表」对应行写明结论与关键决策(以 Python 为准 / 接受差异+原因)。
- 补测试:在对应包加对拍测试(Go 端见
internal/parser/parser/*_test.go现状;Python 端可在rag/flow/parser/或 test 目录),测试应以拼接对比为断言主体。
注意 Go 构建依赖://go:build cgo 的文件(docx/xls/xlsx/ppt/doc)需要 office_oxide 原生库,用 bash build.sh --test ./internal/parser/parser/... 跑,勿直接用裸 go test(会缺 CGO 标志)。详见 AGENTS.md「Go Test Tiers」。
6. 对齐决策记录表(各会话回填)
| 类型 | 结论(一致/以X为准/接受差异) | 关键决策 | 测试位置 | 负责人/会话 |
|---|---|---|---|---|
| markdown | 以 Python 为准(粒度) | 切块保留现状;块边界 Go 收敛到 Python;拼接对比为一级闸门 | 待补对拍测试(会话 A) | A |
| html | 以 Python 为准(粒度) | 切块保留现状;块边界/表格表示以 Python 为准;拼接对比验证内容等价 | 待补对拍测试(会话 B) | B |
| text&code | 以 Python 为准(粒度) | 切块保留现状;delimiter/合并策略对齐 Python | 待补对拍测试(会话 C) | C |
| docx | 接受差异(引擎) | office_oxide vs python-docx,文本/分段发散保留 | 待补对拍测试(会话 D) | D |
| xlsx/xls/csv | 接受差异(引擎) | office_oxide/excelize vs ExcelParser,保留 | 待补对拍测试(会话 D) | D |
| pptx/ppt | 接受差异(引擎) | office_oxide vs python-pptx,保留 | 待补对拍测试(会话 D) | D |
| doc | 接受差异(引擎) | office_oxide vs tika,且输出格式不同(Go text / Py json),保留 | 待补对拍测试(会话 D) | D |
| epub | 不一致(接受差异/并入会话B) | Python 在 Parser 内嵌 512-token 切块(同 HTML §3.2);Go 每 spine 1 item,Python 每 spine 多 chunk;文本提取机制也不同 | 待补对拍测试(与 html 合并,会话 B) | E |
| 基本一致,需小修(Go 对齐 Python) | .msg:接受差异(Go 无 MSG 后端);metadata 门控逻辑 Go 需对齐 Python;附件解码 Go 更稳健(接受差异);text_html 空值需对齐 | 待补对拍测试(会话 E) | E | |
| json | 接受不对称(Go 独有) | Python flow 不补 json 分支;声明 flow 不支持 .json | 无需(仅 Go 侧测试) | E |
7. 关键文件索引(快速跳转)
Go 端
- 分派/调用:
internal/ingestion/component/parser.go(ParserComponent.Invoke)、internal/ingestion/component/parser_dispatch.go(dispatchParse/GetParser入口parser_type.go:31) - 解析器:
internal/parser/parser/{markdown,html,text,docx,xlsx,xls,csv,doc,pptx,epub,email,json}_parser.go - 输出契约:
internal/parser/parser/parse_result.go(ParseResult)、internal/ingestion/component/schema/parser.go(Page/ParserSetup/ParserParam)
Python 端(flow,正确对应物)
rag/flow/parser/parser.py(Parser类、_invoke、set_output、_markdown/_html/_code/_docx/_spreadsheet/_slides/_epub/_email/_doc、function_map、setups)- 底层实现(被 flow 委托):
deepdoc/parser/{markdown,html,txt,docx,excel,ppt,epub,json}_parser.py、rag/app/naive.py(Markdown/Docx/Html包装 +naive_merge*)
切勿混淆:internal/deepdoc/parser/docx/ 是 Go 端另一套 DOCX 解析器,未被 ingestion Parser 组件接入;ingestion 实际用的是 internal/parser/parser/docx_parser.go。