测试 Markdown 文档
【免费下载链接】WeKnoraOpen-source LLM knowledge platform: turn raw documents into a queryable RAG, an autonomous reasoning agent, and a self-maintaining Wiki.项目地址: https://gitcode.com/GitHub_Trending/we/WeKnora
这是一个测试 Markdown 文档,用于测试 Markdown 解析功能。
包含图片
测试图片
包含链接
这是一个测试链接。
包含代码块
def hello_world(): print("Hello, World!")包含表格
| 表头1 | 表头2 |
|---|---|
| 内容1 | 内容2 |
| 内容3 | 内容4 |
测试分块功能
这部分内容用于测试分块功能,确保 Markdown 结构在分块时保持完整。
- 第一块内容
- 第二块内容
- 第三块内容
测试重叠功能
这部分内容可能会在分块时与前后块重叠,以确保上下文的连续性。
这份文件虽然短小,却是一份经过精心设计的"语法全覆盖"夹具,它依次验证了解析链路中的每个关键能力: | 语法要素 | 测试意图 | 对应的解析/分块机制 | |---|---|---| | `#`/`##` 标题层级 | 标题结构识别 | `HeaderTracker` 表头追踪与上下文保留 | | `测试图片` | 图片语法提取 | `MarkdownImageUtil.extract_image` / `extract_base64` | | `测试链接` | 链接语法保护 | `protected_regex` 中的 `\[.*?\]\(.*?\)` | | 围栏代码块 ` ```python ` | 代码块整体保留 | `protected_regex` 中的代码块规则 | | GFM 表格 | 表格标准化与列对齐 | `MarkdownTableUtil.format_table` | | "分块功能"段落 | 结构完整性 | `TextSplitter._split` / `_merge` | | "重叠功能"段落 | 相邻块上下文连续性 | `chunk_overlap` 重叠机制 | 在 [docreader/testdata](https://link.gitcode.com/i/3f719c3334c2ed8816093eae46f02dab) 目录下,与 `test.md` 并排的还有 `test.html`、`test.txt`、`test_download.txt` 以及 `images/` 测试图片目录,共同构成 docreader 多格式解析的测试素材集。其中 `test.md` 专责 Markdown 路线。 ### test.md 的三种真实用途 从仓库使用点看,这份夹具至少在三处被直接引用: 1. **本地解析调试脚本的示例输入**:[docreader/scripts/parse_local.py](https://link.gitcode.com/i/f2f289036c18ab2ef8060f7f86397eab) 的文档字符串将 `docreader/testdata/test.md` 作为第一个示例命令:PYTHONPATH=. docreader/.venv/bin/python docreader/scripts/parse_local.py docreader/testdata/test.md
2. **gRPC 端到端测试的测试文件**:[docreader/client/client_test.go](https://link.gitcode.com/i/47c277a87611630d59713c39307f2b02) 的 `TestReadFile` 通过 `os.ReadFile("../testdata/test.md")` 读取该文件,构造 `ReadRequest{FileContent, FileName: "test.md", FileType: "md"}` 发给 DocReader 服务,并断言返回的 `MarkdownContent` 非空(见 [client_test.go](https://link.gitcode.com/i/47c277a87611630d59713c39307f2b02#L63-L95))。 3. **Markdown 表格格式化的行为基准**:`test.md` 中的 GFM 表格与 [docreader/tests/test_markdown_table_util.py](https://link.gitcode.com/i/586bccaa611a0868e8c9460345377124) 中 `test_format_nonempty_table` 等用例验证的是同一套格式化逻辑。 也就是说,无论你通过 gRPC 服务、Go 客户端还是 Python 直接调用,`test.md` 都是验证"Markdown 能否被正确解析"的最短路径。 ## Markdown 解析流水线:从注册到两阶段处理 ### 解析器注册:md/markdown 指向 MarkdownParser docreader 通过引擎注册表管理"文件类型 → 解析器类"的映射。[docreader/parser/registry.py](https://link.gitcode.com/i/a6ff9bbccfc348b56fb832f1ff50a6d3) 的 `_build_default_registry()` 在内置引擎(`builtin`)中注册了: ```python "md": MarkdownParser, "markdown": MarkdownParser,即扩展名为.md或.markdown的文件默认由MarkdownParser处理。除此之外,注册表还内置了markitdown(微软 MarkItDown 库)与opendataloader(PDF 版面分析)两个引擎;当请求引擎不支持某类型时,会依次回退到类型级默认引擎(如 ppt/pptx/csv 回退 markitdown)再回退 builtin(见 registry.py)。
文件类型并非只信扩展名:docreader/parser/parser.py 中的detect_effective_file_type会检查文件魔数——若扩展名为.docx但内容以 OLE 复合文档魔数\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1开头(典型的"旧版 .doc 改名 .docx"),则改用 DOC 解析器,避免把二进制 OLE 数据喂给 DOCX 解析器(见 parser.py)。
两阶段流水线:MarkdownParser 的组成
docreader/parser/markdown_parser.py 中的MarkdownParser继承自PipelineParser,将处理拆成两个串联阶段:
class MarkdownParser(PipelineParser): _parser_cls = (MarkdownTableFormatter, MarkdownImageBase64)PipelineParser(docreader/parser/chain_parser.py)实现了责任链模式:每个子解析器依次处理前一个的输出文本,各阶段产出的图片与元数据被累积合并进最终Document(见 chain_parser.py)。于是test.md的原始内容会依次经历:
- MarkdownTableFormatter:标准化全文所有表格(对应
test.md的"包含表格"章节); - MarkdownImageBase64:提取图片并转为路径引用(对应
test.md的"包含图片"章节)。
而BaseParser(docreader/parser/base_parser.py)定义了统一的契约:parse_into_text(content: bytes) -> Document,输出 Markdown 文本与图片引用,分块、OCR、VLM 图像描述均不在解析器职责内——这些由 Go 侧模块承担,解析器只做"文档 → Markdown + 图片引用"的轻量转换(见 base_parser.py)。
Document模型(docreader/models/document.py)包含content(Markdown 文本)、images(路径 → base64 数据)、chunks、metadata四部分,is_valid()以content != ""判定解析成功。
表格标准化:MarkdownTableUtil 的格式化规则
test.md中第 22~25 行的 GFM 表格在进入分块前,先由MarkdownTableFormatter处理。其底层工具类MarkdownTableUtil(docreader/parser/markdown_parser.py)的format_table提供如下行为:
- 首尾管道规范化:每行统一格式为
| 单元格 | 单元格 |,单元格两侧补空格,例如|表头1|表头2|→| 表头1 | 表头2 |; - 对齐标记保留:分隔行的
:位置会被保留并规范化为---,如|:---|---:|:---:|原样保留左右/居中对齐语义(format_alignment_row,见 markdown_parser.py); - 空单元格不丢失:内部空单元格(如
| a | | c |)在格式化后依然存在,测试test_preserves_empty_cells明确断言formatted.count("|") == raw.count("|")(见 test_markdown_table_util.py); - 行级处理、避免灾难性回溯:
format_table采用"逐行扫描 + 仅处理首尾都有|的行",替换了旧版正则实现——旧实现对未闭合行会触发指数级回溯(Tencent/WeKnora#2768),新实现将这类行原样透传,测试test_malformed_unclosed_row_is_passthrough用 100 列的未闭合行验证不会卡死事件循环(见 test_markdown_table_util.py); - CRLF 归一化:输入先做
\r\n/\r→\n统一,避免换行符混用导致表格行误判(见 markdown_parser.py),测试test_crlf_table_formats_without_mixed_endings验证输出不含\r且以\n结尾; - 无表头表格补分隔行:对 MarkItDown 产出的无表头表格,自动在首行后插入
| --- |分隔行,保证 GFM 渲染器能将其识别为表格(normalize_spurious_table_prefixes,见 markdown_parser.py)。
对于test.md的标准表格,格式化后输出形如:
| 表头1 | 表头2 | | --- | --- | | 内容1 | 内容2 | | 内容3 | 内容4 |图片与链接:提取、解码与存储交接
test.md的"包含图片"章节是MarkdownImageBase64的触发点。MarkdownImageUtil(docreader/parser/markdown_parser.py)提供三个关键方法:
extract_base64:匹配alt形式的内嵌图片,将其解码为二进制、用uuid4()生成唯一文件名(保留原扩展名),并把文档中的 base64 文本替换为alt路径引用;解码失败时保留 alt 文本并记录 error 日志(见 markdown_parser.py)。MarkdownImageBase64解析器随后将这些二进制数据 base64 编码后放入Document.images;extract_image:提取普通图片路径(alt),支持通过path_prefix为相对路径添加前缀,replace=False时可仅提取不替换(见 markdown_parser.py);replace_path:根据旧路径 → 新 URL 的映射批量替换图片引用,用于"图片已上传、把本地路径替换为存储 URL"的场景(见 markdown_parser.py)。
值得注意的是,docreader 侧不负责图片的最终持久化。gRPC 服务层 docreader/main.py 的_resolve_images将Document.images解码为ImageRef{filename, original_ref, mime_type, image_data}内联字节随响应返回,由 Go App 侧负责写入 local/minio/cos/tos 等存储后端(见 main.py)。对于扫描件 PDF 这类图片量大的文档,ReadStream流式 RPC 采用"一帧一个图片"的方式逐条发送,避免整个响应超过 gRPC 消息大小上限(见 main.py)。
test.md中的"包含链接"章节则无需特殊处理:链接语法由分块阶段的保护正则兜底,确保测试链接不会被从中间切开。
分块与重叠:TextSplitter 如何保持 Markdown 结构完整
test.md最后两个章节明确声明了分块测试意图:"确保 Markdown 结构在分块时保持完整"与"在分块时与前后块重叠,以确保上下文的连续性"。这两条注释精准对应 docreader/splitter/splitter.py 中TextSplitter的两大机制:保护正则(protected_regex)与重叠合并(chunk_overlap)。
默认参数
DEFAULT_CHUNK_OVERLAP = 80 # 块间重叠字符数,约为块大小的 15% DEFAULT_CHUNK_SIZE = 512 # 每块最大字符数代码注释明确指出这两个默认值与 Go 侧生产路径 internal/infrastructure/chunker/splitter.go 的DefaultChunkSize = 512、DefaultChunkOverlap = 80对齐(见 splitter.py 与 splitter.go);Python 版 splitter 目前仅保留给 docreader 边车使用,Go 版是生产主路径。512 字符约对应 100~130 个英文 token 或约 300 个中文字符,80 字符重叠(约 15%)是社区常用的"上下文连续性"经验值。
保护正则:先保结构,再分块
TextSplitter默认加载六条保护正则(见 splitter.py):
| 保护对象 | 正则 | 对应 test.md 要素 |
|---|---|---|
| LaTeX 数学公式 | \$\$[\s\S]*?\$\$ | — |
| Markdown 图片 | !\[.*?\]\(.*?\) | 测试图片 |
| Markdown 链接 | \[.*?\]\(.*?\) | 测试链接 |
| 表格头(含分隔行) | [ ]*(?:\|[^|\n]*)+\|[\r\n]+\s*(?:\|\s*:?-{3,}:?\s*)+\|[\r\n]+ | | 表头1 | 表头2 | |
| 表格体(数据行) | [ ]*(?:\|[^|\n]*)+\|[\r\n]+ | | 内容1 | 内容2 | |
| 代码块头 | ```(?:\w+)[\r\n]+[^\r\n]* | ```python |
处理流程(split_text,见 splitter.py)分五步:
_split:按分隔符优先级(默认["\n", "。", " "],可按需扩展为"\n\n"、"?"、"!"等)递归切分,直到每片小于块大小,切分时保留分隔符;_split_protected:用全部保护正则扫描匹配位置,按"起始位置升序、长度降序"排序并过滤重叠,超长保护内容会被忽略并记录 warning(见 splitter.py);_join:把保护内容从相邻的普通分片中分离出来作为独立单元,确保代码块、表格、链接等结构体在后续合并中不被拦腰截断,并以assert "".join(splits) == text校验无损还原(见 splitter.py);_merge:累积分片直到超出块大小,然后从前端弹出元素直至剩余长度小于重叠值,形成带重叠的新块(见 splitter.py);- 最终每个块输出
(start_pos, end_pos, chunk_text)三元组,restore_text可基于位置信息从带重叠的块中无损还原原文。
表头追踪:把上下文"挂"在块上
_merge过程中还嵌入了HeaderTracker(docreader/splitter/header_hook.py):当检测到 Markdown 表格头(表头行 + 分隔行模式)或代码块起始时,将这些"表头"作为上下文前缀注入到后续块中,直到遇到空行或非表格内容才结束追踪;同时通过header_column_mismatch校验表头列数与实际数据行列数,列数不一致时立即结束追踪,避免把错位的表头拼进无关块(见 header_hook.py)。因此test.md的每个##标题对应的表格、代码块、列表都能在分块后保留其所属章节的语义上下文。
实操验证:三条实测路径
路径一:本地脚本直跑(不经过 gRPC)
在仓库根目录执行(见 docreader/scripts/parse_local.py):
PYTHONPATH=. docreader/.venv/bin/python docreader/scripts/parse_local.py docreader/testdata/test.md脚本会将test.md解析为 Markdown 并打印到标准输出,同时在 stderr 输出content_len、images数量、metadata与耗时统计。可用参数:
| 参数 | 作用 | 示例 |
|---|---|---|
--engine | 选择解析引擎(builtin / markitdown),默认内置 | --engine markitdown |
--type | 指定文件类型,默认按扩展名推断 | --type md |
--out | 将解析结果写入文件,并导出图片到同目录images/ | --out /tmp/out.md |
--scanned | 扫描件 PDF 模式,逐页渲染为图片 | --scanned |
--log-level | 日志级别(DEBUG/INFO/WARNING/ERROR) | --log-level DEBUG |
对test.md而言,解析后应看到:标题层级完整保留、表格被格式化为规范 GFM 形式、测试图片语法保留原样(外部链接图片不会被下载,images计数为 0,因为它是普通路径而非 base64 内嵌)。
路径二:gRPC 服务 + Go 客户端端到端测试
先启动 DocReader 服务(默认监听:50051,支持 TLS,见 docreader/main.py):
PYTHONPATH=. docreader/.venv/bin/python docreader/main.py再运行 docreader/client/client_test.go 中的端到端用例:
cd docreader/client && go test -run TestReadFile -vTestReadFile会读取test.md内容、构造ReadRequest调用ReadRPC,断言MarkdownContent非空并打印content_len与图片数(见 client_test.go)。若服务未启动,测试会通过requireLiveDocReaderClient对localhost:50051的探测自动Skip。
路径三:Python 单元测试
cd docreader && .venv/bin/python -m pytest tests/test_markdown_table_util.py -v【免费下载链接】WeKnoraOpen-source LLM knowledge platform: turn raw documents into a queryable RAG, an autonomous reasoning agent, and a self-maintaining Wiki.项目地址: https://gitcode.com/GitHub_Trending/we/WeKnora
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考