资讯动态

RAG数据导入全攻略:从txt到Markdown的通用文本结构化解法

发布时间:2026/10/6 11:23:28 来源:尧图企业网站定制
RAG 数据导入与解析全攻略一从 txt 到 Markdown 的通用文本与结构化解1. 为什么 RAG 的数据导入第一步就劝退了很多人我接触 RAG检索增强生成这个方向一年多了最初以为最难的是向量模型选型、rerank 排序这些偏算法的东西。真正上手做了几个知识库项目之后才发现最劝退的其实是数据导入。你想RAG 的核心价值是先检索、再回答检索的质量完全取决于知识库里的文档被拆解得怎么样。如果导入阶段文本乱成一团、标题层级丢光、表格支离破碎后面向量检索效果再好也救不回来。很多刚入门的朋友喜欢直接拿 PyPDF2 或 pdfplumber 把 PDF 按页抽成纯文本然后一股脑切片塞进向量库。我一开始也这么干过结果问答测试时模型经常答非所问排查半天才发现是导入环节把文档原有的结构信息全部抹掉了章节标题、列表层级、代码块、表格全部变成了一堆连续字符串切片之后语义严重割裂。所以说RAG 的数据导入不能简单理解成把文件读出来它至少包含三个层次格式解析、结构还原、切片策略。格式解析解决的是从各种文件类型中提取文本的问题结构还原解决的是把提取出来的文本恢复成有层级、有语义边界的 Markdown 结构的问题切片策略解决的是怎么切才能让每个 chunk 在语义上尽量完整的问题。这套攻略我计划写成一个系列本篇只讲第一层从最通用的 txt 文本到 Markdown 结构化文本怎么在解析阶段就把数据基础打牢。这篇内容适合谁适合正在搭 RAG 知识库但总感觉检索效果不稳定的人适合需要批量处理各种杂七杂八文档txt、md、docx 导出的文本、网页另存为的文本的数据工程师也适合想彻底搞明白文档解析到底在解什么的初学者。我会把通用文本解析的完整思路、代码实现、踩坑经验和边界情况都拆开讲。先说结论txt 虽然在文件格式里最低级但它恰恰是 RAG 数据导入里最需要设计的一环。因为 txt 没有内建结构所有结构都必须靠解析规则从文本内容本身推断出来而 Markdown 恰好是这种推断结果的天然载体。把 txt 高效地转成高质量的 Markdown你在后续处理 docx、HTML、PDF 时很多思路是可以直接复用的。2. txt 文件解析的真实痛点你以为简单其实全是细节2.1 编码问题第一个坑也是最隐蔽的坑txt 的编码问题常年排在我踩坑榜单第一名。网上随便下载的资料、客户发来的导出文件、老系统的日志导出编码能给你整出各种花样来。UTF-8 带 BOM、UTF-8 无 BOM、GBK、GB18030、Unicode、Latin-1甚至同一个目录下的文件编码还不统一。如果你一上来就 open(path, r, encodingutf-8)十个文件里至少有三四个会在你脸上报 UnicodeDecodeError或者更恶心的是不报错但读出来全是乱码。这里我给的方案是先用二进制模式读取文件然后利用字符集探测工具 chardet 或 charset_normalizer 判断编码再转成统一的 UTF-8 纯文本。较长文档建议用 charset_normalizer它的准确率比 chardet 高不少尤其在中文场景下。# -*- coding: utf-8 -*- from charset_normalizer import from_bytes def read_txt_with_encoding(filepath: str) - str: with open(filepath, rb) as f: raw f.read() result from_bytes(raw).best() if result is None: # 实在探测不出就按 UTF-8 硬解并做容错 return raw.decode(utf-8, errorsreplace) # 探测出的编码统一转成 utf-8 文本返回 return str(result)注意如果返回的字符串里出现了大量替换字符UFFFD说明编码探测失败或者文件本来就是混合编码建议人工介入检查不要硬着头皮往下做。2.2 行尾与分隔符的归一化Windows 下的文本文件常用 CRLF\r\nLinux 和 macOS 下是 LF\n老 Mac 的 CR\r偶尔也能碰到。如果不在解析前做归一化后续做正则匹配、做切片、做标题识别时\r 会阴魂不散地出现在各种你意想不到的地方。我的习惯是解析流程第一步就把所有行尾统一成 \n顺带把各种全角空格、不间断空格\u00a0、零宽字符等干扰字符处理掉。这一步看起来不起眼但对后面的 Markdown 表格识别和代码块识别影响很大。def normalize_text(raw_text: str) - str: # 统一行尾 text raw_text.replace(\r\n, \n).replace(\r, \n) # 全角空格转半角中文场景常见 text text.replace(\u3000, ) # 不间断空格转普通空格 text text.replace(\u00a0, ) # 去掉零宽空格/零宽连接符等不可见控制字符 text text.replace(\u200b, ).replace(\u200d, ) return text提示全角空格在中文文档里极其常见不处理的话你后面按空格做分割或做代码缩进识别时会出现很多莫名其妙的 bug。2.3 空行、缩进与制表符的信息量txt 里的空行通常代表段落边界但多少个空行算一个段落边界其实没有统一标准。有些文档每行之间都有空行有些文档一个空行是换段落两个空行可能意味着新的章节。制表符和连续空格又可能是代码块的缩进。我的策略是在文本归一化阶段只处理不可见脏字符绝不轻易合并或删除空行。空行信息先原样保留等到结构识别阶段再按规则消费掉。这样做的原因是我见过太多人在第一步就把空行全删了结果后续想用空行特征识别标题层级或者段落边界时已经没了依据。3. 从 txt 到 Markdown四步结构化解析法实战3.1 第一步预清理与文档分块先分块再逐块识别很多人一听 Markdown 结构化就想着直接用一个大正则把整个文档从头扫到尾。文档短还好文档一长几十万字的小说、技术手册、导出的日志正则回溯能把性能拖垮而且全局规则之间容易互相干扰。我推荐的做法是先把文档切分成逻辑块每个逻辑块独立识别最后再把识别结果组装起来。具体切分逻辑如下按空行分隔得到粗块如果粗块内部整体缩进一致且每行都以短横线、数字加圆点、星号等开头则可能是列表需要在后续步骤单独识别如果粗块内部包含表格特征连续的 | 分隔行则可能是表格候选如果一个粗块只有一行且后面紧跟一个非空行这一行可能是标题候选。这个先分块再识别的思路本质上是在模仿人类阅读文档的方式人不会逐字看完整本再总结结构而是先看段落再判断每个段落是什么类型。代码实现上我通常用 yield 的方式做一个生成器把一个个候选块吐出来方便后续流水线处理。3.2 第二步标题层级识别人靠眼睛程序靠规则Markdown 标题用 # 级数表达但 txt 里没有 #标题的样子五花八门。常见情况有只有一行文本下文空行分隔行内出现第X章1.1一、结论等强关键词一行文本前后有大量空白、星号、横线装饰比如 或 ---- 在标题行下面序号加顿号或点号比如1. 引言1.1 背景。最稳的策略是两层判断先做装饰性标题识别比如行下方是 或 ---- 的 Setext 风格再做序号标题识别。对序号标题我维护一个标题栈记录当前已出现的最大层级。例如1是 H11.1是 H21.1.1是 H3一、可能对应 H1一对应 H2。当遇到新标题时根据它的序号深度决定是新增一个子标题还是回到上层标题以此保证 Markdown 树状结构不乱。import re _TITLE_NUM_PATTERN re.compile( r^(?Pnum(?:\d(?:\.\d)*|[一二三四五六七八九十])\.?)\s*(?Ptitle.)$ ) def infer_heading_level(line: str, stack: list) - tuple[int | None, str]: m _TITLE_NUM_PATTERN.match(line) if not m: # 尝试 Setext 风格行下方出现 或 --- return None, line num_part m.group(num).strip(.) title m.group(title).strip() if . in num_part: # 数字编号比如 1.1.2 level len(num_part.split(.)) else: # 中文序号比如 一、二、三 level 1 # 利用 stack 控制升降级这里省略具体实现细节 return level, title注意装饰线在 Markdown 里本身也有 Setext 标题语义所以解析时不要把 行当成内容输出它应该被消费掉只输出对应标题行。3.3 第三步段落、列表与引用块的还原在 txt 里段落往往就是连续的非空行。转成 Markdown 时最少的变化是段落之间以一个空行分隔这样 Markdown 渲染出来才是独立段落。列表识别需要额外小心。txt 里常见的列表特征包括以 -、*、 开头以数字加点或顿号开头1. / 1、以12开头。最稳妥的方式是按块识别先检测一个块的多行是否都有列表前缀再判断列表前缀是否统一统一才转成 Markdown 列表。如果只有一行有前缀、其他行没有那大概率不是列表而是普通文本里的序号不要强行转。引用块相对简单只要行首出现 或者 》 这类符号就可以在 Markdown 输出时加一层 前缀。但要注意引用块里的空行处理不当会导致 Markdown 引用断掉所以遇到引用块内空行我习惯转成 \n 而不是空行。3.4 第四步表格、代码块与图片的特殊处理txt 里有两类特殊情况非常考验解析器表格和代码块。表格在 txt 里常见形态是用空格或制表符对齐的列少量是 Markdown 风格的 | 分隔表格。前者很难百分百还原我通常采用启发式方案先按行切分检测多行是否以相同分隔符空格/制表符分割出类似的列数如果列数一致且列边界对齐再尝试用正则按 2 个及以上连续空格或制表符切列生成 Markdown 表格。代码块识别则要靠缩进。Markdown 本身支持四个空格缩进表示代码块和三个反引号围栏代码块两种形态。从 txt 转 Markdown 时如果检测到连续多行行首是制表符或 4 个空格我会优先输出为围栏代码块txt ...这样在后续切片时比较容易作为整体保护不会被拆得七零八落。图片引用在 txt 里通常是 [图片] 或 插入图片 这种占位符。由于 txt 本身不会携带图片字节解析器能做的只是在 Markdown 里保留一个 alt 占位符例如![未命名图片-001](待上传/未命名图片-001)这样在后续的多模态 RAG 流程里可以再根据这个占位符做图片关联至少信息没有丢。4. 结构化切片的边界问题Markdown 转出来后还要过一关4.1 以 Markdown 标题为骨架的切片树txt 转成 Markdown 之后还没到直接丢给向量库的时候。你还需要基于 Markdown 结构做切片否则前面的结构化等于白做。我的推荐做法是先把 Markdown 文档解析成一棵标题树类似很多 Markdown 解析器输出的 AST然后以叶子节点为单位切块。比如一个 H2 下面挂了两个 H3每个 H3 下的内容各自成块如果 H2 下没有 H3那 H2 下的第一层内容自成一块。这样做的好处是每个 chunk 都携带了从根到叶的完整标题路径后续给 LLM 生成回答时天然知道这段内容属于哪个章节上下文信息完整。你可以在 chunk 的前缀里拼上标题路径形成类似这样的文本## 第二章 RAG 数据导入 ### 2.1 编码问题 正文内容...4.2 代码块、表格、长段落不能被盲目切片切片时最常见的问题是一个代码块被拦腰截断或者一个表格被拆成两半或者一个长段落因为超过 token 上限被硬切。这些问题都会让检索出来的 chunk 语义破碎。我的应对策略有三条把代码块整体视为不可拆分单元如果代码块本身超长要么单独存要么按代码语义函数、类再细分而不是按字符数硬切表格块优先整体保留表格太大时可以按行分组但每组要保留表头对纯文本长段落优先按句号、感叹号、问号等句边界切分兜底再按 max_chunk_size 硬切。我见过很多人在这一步被 max_tokens 参数带着走把所有文本无脑切成 512 token 的块。对短问答类文本可能够用但对技术手册、论文这种密集文字的内容512 token 的固定尺寸会把完整论证过程拆得七零八落。这也是我强调先结构化后切片的核心原因。4.3 标题链路冗余与去重在 chunk 前缀拼上完整标题路径会带来一个问题相邻 chunk 的标题前缀大量重复向量化之后这些重复文本会占据不少 embedding 维度甚至可能干扰检索相似度。我的做法是如果 chunk 内容本身就是标题下的第一段标题前缀保留如果后续多个 chunk 属于同一个小节只有第一个 chunk 带完整标题路径后续 chunk 只带节号或者不带头标题用 text 里的首句作为上下文在入库时单独存一个 metadata 字段保存完整标题路径检索时用来过滤或展示而不是混在向量化内容里。5. 中文场景下的特有问题一个容易翻车的方向5.1 中文标点与分句中文没有空格分词一个长段落里的分句主要靠句号、感叹号、问号。但很多中文文档用的是英文标点.或者半角逗号乱入这在分句时会非常头疼。我通常会在归一化阶段把英文逗号、句号在中文文本中的出现先替换成中文标点但前提是不要误伤英文句子。这个过程需要小心处理我的建议是做一个基于 Unicode 脚本检测的规则如果相邻字符属于中文、日文、韩文等表意文字就把标点按中文习惯归一。举个例子他今年25岁。来自北京。这句话中如果简单把。替换成.对 Markdown 渲染影响不大但分句时就有麻烦。所以更稳妥的做法是用正则直接匹配中文结尾标点。5.2 中文序号体系映射中文正文里常见的一、二、三、一二1.1第1章等标题形态在生成 Markdown 标题时层级如何对应我的经验是txt 原始形态推断层级Markdown 输出示例第一章 / 一、H1# 第一章 xxx一/ 1.1H2## 1.1 xxx1. / 1H3### 1. xxx第1.1.1节H4#### 1.1.1 xxx当然这个映射不是绝对的因为同一份文档里可能出现混用。比如有的人写第一章作为 H1但里面的小节却用一、这时候一、应当映射为 H2 而不是 H1。所以标题层级推断必须依赖文档内统计信息而不是一个写死的对照表。5.3 合并段首缩进问题很多中文 txt 的段首要空两格两个全角空格这在 Markdown 里如果不处理会被渲染成代码块缩进因为 Markdown 里四个空格缩进是代码块语义两个空格倒是安全。但如果你统一按四个空格判断代码块就很容易把中文正常空两格的段落误判为代码。我的规避方式是代码块识别前先处理段首全角空格缩进特征只有在单行缩进一致且连续多行的情况下才判定为代码块中文段落中的首行局部缩进不参与代码块识别。6. 从文本到 Markdown 的完整案例一份实际文档的解析过程6.1 原始文档样例我模拟一份典型的中文技术说明文档txt 内容大致是这样的第一章 项目概述 本项目旨在构建一个基于 RAG 的知识库问答系统支持多种文件格式的导入与解析。 1.1 背景介绍 随着企业内部文档数量的增长传统的关键词检索已无法满足需求。 一当前痛点 1. 检索结果相关性差 2. 无法理解语义 3. 对长尾问题支持不足 二建设目标 构建一套可扩展的 RAG 流水线覆盖数据导入、切片、向量化、检索、问答全链路。 以下是示例代码 from rag_utils import chunker chunks chunker.split(markdown_text) 性能对比表 方案 准确率 耗时 关键词检索 62% 1.2s 向量检索 85% 0.8s RAG rerank 91% 1.1s6.2 解析流程模拟我按照前面说的四步法跑一遍第一步预清理与分块。原始文本先归一化行尾和全角空格然后按空行切成若干粗块。上面这份文档会被切成标题块、段落块、二级标题块、段落块、三级标题块、列表块、三级标题块、段落块、代码块候选、表格块候选。第二步标题识别。第一章 项目概述识别为 H11.1 背景介绍识别为 H2一当前痛点识别为 H3。第三步段落、列表、引用块还原。普通段落转成 Markdown 段落列表块转成 Markdown 无序列表。第四步表格与代码块特殊处理。代码块识别为连续缩进行转成围栏代码块。表格按行列对齐信息转成 Markdown 表格。最终的 Markdown 输出如下# 第一章 项目概述 本项目旨在构建一个基于 RAG 的知识库问答系统支持多种文件格式的导入与解析。 ## 1.1 背景介绍 随着企业内部文档数量的增长传统的关键词检索已无法满足需求。 ### 一当前痛点 - 检索结果相关性差 - 无法理解语义 - 对长尾问题支持不足 ### 二建设目标 构建一套可扩展的 RAG 流水线覆盖数据导入、切片、向量化、检索、问答全链路。 以下是示例代码 txt from rag_utils import chunker chunks chunker.split(markdown_text)性能对比表方案准确率耗时关键词检索62%1.2s向量检索85%0.8sRAG rerank91%1.1s### 6.3 转换过程中的取舍说明 这份输出里有一个很重要的取舍原文档的一当前痛点被映射成了 H3但严格按中文序号习惯它其实可以对应 H2。我在解析器里设定了一个规则当1.1已经占据 H2 时一自动降一级变成 H3。这是为了避免同一个文档里出现两套同级标题导致结构混乱。 另外表格原样里各列之间用 2 个空格分隔并没有 | 符号。我的解析器通过检测多行之间分隔位置的一致性来推断列边界最终成功转成了 Markdown 表格。如果原文档列对齐混乱这种推断会失败退化成普通段落而不是表格我认为这是合理的兜底行为——宁可让它变成正文也不要生成一张列错位的假表格。 ## 7. 工程实现层面的完整流程与模块拆分 ### 7.1 模块化设计解析器与转换器分离 一个足够健壮的 txt 转 Markdown 工具不应该是一个 500 行的巨型函数。我会把它拆成几个模块 - 读取与编码探测模块解决二进制读取、编码识别、编码转换。 - 文本归一化模块解决行尾、空白字符、标点归一化。 - 块切分模块解决按空行/上下文把文档切成粗块。 - 行类型识别模块对每一行/每一块做类型判断标题、列表、代码、表格、引用、段落。 - 树组装模块基于标题栈和块序列组装出 Markdown 树。 - 输出模块把 Markdown 树渲染成文本或者进一步导出 AST。 这种拆分的好处有两个一个是每个模块可以单独写单元测试另一个是在后续处理 docx 或 HTML 时替换掉读取与编码和行类型识别两个模块其余部分可以复用加入 docx 的样式信息和 HTML 的标签信息之后结构化效果会更好。 ### 7.2 配置化解析规则不该写死在代码里 不同领域文档的标题风格差异极大法律文书爱用第一条技术文档爱用1.1公众号导出的文本常有多级 emoji 前缀。如果一套规则打天下必然在某些语料上翻车。 所以我把常见规则做成配置项例如 python CONFIG { heading_rules: [numbered_chinese, numbered_dot, setext], enable_table_detect: True, table_sep_min_width: 2, # 至少 2 个连续空格视为列分隔 code_indent_spaces: 4, list_markers: [-, *, , •], max_heading_stack_depth: 6, }这样在特定项目里遇到特殊语料时我只需要调整配置而不需要改算法逻辑。遇到完全没有规律的 txt比如某个远古游戏攻略里全篇一个空行都没有那么配置再丰富也白搭这种语料我的兜底策略就是整篇视为一个段落或者按固定长度硬切——这本来就该是兜底。7.3 性能问题大文档处理时的注意点处理几十 MB 的 txt正则表达式和大字符串切片可能会很慢。我实测下来性能瓶颈往往出现在两个地方对整篇文档执行全局正则匹配、替换时可能产生多次 O(n) 扫描叠加起来就慢把每行字符串不断拼接、分割时极端情况下可能触发大量内存拷贝。优化的思路是尽量只用一次线性扫描比如逐行读取、逐行分类、累积块信息而不是反复做全局正则。Python 里用生成器逐行处理内存占用能压到很低处理大文档也更从容。8. 维护一份开源工具链还是自己造轮子8.1 现有的轮子有哪些这个方向其实已经有不少现成工具我整理一份供你参考工具强项边界Pandoc文本转 Markdown 的老牌神器支持格式多对中文弱结构 txt 的分层推断并不智能Markdownify把 HTML 转成 Markdown轻量只针 HTML对 txt 无效beautifulsoup4 手写规则灵活需要较多代码量维护成本高LlamaIndex / LangChain 的 loaders开箱即用解析质量一般尤其是中文文档MinerU / 各类商业解析 API文档结构还原能力强偏复杂文档简单 txt 有点大材小用Unstructured通用文档解析定制成本高环境依赖重8.2 什么情况下建议自己写如果你是做一个内部知识库文档种类固定、结构相对规整我建议自己写个轻量解析器。原因有三点内部工具的解析规则可控避免把敏感文档送到外部解析 API出问题时好排查好迭代。如果你的场景是海量未知来源的互联网文档或者 PDF 为主且扫描件比例很高我不建议在文本解析上自己去卷直接上 MinerU 这类专门做版面分析与结构还原的工具性价比高得多。txt 转 Markdown 这种通用解析自己写是完全可行的因为文本本身就承载了足够的信息不像扫描 PDF 还要依赖 OCR。8.3 对现有轮子的定制技巧如果决定用 Pandoc 做批处理有个小技巧先用 Python 对 txt 做一轮预清理和标题标记把识别出的标题行统一加上 #再用 Pandoc 转成标准化 Markdown。这样既保留了自己对标题层级的控制又能借助 Pandoc 完成后续的格式规整。单纯把整个 txt 丢给 Pandoc 的 markdown 读者它基本只会按空行分段落标题识别效果非常弱。9. 实际项目中的常见翻车点与避坑清单9.1 隐藏字符引发的向量化脏数据你千辛万苦把 txt 转成了 Markdown但如果文本里残留了零宽字符、软连字符、不间断空格等不可见字符向量化时 embedding 模型会把这些字符也编码进语义表征但实际效果是污染了语义甚至可能导致同一个词在表面相似但字符层面不同的情况下向量距离反而变远。我的习惯是在写入向量库的前一步再跑一次字符级清洗把 ASCII 控制字符、零宽字符、软连字符、替换字符全部清掉。9.2 切片数量与检索效果的平衡结构化切片比固定长度切片生成的 chunk 数量往往更多、更短。这会让向量库的条目数量上涨检索时如果不做 reranktop-k 的结果可能碎片化。我的实测经验是结构化切片之后把向量检索的 top-k 适当调小一些比如从 5 调到 3再配合 rerank 把语义最相关的 1-2 个块放最前面整体问答质量会明显提升而不是一味靠加大 k 来兜底。9.3 表格转 Markdown 后列错位列错位是我在文本表格解析里翻车最多的一个问题。根源通常是原文的列分隔不够一致或者单元格内出现换行。我的建议是解析时先按行切验证每一行的列数是否一致首行和后续行列数不一致时放弃表格语义直接按段落输出再补充一个 ASCII 表格占位符保证信息不丢。10. 下一步从通用文本走向复杂文档txt 转 Markdown 是基础也是整个 RAG 数据导入攻略的地基。这一层做扎实之后往 docx、PDF、HTML 扩展时核心思路是一样的从文件格式中尽可能提取结构信息统一映射到 Markdown AST再做切片。下一篇我准备写 docx 和 HTML 的结构化解析。docx 里每个段落自带 style 信息Heading 1、Heading 2、正文只需要用 python-docx 或直接读 document.xml 提取样式和文本节点比 txt 的纯启发式识别要轻松太多HTML 则可以靠标签语义直接映射比如 h1-h6、p、ul、ol、table、pre、blockquote映射到 Markdown 时非常直接。不过有个细节要提前说docx 的标题样式并不总是可信。很多人在 Word 里压根不用标题 1样式而是手动加大加粗改字号这在 docx 解析时同样需要启发式兜底。HTML 解析相对可靠但网页里大量存在的导航、页脚、广告等噪声需要一个预处理层专门滤除。这些我们下一篇细讲。如果你正在搭 RAG 知识库我建议你把本篇提到的四步解析法先在 txt 语料上跑一遍把你手头的废话文本、技术文档、聊天记录导出分别测试一下你会发现同样的代码在不同语料上的表现差异极大调参过程本身就是对解析规则最好的理解方式。希望这套攻畋对你有用我们下一篇见。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑