☰
RAG数据准备实战:txt解析与Markdown转换指南
2026/10/6 6:37:21 网站建设 项目流程

前阵子帮朋友排查一个RAG项目,他的知识库用的是现成的RAG框架,向量库、召回、生成环节看起来都没毛病,但检索结果就是又碎又乱。查了两天,问题不在模型,也不在框架,而是卡在最前面那个最不起眼的步骤:数据导入与解析。几十个txt文件被原封不动丢进切块器,目录、页眉、网页复制下来的广告全成了向量,检索自然乱成一团。

这个场景太典型了。RAG这条链路里,数据入口最容易被当成“不是技术活”,但它恰恰决定了后续切块、向量化、召回的上限。这篇先写系列第一篇,聚焦通用文本:怎么把txt这类最“没有结构”的文本,处理干净并转成Markdown这种带结构的中间格式。对象是正在搭RAG知识库、或者被召回效果折磨的开发者。不管你在Mac还是Windows上搭环境,数据准备这一关绕不开,看完你至少能把通用文本解析这个大头老老实实走通。

1. RAG项目里最容易翻车的不是模型,而是数据入口

1.1 RAG整条链路里,数据准备决定了什么

先说清楚RAG的完整链路:数据导入 -> 文档解析 -> 文本切块 -> 向量化 -> 语义检索 -> 生成回答。很多人用现成RAG框架时,只管调接口,把文件往里一丢就完事,忽略了中间每个环节的质量。结果就是:检索出来的top-k结果十个有八个是废话,大模型再聪明也只能在垃圾上生成答案。这类问题有个通用说法叫“Garbage in,garbage out”,在RAG里体现得格外明显。

数据准备这一步决定了三件事:第一,一句话会不会被拦腰截断。txt里的换行经常是视觉换行,不是语义换行,直接按换行切块会让“苹果是一种水果,富含维生素C”变成两个孤立碎片。第二,一个概念能不能被正确归属到所属章节。没有结构信息时,向量检索分不清“Windows安装”和“Windows常见问题”到底谁是谁。第三,无效字符会不会污染语义。页眉、页脚、目录页码、版权声明这些内容一旦进入向量库,检索时它们会大量命中,挤占有效的top-k位置。

换句话说,数据入口的质量不是“锦上添花”,而是“决定天花板”。模型再强,框架再成熟,解析这关糊弄过去,后面全部白搭。

1.2 三类最典型的“脏txt”场景

我在实际项目里见过最多的脏txt,基本逃不出下面三类,你可以对照自己的数据源看看有没有中招。

场景A:从各种网站上扒下来的小说、教程、电子书txt。这类文件里通常带着目录、章节标题、作者简介、更新公告,甚至还有“本小说来自某某论坛,请支持正版”这种广告行。它们的问题不是乱,而是“半结构”——明明有章节结构,但没有统一格式,目录和正文挤在一起,直接切块会把目录里的章节名变成高权重向量,导致检索时频频命中目录而不是正文。

场景B:PDF转txt导出的文件。这类txt最坑的地方在于:PDF里每一页的页眉页脚、页码、“第X页共X页”全部保留,英文单词还会在行尾被断成两半,比如“informa- tion”。最麻烦的是PDF表格转出来后变成一堆空格和制表符,看起来对齐,实际上在Markdown里根本没法用。

场景C:日志文件、老系统导出数据、爬虫抓取的网页正文。这类文件里混着时间戳、特殊分隔符、HTTP状态码、重复的导航文案。如果解析时不做清洗,这些噪音片段会被当成正常文本向量化,检索效果可想而知。

所以通用文本解析的第一步,不是你急着转Markdown,而是先搞清楚你的txt里到底有什么。看一眼原始文件长什么样,比自己瞎猜结构重要得多。

2. txt不是一种格式:编码、脏字符与伪结构的基础排查

2.1 编码问题:从“锟斤拷”说起

txt看起来是个文件后缀,实际上它根本不是一种“格式”,而是一坨没有任何结构信息的纯字节流。处理txt最先遇到的坑是编码。中文环境里最常见的三套编码是UTF-8、GBK、GB18030。现代RAG框架默认按UTF-8读取,但老系统、Windows记事本另存的txt经常是GBK或GB18030。你用UTF-8去解GBK文件,中文就变成“锟斤拷烫烫烫”这种经典乱码。

乱码的根源不复杂:同一个字节序列,用不同编码规则解码,出来的字符完全不同。GBK里一个汉字占两个字节,UTF-8里一个汉字占三个字节,错位之后,原本“知识库”三个字就可能变成一堆符号替代符U+FFFD。这种文本进入向量化环节,不仅浪费Token,还会产生完全无意义的向量。

检测编码的方法有很多,命令行优先:在Linux或Mac下直接执行file -i 文件名.txt,大部分情况下能直接告诉你编码类型。想更精确就用Python的chardet库,它会基于统计学给出可能的编码候选。VS Code用户更简单:打开txt文件,右下角会显示当前编码,点击就能切换和重新保存。拿到编码后,务必统一转成UTF-8,这是后续一切解析的前提。

2.2 换行符、BOM与控制字符:看不见的搅局者

编码之外,还有三类看不见的字符在暗中搞事。

第一类是换行符。Windows用CRLF(\r\n),Unix/Linux/Mac用LF(\n),老版Mac用CR(\r)。如果你把不同系统的txt拼接在一起或批量合并,文件里可能混着两种甚至三种换行。切块时如果对这些换行一视同仁,逻辑上没错,但有些解析器会把\r当成普通字符残留在切块结果里,看起来就是每段末尾多了一个奇怪的符号。更麻烦的是Markdown解析器对\r的处理标准不一,可能导致渲染出现异常空行。

第二类是BOM头。Windows记事本保存UTF-8文件时经常加一个BOM(字节顺序标记),对应字节序列是EF BB BF,在Python里读取后会变成字符串开头的\ufeff。这个看不见的字符一旦进入切块器,会被算成一个独立Token。排查方法可以用Hexdump:hexdump -C 文件名.txt | head -n 1,看到开头是ef bb bf就说明有BOM。

第三类是控制字符。日志文件里经常夹着\x00、\x1a这类不可见控制字符,它们不会在编辑器里正常显示,但会在解析时制造异常边界。处理思路也很简单:只保留正常文本字符,把ASCII控制字符剔除掉。

2.3 伪结构:txt里没有真正的“结构”

txt最大的问题不是脏,而是“没有结构”。所谓章节、层级、列表,在txt里全靠视觉惯例表达:缩进、数字编号、下划线分隔线。这些惯例在不同文件里千奇百怪,程序无法直接识别。

举个典型的例子看为什么不能简单粗暴地判断标题。假设有一行文本是“3. 根据上述内容,总结如下:这是一个非常长的段落,里面讲了很多东西……”。它看起来像是有个数字编号“3.”,但你把它当成标题就会出大问题。真实项目里必须设计规则:标题行通常很短、有编号模式、后面多半跟一个空行或正文段落。而正文里的“3.”只是某个句子的开头。

所以解析txt的核心思路是:先清干净脏数据,再靠规则把“视觉结构”还原成“语义结构”。这个过程不可能100%完美,但足够把大部分文档处理得很像样。后面我会给出具体代码。

2.4 清洗操作清单

把上面说的整理成一张可执行的检查清单,每次处理txt前过一遍:

问题类型典型表现处理方式
编码错误中文变“锟斤拷”或乱码检测后统一转UTF-8
BOM残留切块首字符异常删除\ufeff
换行符混杂段落间多出符号或空行全部统一为\n
控制字符显示为乱码方块正则过滤,只保留可见字符
全角符号检索不准确NFKC规范化转为半角
连续空行切块产生空白碎片压缩为单个空行
页眉页脚/广告污染向量库用规则或黑名单过滤

表格看起来简单,但每一条都是真实项目的血泪。尤其全角符号,很多中文文档里的括号、逗号、空格都是全角,你不做NFKC规范化,切出来的文本里就会混着两种看似相同但字节不同的符号,导致检索匹配率下降。

3. 为什么Markdown适合当RAG的“中间层”

3.1 Markdown的语义标签就是切块边界

处理完txt的脏数据后,下一步不是直接切块,而是先转成一种“中间格式”。我强烈推荐Markdown,因为它天生就是为内容分块设计的。井号标题(#、##)表示层级,列表项表示并列内容,表格表示结构化数据,代码块用反引号包裹,引用块用大于号开头,粗体和斜体补充强调信息。这些东西看上去只是排版标记,实际上每一类都对应一种语义边界。

切块时,Markdown的标题层级可以直接复用:一个二级标题下的内容,天然是一个候选块;三级标题则可以作为子块。代码块、表格、公式单独成块,避免被普通段落切碎。相比纯txt那种只能靠空白和换行猜测段落,Markdown给了切块器一套明确的规则,让整个RAG链路的安全感提升一大截。

数学公式也要提一句,很多人关心Markdown数学公式插件,其实在RAG解析阶段你不需要一个“渲染插件”,你只需要保证LaTeX语法的完整保留。比如$f(x) = x^2$行内公式和$$E=mc^2$$块级公式,只要切块时不把$$从中间断开,后续检索和展示都能正常工作。

3.2 为什么不用HTML、JSON或纯txt

中间格式不是越复杂越好,关键看“解析成本”和“下游适配”。我把几种格式放在一起对比过:

格式人读性结构化程度解析成本RAG适配度
txt一般无低差,需大量规则
Markdown好半结构化中好,切块边界明确
HTML差,噪音多结构化高,标签冗余中,需深度清洗
JSON差结构化中适合元数据,不适合正文

HTML其实也能表达结构,但网页文本里标签、class、style、脚本残留实在太多,解析它需要额外做一层噪声过滤。JSON作为正文格式更是灾难,你会被嵌套和引号转义搞疯,阅读性极差。有些项目纠结“wiki格式还是Markdown”,我的观点是:只要能形成统一的标准,两者都可以,但Markdown生态更统一,各种解析器、渲染器、向量数据库插件支持最广,踩坑最少。

3.3 现成工具摸底:pandoc、markdownify与本地拆解

处理Markdown转换,我常用的工具链包括:pandoc、markdownify、trafilatura。pandoc是格式转换的瑞士军刀,能把txt、HTML、docx、epub等各种格式转成Markdown,功能极其强大。markdownify是Python库,专门处理HTML转Markdown,写爬虫管线时很顺手。trafilatura主攻网页正文提取,能自动滤掉导航栏和页脚,适合RAG语料采集。

但注意,这些工具对已经“有格式”的文件效果很好,处理txt这种裸文本时能力有限。pandoc转txt时往往只做简单包裹,不会帮你识别“第一章”这种伪结构。所以如果你想完全省力,还有一条路:用语言模型做结构化解析。让LLM直接把整篇txt按内容重排成Markdown,效果在复杂文档上确实好,但成本高、速度慢、有幻觉风险。我的建议是:先上规则脚本,规则搞不定的边缘case再让LLM补刀。这也是为什么我坚持要你掌握下一节的手写转换流程——不是为了炫技,而是它能让你在“规则”和“模型”之间自由切换。

4. 不依赖轮子:手写txt到Markdown的转换流程

4.1 整体流程设计

手写转换器听起来工程量大,实际只要拆成五步就很清晰:读原始字节 -> 检测编码 -> 解码与清洗 -> 行级分类与结构推断 -> 输出Markdown。关键是每步只做一件事,避免在一步里塞进太多逻辑。

我先给一个整体的架构思路:清洗完的文本按行拆开,程序逐行判断它是什么类型:是标题?列表?引用?表格?还是普通段落。判断完成后再重构为Markdown字符串。这个“先分类、后输出”的好处是你可以随时调整单行的识别规则,不会破坏整条链路。

很多人可能会说:“这类活我直接用大模型几行提示词不就行了?”大模型确实可以,但规则脚本在批量处理时更稳定、更便宜,而且可解释。解析出问题时,你能一眼看出是哪条正则误判,而不是去翻模型输出排查为什么把“3. 一个普通句子”当成了标题。我个人建议的搭配是:规则为主、模型为辅。

4.2 编码检测与文本清洗代码

第一段代码解决“读进来”的问题。以下Python函数读取txt文件,自动检测编码,清洗BOM、换行符和控制字符,并返回UTF-8的统一文本:

import re import unicodedata import chardet def read_txt_clean(path): with open(path, 'rb') as f: raw = f.read() # 1. 编码检测 encoding = chardet.detect(raw)['encoding'] if encoding is None: encoding = 'utf-8' # 2. 解码,errors='replace'能保证解码失败时不中断 text = raw.decode(encoding, errors='replace') # 3. 去BOM text = text.replace('\ufeff', '') # 4. 统一换行符 text = text.replace('\r\n', '\n').replace('\r', '\n') # 5. NFKC规范化:全角字母、数字、空格转半角 text = unicodedata.normalize('NFKC', text) # 6. 过滤控制字符,保留\n和\t text = ''.join(ch for ch in text if ch == '\n' or ch == '\t' or ord(ch) > 31) return text, encoding

这里有个细节值得多说一句:errors='replace'。如果你直接使用text.decode(encoding),遇到无法解码的字节会直接抛异常,整个文件挂掉。使用errors='replace'后,那些异常字节变成符号替代符U+FFFD,虽然看着丑,但不会中断流程。真正重要的文本内容仍然能保留大部分,后续交给规则清洗再优化。

4.3 结构推断规则:怎么把“视觉结构”还原成“语义结构”

第二步是识别每一行的类型。我对常见txt总结了几个高命中率模式:

def classify_line(line): s = line.strip() # 章节标题:第X章 / 第X节 / 第X篇 if re.match(r'^第[0-9一二三四五六七八九十百]+[章节篇部]', s): return 'heading' # 数字编号标题:1. 简介 / 1、背景 / 2.1 方法 if re.match(r'^\d{1,3}([.、\s])\s*\S+', s) and len(s) <= 50: return 'heading' # 无序列表 if re.match(r'^[-*•]\s', s): return 'bullet_list' # 有序列表 if re.match(r'^\d{1,3}[.)、]\s', s): return 'ordered_list' # 引用块 if re.match(r'^>\s?', s): return 'quote' # 表格行:至少包含一个竖线分隔符 if s.count('|') >= 2: return 'table_row' return 'paragraph'

注意几个设计细节。标题限制在50字符以内,是为了避免把一大段以数字开头的正文误判为标题;“第X章”的正则优先级最高,因为它模式足够强;列表识别放在标题之后,否则“1. 第一章内容”可能会先被当成标题,再看前缀才识别为有序列表,那就错了。

表格识别的逻辑比较粗糙,但仍很实用。txt里真正规整的表格不多,更多是用制表符或竖线粘在一起的伪表格。你可以在此基础上加入“同行竖线数量是否一致”的判断,能进一步降低误判率。

这套规则不完美,没事。它最大的优势是“可解释”“可修”—某条规则误判了,你改一行正则,马上能验证。不要把规则写得太大太复杂,保持简单,让难以处理的长尾问题交给模型。

4.4 输出Markdown与校验统计

第三步是把分类后的行输出为Markdown。这一步同时要处理两个问题:格式生成和校验统计。

格式生成的思路是:标题前加对应数量的井号,普通段落之间用空行隔开,列表项前加“-”或“1.”,引用行前加“>”。表格行则保留竖线,并在输出前确保单元格内没有换行符。

我建议在转换末尾加一个统计报告,打印出基本信息,这个简单步骤能帮你快速判断这次转换是否离谱:

def to_markdown(text): lines = text.split('\n') md_lines = [] block_count = 0 heading_count = 0 for line in lines: kind = classify_line(line) if kind == 'heading': heading_count += 1 md_lines.append(f'## {line.strip()}') block_count += 1 elif kind == 'bullet_list': md_lines.append(f'- {line.strip()[1:].strip()}') elif kind == 'ordered_list': md_lines.append(f'1. {line.strip()}') elif kind == 'quote': md_lines.append(f'> {line.strip()}') else: md_lines.append(line) # 段落之间补空行 md_lines.append('') md = '\n'.join(md_lines) return md, heading_count, block_count

这里的heading_count就是后续切块的重要输入指标。如果一份5000字的文档,解析出来没有标题,说明规则没有匹配到这个文档的标题风格,需要你回去再调正则。如果标题数量暴涨到50个,大概率存在误判。这种“数字反馈”比肉眼抽查快得多。

输出完成后,用VS Code的Markdown预览或者任意渲染器看一眼效果。结构清楚、标题层级正确、表格不破,才说明解析真的通过了。

5. 结构化之后:切块与元数据注入

5.1 标题树与“父子路径”

Markdown转换完成后,下一步就是切块。很多RAG框架内置了切块器,默认按固定字符数(比如500或800)硬切。这种一刀切的方式在长文档上问题很大:一个观点可能正好被切成两半,或者一个大段落因为字符数超标被塞进多个chunk。

用Markdown结构切块就优雅很多。思路很简单:把文档解析成“标题树”,每个叶子节点保存从根节点到自身的完整路径,以及路径下对应的正文内容。

举例说明,假设转换后的Markdown长这样:

## 用户手册 ### 安装 #### Windows 安装过程分为三步... #### macOS 直接拖入应用程序文件夹... ### 常见问题 启动闪退怎么处理...

切块时,你可以得到两个主语义块:路径为“用户手册 > 安装 > Windows”的内容块,和路径为“用户手册 > 安装 > macOS”的内容块。向量化时,把“用户手册 > 安装 > Windows”这一串路径文本拼到正文前面,检索“Windows安装失败”时,这个块会因为路径里有强相关词而更靠前。这是纯文本切块完全做不到的。

5.2 表格、代码块、公式的特殊处理

标题树能解决大部分文档,但表格、代码块和公式需要单独对待。

表格在RAG里是个麻烦角色。一个40行的表格,整块塞进一个chunk很容易超Token,而且表格中很多单元格单独拿出来没有完整语义。我的做法是:按行或行组分切,比如每5行切成一个小块,并在块前面加上列头作为上下文。如果表格本身有“表头+表格正文”的结构,务必保留表头信息,否则检索出来的内容会让人看不懂在说什么。

代码块单独成块,最好保留语言标注,比如python、bash。这样后续甚至可以针对代码块做特殊的检索或过滤。

公式的处理前文提过:块级公式和行内公式都必须保持完整,不能在切块时拦腰截断。这里补充一个细节:用Markdown表示公式时,行内用$...$,块级用$$...$$,切块器的正则要先把$$...$$整体识别为一个单元,再从公式外寻找切分点。

还有一个经常被问到的点:RAG知识库能存图片吗?答案是:Markdown里的图片语法!alt文本](路径),在RAG索引时实际被处理的往往是alt文本或文件路径,而不是图片本身。如果你需要直接检索图片内容,那已经不是“文本解析”的范畴,得接多模态模型或者CLIP向量化管线。所以,指望把图片放进txt转Markdown就能让RAG看懂图片,是定位错了方向。

5.3 元数据:让检索按来源与上下文过滤

切块之后,还有一道工序很多人会漏掉:给每个块加元数据。

元数据的价值体现在场景上。你有一个包含几十种文档的知识库,用户只关心“运营手册”里的内容,检索时如果不按来源过滤,就可能把“技术文档”里的相似内容也捞出来。在chunk前添加YAML格式的元数据块,就是个很实用的做法:

--- source: manual_2024.txt title: 用户手册 section_path: 安装 > Windows chunk_id: manual_2024_0032 ---

source记录原始文件,方便排查问题;title是文档标题;section_path是标题树路径,检索时可以作为过滤条件或拼接上下文;chunk_id用于去重和调试。有了这些字段,你可以在向量检索时做字段过滤,比如“只检索section_path里包含‘安装’的块”,效果会精准非常多。

6. 实战复盘:我在真实项目里踩过的解析坑

6.1 编码推断翻车,不只是chardet的锅

有一次我在处理一批短文件时,chardet把两个UTF-8编码的文件识别成了windows-1252,结果中文全部乱码。后来排查发现,短文件信息量太少,统计检测算法容易误判。

现在我会用双重策略兜底:先用chardet给出候选,再按候选顺序逐个尝试解码,并统计解码后中文字符占比,占比最高者胜出。如果中文字符占比都过低,就优先回落UTF-8。简单说:不要盲信任何单一检测器的结果,数据量太小时宁可多试几种编码。

6.2 正则误判,把正文句子当成标题

前面写过标题规则要限制长度,这个想法不是凭空来的。我曾经用^\d{1,3}[.、]\s*当标题识别规则,结果文档里“3. 根据以上分析,可以得出结论:……”这种句子全部被标记成标题。输出后我一看统计,一份文档冒出三十多个标题,才意识到正则太激进。

修复思路是组合条件:数字编号 + 短行 + 下一行是空行。只有满足这三个条件的才认定是标题。你可以继续增加规则,但原则不变——宁可漏掉几个标题,也不要误判一堆正文。漏检的后果只是切块变大,误判的后果是语义边界彻底错乱。

6.3 表格转Markdown后渲染破损

手写转换器把带竖线的txt转成Markdown表格,看着很简单,实际容易翻车。有一次某个单元格里含有换行符,转出来的表格多了一行,整个表格在渲染器里错位。后来我在写入表格行时强制把单元格内的换行替换成空格,并保证续表行数与表头一致。这个修复很小,但对后期人工阅读和检索质量影响很大。

6.4 验证指标与最小召回测试

解析做完别急着灌进向量库,先做三件事。第一,跑一遍统计脚本,确认总字符数、行数、段落数、标题数、表格数在一个合理范围。第二,人工抽检20行,看看标题分类和列表识别是否正确。第三,在RAG系统里做一次“最小召回测试”:用几个你真实关心的业务问题去检索,看看返回的是不是对应的核心章节。这三步走完,数据入口才算真正守住。

上面这些坑几乎每一条都在实际项目里出过问题。我自己现在跑通用文本解析时,会先准备好一套固定规则脚本,再搭配人工抽检,确认统计指标正常后才会进切块。如果你刚起步,建议先拿三五十个真实文件跑一遍,看看标题分类准不准,比急着调向量模型有用得多。数据入口稳了,后面的RAG链路才算真的稳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询