RAG数据导入第一步:从txt到Markdown的通用文本解析攻略

发布时间:2026/10/6 10:17:01
RAG数据导入第一步:从txt到Markdown的通用文本解析攻略 1. 为什么 RAG 的第一步永远是“把文本喂干净”做 RAG 的人都有一个共识检索效果差八成不是模型不行而是数据没处理好。我见过太多团队一上来就调 embedding 模型、换向量库、加 rerank折腾一圈发现召回率还是上不去回头一看原始数据——PDF 里全是断行、表格错位、页眉页脚混在正文里这种数据喂给再好的模型也是白搭。这篇要聊的是 RAG 数据导入与解析的第一环从最朴素的 txt 文件到结构清晰的 Markdown。为什么单独把 txt 和 Markdown 拎出来讲因为它们是整个 RAG 数据管道里最基础、最高频、也最容易被轻视的两种格式。txt 是绝大多数原始语料的默认形态——日志导出、爬虫抓取、数据库字段导出、小说文本、词典文件最后往往都落到 txt。而 Markdown 是当前 RAG 知识库最友好的中间格式它用极低的语法成本保留了标题层级、列表、代码块、表格这些结构信息让后续的切分chunking能按语义边界走而不是傻乎乎地按固定字数硬切。这篇文章适合谁看如果你正在搭 RAG 知识库手头有一堆 txt 不知道怎么清洗如果你在做文档结构化解析想让切分更聪明如果你只是好奇“为什么别人的 RAG 召回那么准”那这篇从 txt 到 Markdown 的通用文本与结构化解析攻略能给你一套可以直接抄作业的流程。我会把每一步的为什么这么做讲透而不是甩一段代码让你自己猜。先说清楚一个核心判断RAG 的数据导入不是“读文件”而是“重建语义结构”。txt 本身几乎没有结构只有换行和空格Markdown 的价值在于它把“这段是标题”“这段是列表”“这段是代码”显式标注出来。解析的本质就是从无结构或弱结构的文本里把人类能看懂、机器却看不懂的层级关系重新变成机器能识别的标记。这一步做得好后面的切分、embedding、检索全都顺这一步偷懒后面全是坑。2. 通用文本解析的整体设计思路2.1 先搞清楚txt 到底“通用”在哪txt 号称通用文本格式但它的“通用”恰恰是它最大的问题——它通用到没有任何结构约定。同样一个 txt 文件可能是一整篇小说段落之间用空行分隔一份日志每行一条记录字段用空格或制表符隔开一个词典每行是“词 释义”的固定格式一份从 PDF 复制出来的文档换行位置完全乱套一句话被切成三行一份从 Excel 导出的数据字段用逗号或竖线分隔。所以做 txt 解析第一步永远不是写代码而是先看数据长什么样。我习惯先跑三个命令摸清底细# 看文件编码中文乱码十有八九是编码问题 file -i input.txt # 看前 50 行判断大致结构 head -n 50 input.txt # 统计行数和字符数估算处理规模 wc -l -m input.txt这三个命令能帮你快速判断这是段落型文本、行记录型文本还是半结构化的混合文本。判断错了后面全错。2.2 为什么选 Markdown 作为中间格式有人会问为什么不直接解析成 JSON 或者 HTMLJSON 结构最清晰但可读性差人工校对成本高HTML 标签太重噪声多清洗麻烦。Markdown 是个折中格式结构表达力可读性清洗成本适合 RAG 切分纯 txt极弱高低但需自建规则差JSON强差中好但需转换HTML强中高中Markdown中强高低好Markdown 的标题#、列表-、1.、代码块、表格|这些标记天然对应 RAG 切分时想要的语义边界。一个##标题就是天然的 chunk 分界点一个代码块就应该整体保留不被切断。这就是为什么主流 RAG 框架比如 LangChain 的 MarkdownHeaderTextSplitter都优先支持 Markdown 切分。2.3 整体流程拆解我把从 txt 到 Markdown 的解析流程拆成五步后面每一节都会展开编码与清洗统一编码为 UTF-8去掉不可见字符、页眉页脚、乱码。结构识别判断文本类型段落型/行记录型/混合型识别标题、列表、表格等结构。结构重建把识别出的结构转成 Markdown 标记。质量校验检查转换后的 Markdown 是否语义完整、层级正确。切分准备按 Markdown 结构做初步切分为后续 embedding 做准备。这五步里结构识别是最难的一步也是决定成败的一步。因为 txt 没有显式结构你得靠启发式规则去猜。猜得准不准直接决定最终质量。3. 核心细节解析与实操要点3.1 编码处理中文乱码的根源与解法中文 txt 最常见的坑就是编码。Windows 下默认 GBK/GB2312Linux 和 Mac 默认 UTF-8一个文件在两边打开可能就是一堆问号。更麻烦的是有些文件是 GBK 和 UTF-8 混着来的或者带 BOM 头。处理原则很简单统一转成 UTF-8 无 BOM。但转换前要先探测编码。我常用chardetPython来探测import chardet def detect_encoding(file_path): with open(file_path, rb) as f: raw f.read(100000) # 读前 100KB 足够判断 result chardet.detect(raw) return result[encoding], result[confidence] # 实测confidence 低于 0.8 时要人工确认别盲目相信注意chardet对短文本判断经常出错尤其是纯中文短句。如果 confidence 低于 0.8建议手动用编辑器打开确认或者用iconv试转几个编码看哪个不乱码。转换时用iconv最稳# GBK 转 UTF-8 iconv -f GBK -t UTF-8 input.txt -o output.txt # 如果报错加 //IGNORE 跳过无法转换的字符 iconv -f GBK -t UTF-8//IGNORE input.txt -o output.txtPython 里转换要注意errors参数with open(input.txt, r, encodinggbk, errorsignore) as f: content f.read() with open(output.txt, w, encodingutf-8) as f: f.write(content)errorsignore会丢掉无法解码的字符可能丢信息errorsreplace会替换成保留位置但内容没了。我的经验是先 ignore 跑一遍统计丢了多少字符如果丢得少就接受丢得多说明编码判断错了得重新探测。3.2 不可见字符清洗那些看不见的“脏东西”txt 里藏着一堆看不见的字符它们不影响阅读但会污染 embedding。常见的\x00空字符从某些系统导出时会有\xa0不换行空格从网页复制常见\u200b零宽空格从某些编辑器复制会有\r\n和\n混用跨平台文件常见全角空格\u3000中文排版常见。清洗用正则一把梭import re def clean_invisible(text): # 统一换行符 text text.replace(\r\n, \n).replace(\r, \n) # 去掉零宽字符和空字符 text re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f\u200b-\u200f\ufeff], , text) # 不换行空格转普通空格 text text.replace(\xa0, ).replace(\u3000, ) # 多个连续空格压成一个但保留行首缩进要谨慎 text re.sub(r[ \t]{2,}, , text) return text注意压缩连续空格时如果原文有靠空格对齐的表格或代码会被破坏。所以这一步要在结构识别之后、针对正文段落做别一上来就全局压。3.3 结构识别从“猜”到“有依据地猜”结构识别是核心难点。我把它分成三类文本分别处理段落型文本特征是段落之间有空行段落内部换行是排版换行而非语义换行。典型的是小说、文章。处理重点是合并被硬换行切断的句子。判断规则如果一行结尾不是句号、问号、感叹号、冒号、分号且下一行开头不是特殊标记就合并。def merge_paragraphs(text): lines text.split(\n) merged [] buffer for line in lines: stripped line.strip() if not stripped: if buffer: merged.append(buffer) buffer merged.append() # 保留空行作为段落分隔 continue if buffer and not re.search(r[。.!?:;”)]$, buffer): buffer stripped else: if buffer: merged.append(buffer) buffer stripped if buffer: merged.append(buffer) return \n.join(merged)行记录型文本每行一条独立记录比如日志、词典、CSV。处理重点是识别字段分隔符转成 Markdown 表格或列表。先统计每行分隔符出现次数如果次数稳定说明是结构化行记录。from collections import Counter def detect_delimiter(lines, sample100): candidates [,, \t, |, ;, ] scores {} for delim in candidates: counts [line.count(delim) for line in lines[:sample] if line.strip()] if not counts: continue counter Counter(counts) most_common_count, freq counter.most_common(1)[0] # 分隔符出现次数稳定且大于 0得分高 if most_common_count 0: scores[delim] freq / len(counts) return max(scores, keyscores.get) if scores else None混合型文本既有段落又有列表、表格。这种最难需要先按空行分块再对每块单独判断类型。我的策略是先粗分块再逐块识别而不是全局套一个规则。3.4 标题识别没有#怎么知道它是标题txt 里标题没有任何标记但通常有这些特征单独成行前后有空行长度较短一般不超过 30 字不含句号等句末标点可能带编号如“第一章”“1.1”“一、”字体在原始文档里可能加粗或放大但 txt 丢失了这些信息。我用的启发式规则组合def is_heading(line, prev_line, next_line): stripped line.strip() if not stripped or len(stripped) 40: return False # 前后都是空行 if prev_line.strip() or next_line.strip(): return False # 带编号模式 if re.match(r^(第[一二三四五六七八九十百][章节篇部分]|[0-9](\.[0-9])*[、.]|[一二三四五六七八九十][、.]), stripped): return True # 短行且无句末标点 if len(stripped) 20 and not re.search(r[。、]$, stripped): return True return False注意这套规则误判率不低尤其是短句成段的诗歌、对话。所以识别完一定要人工抽检别全自动跑完就入库。我的做法是随机抽 20 个识别为标题的行人工确认准确率低于 90% 就调规则。识别出标题后还要判断层级。带“第X章”的是 H1“1.1”这种数字层级按点数决定 H2/H3。没有编号的短标题默认 H2因为 H1 通常留给文档标题。4. 实操过程与核心环节实现4.1 完整流程一个真实 txt 的解析实录拿一个真实场景举例我手头有一份从某系统导出的产品手册 txt大约 3 万字结构混乱——标题没标记列表用空格缩进表格用竖线分隔但列数不齐。目标是把

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询