恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

从txt到结构化块对象:RAG文本解析与分块实战

  • 首页
  • 资讯中心
  • /
  • 从txt到结构化块对象:RAG文本解析与分块实战

相关资讯

正片负片分不清?Allegro焊盘三兄弟一次讲透 2026/10/6 5:52:25
Allegro焊盘设置详解:Regular Pad、Thermal Relief与Anti Pad 2026/10/6 5:52:25
NVIDIA KDA v0.6 在 B300 上推理 Moonshot 模型性能提升 2.96 倍实战解析 2026/10/6 5:52:25

最新资讯

迈普交换机IGMP Snooping与Storm Control实战配置指南
UE5蓝图编辑器:图形化编程语言的工程化实践指南
从S参数到眼图:高速串行链路联合仿真全解析
AI日报制作全攻略:从信息筛选到高信噪比内容输出
网络安全应急演练:从文档到自动化响应的实战闭环
工控AI的本质:实时性、确定性与工艺语义的深度融合

今日推荐

2026 AI 开发全家桶落地指南:TaoToken 统一 Key 打通 IDE 插件、Agent 与自动化代码审查全链路配置实测
MR25H40CDF+STM32F031C6工业级高可靠数据存储方案
MRAM+STM32工业断电数据保全实战指南

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

从txt到结构化块对象:RAG文本解析与分块实战

发布时间:2026/10/6 5:52:25
从txt到结构化块对象:RAG文本解析与分块实战 1. 数据准备阶段为什么是RAG项目最容易被低估的一环1.1 一次检索翻车让我重新盯上解析链路做RAG项目的人迟早会在数据导入这一步栽跟头。我自己搭本地知识库第一版的时候数据来源是一批产品使用说明和项目复盘记录格式基本上全是txt。当时图省事把所有文件拼接成一个大文本按固定长度直接切成块就丢给embedding模型。上线跑了两天用户问“设备告警阈值在哪配置”返回的上下文却是一段关于维护周期的废话。查了半天才发现问题不在模型也不在向量检索参数而是原始文本被切得七零八落一个完整的功能说明分散在六个互不关联的块里检索召回的前几块全是边缘内容。从那以后我重新审视了整条RAG链路文档加载、数据解析、分块、向量化、检索、生成。绝大多数人会把精力放在embedding选型、向量库调参、提示词工程上却很少想过一个问题——喂给模型的文本到底长什么样。RAG的实际效果有一个很朴素的上限检索能召回什么很大程度取决于解析和分块阶段把文档变成了什么。换句话说数据导入与解析不是“工具性环节”它直接决定了后面所有东西的天花板。这一篇是这个系列的第一篇主题就是通用文本与结构化解的起点如何把一个txt文件通过编码识别、内容清洗、结构还原整理成Markdown再输出为带层级关系与元数据的结构化块对象。整套方法不依赖重型框架用Python标准库加一两个解析工具就能跑通适合正在搭RAG知识库但是对解析链路没什么头绪的人也适合已经跑通Demo、想回头优化召回质量的人参考。1.2 通用文本与结构化文档的本质差异在动手之前得先分清两类输入。RAG领域经常提到“非结构化数据”但它内部其实差别很大。纯通用文本txt、log、随手记录的笔记。内容是线性排列的没有明确的层级标题、段落、列表全靠肉眼辨认。半结构化文档带有Markdown语法、HTML标题、Word标题样式、PDF书签的文档。它们虽然格式各异但内部存在可提取的树状结构。结构化数据类似数据库表、CSV、JSON这种字段明确的记录一般不走文档解析流程而是直接包装成元数据或知识图谱中的节点。所谓“结构化解”是指让原本没有机器可读结构的文本被赋予可被程序提取的标题层级、段落边界、内容类型和元数据。它不是要把txt变成一个二维表格而是要把它还原成一种“人检查起来不费劲、程序消费起来不迷路”的中间形态。我常用的做法是先把txt自动整理成Markdown再基于Markdown的标题层级和内容边界生成一份带文档树的JSON块列表。后续不管是做向量化、关键词检索还是作为知识图谱候选都能直接复用。理解这个差异能帮你避开一个常见误区不是所有文档都需要深度解析。一份只有三段的说明文字直接整体作为一个块使用就挺好一份几十章的操作手册才值得花力气把章节结构还原出来。解析方案的复杂度应该跟着文本的语义结构走而不是盲目上重型框架。2. txt 导入先解决编码、清洗和拆块顺序2.1 编码识别不要打开记事本看一眼就完事txt文件最大的坑不是格式而是编码。同一个“配置说明.txt”可能是UTF-8、GBK、GB18030甚至带着BOM头。用记事本打开能看懂不代表Python能直接读对。我第一次批量导入时就吃过亏一部分文件用UTF-8解码后文本里有大量替换字符检索时关键词完全匹配不上。推荐用charset-normalizer来做编码识别它的准确率比老牌的chardet高处理中文场景也更稳。基本用法是这样from pathlib import Path from charset_normalizer import from_bytes raw Path(sample.txt).read_bytes() match from_bytes(raw).best() if match is None: raise ValueError(无法识别的编码) text str(match) print(识别编码:, match.encoding)几个补充要点如果识别结果是utf-8但文本开头有\ufeffBOM建议用utf-8-sig重新解码避免第一个字符混入不可见字符。中文场景下GBK和GB18030经常互相误判。实测中charset-normalizer对GB18030的支持不错但如果发现乱码可以手动把编码列表按[utf-8, gb18030, gbk, big5]的顺序尝试解码。不要完全信任识别结果。批量处理时建议抽样打开转换后的文件看两眼重点看中文引号、单位符号、数字上标这类易错字符。2.2 清洗规则哪些字符必须处理掉编码问题解决之后接着要做的是清洗。清洗的目的不是让文本变漂亮而是消除那些会干扰分块和向量化的杂质。我常用的一套规则大致包含这几项统一换行符把\r\n、\r统一成\n去掉零宽字符\u200b、\u200c、\u200d这些不可见字符经常从网页复制文本时混入去掉BOM\ufeff合并多余空格连续空格、行首行尾空格统一处理合并空行多个连续空行压缩成一个保证段落边界清晰剔除内容头部的目录和页眉页脚很多txt从PDF转出来后会带“第X页”“目录”等噪声对应代码如下import re def clean_text(text: str) - str: text text.replace(\r\n, \n).replace(\r, \n) text re.sub(r[\u200b\u200c\u200d\ufeff], , text) text re.sub(r[ \t], , text) text re.sub(r\n\s*\n, \n\n, text) text re.sub(r(第\s*\d\s*页\s*\n?), , text) return text.strip()有一点要提醒清洗规则不能做太死。比如“合并空格”这一步对于代码片段是灾难因为代码块的空格缩进是语义的一部分。正确做法是把代码块先隔离出来或者清洗阶段不要动代码块内部的内容。后面讲Markdown结构化时我会专门处理代码块的保留问题。2.3 先清洗后分块顺序不能反过来很多人会问分块和清洗谁先谁后我的建议永远是先清洗、后分块。理由很直接分块依赖文本的段落边界和标题位置如果脏字符、零宽字符、异常换行没清理干净分块的边界就会错位有时候还会把两个不该挨着的段落粘在一起。举个实际例子。一份文本里大约每隔几行就有个\u200b零宽字符按固定窗口长度去切时切割点可能会落在单词或标点中间后续向量化时这个块和其他块的语义边界是混乱的。先清洗能显著降低这种随机性让分块算法面对的是干净的、有明确段落边界的文本。至于分块策略我看到过很多方案大致能分成三类固定窗口分块按字符数或token数硬切。实现简单但容易切断句子、表格、列表。语义感知分块按段落、句子边界扩展尽量凑满窗口。效果比固定窗口好但没有利用文档结构。标题感知分块先识别标题树再保证每个块不跨越标题边界块与章节层级严格对齐。这是目前文档型RAG里我最推荐的做法。在这个系列第一篇里我不会直接让大家跳到第3种方案去写复杂逻辑而是建议先走一条稳妥路线txt清洗后先还原Markdown结构再在Markdown的AST上做标题感知分块。这样既保留了文档层级又把“分块”从一场字符串切割游戏变成了一次结构遍历。3. Markdown 作为中间层的结构化实验3.1 为什么我用 Markdown 而不是直接把 txt 切开明确一个理念RAG数据导入时Markdown不是最终存储格式而是中间的交换格式。我选择它作为结构化解的中间层理由有四个。第一可读性。Markdown是纯文本清洗、分块的结果可以直接用编辑器打开检查出了问题能定位到具体某一行。相比之下直接输出JSON数组虽然方便程序但人眼很难判断“这个块的内容是否完整”。第二可解析性。Markdown有一堆成熟的解析器比如markdown-it-py能把文本变成带类型的token流。标题、段落、列表、表格、代码块都有明确的边界这是纯txt不具备的。第三可追溯性。Markdown标题的层级天然对应文档的目录结构。从“第一章/1.1/背景”这种路径可以直接映射到原始txt的字符偏移位置。检索结果需要引用溯源时这条路径就是现成的证据链。第四和LLM的亲和性。现在的主流模型对Markdown格式的理解能力很强把Markdown片段拼进提示词模型能很快识别出标题、列表、表格之间的关系减少格式噪声。所以在我的流程里通用文本处理到结构化解之间永远有一道“Markdown牌缓冲区”。它不参与最终的向量化但所有结构化信息都以它为基准生成。3.2 从 txt 自动生成 Markdown 轮廓的可行思路这一步的目标是让一份纯txt具备基本Markdown轮廓至少包括标题层级、列表、表格、代码块标记。完全自动化很难做到完美但能覆盖大多数中文技术文档和说明手册。标题识别是最核心的一步。我总结了几种常见的中文文档标题模式中文序号式第一章、第1章、一、、一数字层级式1.1、1.1.1、2.3这种小标题无编号短语标题像背景、目标、实施方案这类独立成行且下一行空行的短句用正则做初步识别代码思路如下import re HEADING_PATTERNS [ re.compile(r^\s*(第[一二三四五六七八九十百千万\d][章部节卷].*)$), re.compile(r^\s*((?:\d\.)\d)\s*(.*)$), re.compile(r^\s*([一二三四五六七八九十])、(.*)$), ] def detect_heading(line: str): for pat in HEADING_PATTERNS: m pat.match(line) if m: return ## line.strip() return None识别出标题之后再结合段落判断就可以把文字行的层级关系映射成Markdown的#、##、###。大标题用##而不是#是因为很多Markdown解析器把#当作文档主标题而RAG分块时通常不希望主标题参与层级计算。列表和表格的处理相对简单行首以-、*、数字加点开头的内容保留为列表整行以|开头且连续多行出现的保留为Markdown表格。代码块则以三个反引号作为成对标记。这里有一个提示自动转换只保证“基本可读”不会比你人工整理更精确。如果导入的txt是扫描OCR出来的连标题模式都可能不规整那就需要先做OCR修正这部分属于后几篇的内容。3.3 Markdown 解析后的块对象长什么样Markdown轮廓生成后下一步是把它解析成结构化对象。我用的解析库是markdown-it-py它是Python生态里对CommonMark语法支持比较全的解析器。解析后会得到一组token每个token有类型、标签、内容等信息。遍历token流时重点关注几类tokenheading_open/heading_close标题开始和结束tag字段是h1、h2这样的层级inline普通行内文本content字段就是文本内容本身bullet_list_open/ordered_list_open与其close列表边界table_open/table_close表格边界fence代码块内容一个简化版的遍历逻辑大致是from markdown_it import MarkdownIt def markdown_to_blocks(md_text: str): md MarkdownIt(commonmark, {html: False}).enable(table) tokens md.parse(md_text) blocks [] current_heading None buffer [] for tok in tokens: if tok.type heading_open: if buffer or current_heading: blocks.append({ heading: current_heading, content: \n.join(buffer).strip() }) level int(tok.tag[1]) current_heading f{# * level} buffer [] elif tok.type inline: if current_heading and tok.content: buffer.append(tok.content) elif tok.type fence: buffer.append(tok.content) if buffer or current_heading: blocks.append({ heading: current_heading, content: \n.join(buffer).strip() }) return blocks这样解析出来的块标题路径和正文内容都保留了下来。如果你还需要精确的字符串偏移可以在遍历时用tok.map拿到token在源Markdown中的行号区间再换算成原始txt的字符偏移。把这些信息放进块对象里就是下一章要说的结构化输出协议。4. 结构化输出协议让解析结果能被 RAG 链路直接消费4.1 块对象字段设计和元数据约定解析只是一半工作另一半是把结果组织成统一协议。我建议每个块对象固定包含下面这些字段字段类型说明block_idstring全局唯一块ID推荐由文档ID加标题哈希组成doc_idstring来源文档唯一标识heading_pathstring从根到当前块的标题路径例如“第2章/2.3 配置说明”heading_levelinteger当前块所属标题层级根文档为0contentstring块的实际文本内容content_typestringtext / list / table / code / imageparent_idstring/null父块ID用于上下文重组char_rangearray在原始txt中的起止偏移[start, end]metadataobject来源文件、修改日期、语言、页码、标题别名等附加信息字段不是拍脑袋定的。heading_path是为检索后的引用溯源准备的parent_id是为了让一个被召回的块能快速拼上父章节内容char_range则是为了后期人工核查和去重。你可以按需裁剪但核心建议是至少保留doc_id、heading_path、content_type这三项否则后续想优化召回上下文时你会发现缺了太多信息。4.2 标题树、父块关系与引用完整性结构化输出时最需要注意的是块不能跨越标题边界。这个原则叫“引用完整性”。一个块只属于一个标题节点这样检索到某个块就能通过heading_path知道它来自文档的哪个章节也能顺着父块和子块组合出更完整的上下文。构建标题树的做法是维护一个栈。遇到h1就清空栈遇到h2就把栈压到二级遇到h3压到三级普通段落则归属于当前栈顶标题。伪代码逻辑如下stack [] # 存储当前标题路径 for block in markdown_to_blocks(md_text): level block[level] stack [h for h in stack if h[level] level] [block[heading]] block[heading_path] /.join(stack) block[parent_id] stack[-2][id] if len(stack) 2 else None这样处理之后每个块都自带完整的层级上下文。检索阶段如果发现某个块内容偏短可以顺藤摸瓜把父块一并喂给生成模型比单纯堆多个召回块要更可控。4.3 输出格式选型JSON、Markdown 还是两者都要有人会问既然Markdown已经是结构化中间层直接入库向量化不行吗我的建议是交两份结果出去。Markdown版本给人看、给LLM看适合做人工校验和生成提示词片段。JSON版本给程序看适合做向量化、元数据聚合、后续的图结构构建。向量化时不是只对content做embedding更好的做法是构造一段带语义前缀的文本比如把heading_path拼在正文前面第2章/2.3 配置说明\n block[content]。这样检索时的相似度计算会额外关注标题语义对“按章节找内容”的查询特别有效。另外要区分清楚我这里说的结构化解和知识图谱里的结构化不是一回事。RAG知识库和KG知识库是两种路线前者靠向量检索逼近语义后者靠实体关系精确查询。文档结构化解是RAG和KG都能受益的前置步骤它不要求你建立实体关系只需要把文档自身的目录层级还原好后续想要建图也能从这些块里抽取实体。5. 一套可直接复用的本地解析流程5.1 准备环境与工具清单聊到实际操作我建议先用最小依赖跑通不要一上来就上unstructured、LlamaIndex、LangChain全家桶。前期核心工具清单大致如下工具用途Python 3.10运行环境charset-normalizertxt编码识别markdown-it-pyMarkdown解析提取ASTtiktoken可选token计数验证块大小jieba或简单分词可选检索链路验证阶段辅助召回测试这些库直接用pip安装即可不会引入太重的外部依赖pip install charset-normalizer markdown-it-py tiktoken5.2 从 txt 到 Markdown 再到 JSON 的完整脚本下面给出一段可复用的流水线脚本逻辑分四步读文件识别编码、清洗文本、生成Markdown轮廓、解析成块对象并输出JSON。import json import re from pathlib import Path from charset_normalizer import from_bytes from markdown_it import MarkdownIt def read_txt(path: Path): raw path.read_bytes() match from_bytes(raw).best() if match is None: raise ValueError(f无法识别文件编码: {path}) return str(match) def clean_text(text: str) - str: text text.replace(\r\n, \n).replace(\r, \n) text re.sub(r[\u200b\u200c\u200d\ufeff], , text) text re.sub(r[ \t], , text) text re.sub(r\n\s*\n, \n\n, text) return text.strip() HEADING_PATTERNS [ re.compile(r^\s*(第[一二三四五六七八九十百千万\d][章部节卷].*)$), re.compile(r^\s*((?:\d\.)\d)\s*(.*)$), re.compile(r^\s*([一二三四五六七八九十])、(.*)$), ] def txt_to_markdown(text: str) - str: lines text.split(\n) md_lines [] in_code False for line in lines: if line.strip().startswith(): in_code not in_code md_lines.append(line) continue if not in_code: heading None for pat in HEADING_PATTERNS: m pat.match(line) if m: heading ## line.strip() break if heading: md_lines.append(heading) else: md_lines.append(line) else: md_lines.append(line) return \n.join(md_lines) def markdown_to_blocks(md_text: str): md MarkdownIt(commonmark, {html: False}).enable(table) tokens md.parse(md_text) blocks [] level 0 content_buffer [] for tok in tokens: if tok.type heading_open: if content_buffer: blocks.append({ heading_level: level, content: \n.join(content_buffer).strip() }) content_buffer [] level int(tok.tag[1]) elif tok.type inline: content_buffer.append(tok.content) elif tok.type fence: content_buffer.append(tok.content) if content_buffer: blocks.append({ heading_level: level, content: \n.join(content_buffer).strip() }) return blocks def build_heading_path(blocks): stack [] for b in blocks: level b[heading_level] stack [s for s in stack if s[0] level] [(level, b[content].split(\n)[0])] b[heading_path] /.join([s[1] for s in stack]) return blocks def process_txt(path: Path): text read_txt(path) text clean_text(text) md_text txt_to_markdown(text) blocks markdown_to_blocks(md_text) blocks build_heading_path(blocks) return { doc_id: path.stem, source: str(path), blocks: blocks } if __name__ __main__: result process_txt(Path(input.txt)) Path(output.json).write_text( json.dumps(result, ensure_asciiFalse, indent2), encodingutf-8 )这套脚本是“最小可用版本”离生产级还有距离但足够让你把手头txt看明白并确认结构化解的方向对不对。表格和复杂列表的完整处理可以在跑通之后再迭代补充。5.3 入库验证与检索测试拿到JSON之后别急着接向量库先做一个低成本的召回测试。我用TF-IDF加余弦相似度就能初步判断分块质量from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity blocks result[blocks] contents [f{b[heading_path]}\n{b[content]} for b in blocks] query 设备告警阈值在哪里配置 vectorizer TfidfVectorizer() corpus contents [query] matrix vectorizer.fit_transform(corpus) sims cosine_similarity(matrix[-1], matrix[:-1])[0] top3 sims.argsort()[::-1][:3] for idx in top3: print(round(float(sims[idx]), 3), blocks[idx][heading_path])这一步不是为了真正评估语义检索效果而是检查分块结果里有没有明显错位。如果前三名返回的块横跨了多个无关章节说明标题路径或分块边界还有问题需要回到第三步调整Markdown轮廓或者检查清洗阶段是否把标题和正文粘在了一起。6. 我踩过的坑和给你的一点建议6.1 编码误判导致的静默截断最容易坑人的一个问题是文本编码误判后没有抛错而是悄悄产生乱码。比如GBK文件被识别成UTF-8时解码结果里会出现一堆替换字符但程序不会中断后续分块照跑向量化也不报错。最典型的症状就是你肉眼打开原始txt能看到关键词向量检索却永远召不回这一段。解决思路有两层。第一层是在读文件时做双重校验比如解码后统计替换字符比例超过阈值就改用下一种编码重试。第二层是在检索侧加一个抽查脚本定期选几个已知关键词验证它们在解析结果里是否存在。我在本地知识库上线头两周全靠这种抽查捞回了三份编码异常的文档。6.2 表格和列表被拆碎后的检索灾难固定窗口分块在遇到表格和列表时特别容易翻车。表格的一行被单独切成一个块检索时可能只命中“阈值 80”这种孤零零的数字完全丢失表头和上下文。列表也是一样一个三层嵌套列表被拆成七八个碎片语义已经支离破碎。我的建议是在解析阶段就把表格识别为一整块不要对表格内部做切割。如果表格太长超过token上限再按行拆分但每一行都要把表头字段拼接进去。列表同理把整个bullet_list或ordered_list视为一个块除非列表特别长否则不拆。这个思路对应到Markdown AST就是对table_open到table_close、bullet_list_open到bullet_list_close的整体区间做操作。6.3 关于 RAG 数据解析的边界认知最后说一点容易被人忽略的边界认知。不是所有文本都值得“结构化”。比如聊天记录、日记、随手笔记本身没有严谨的标题树强行识别章节反而会制造虚假层级。对这种数据按语义段落切块可能比按标题切块更合理。也不要指望一套流程通吃所有格式。PDF带扫描图片、多模态内容、复杂公式都需要在前面加OCR或特殊解析器那超出了本篇的讨论范围。市面上有很多本地RAG文本拆解工具比如unstructured、LlamaIndex的解析器、LangChain 的MarkdownHeaderTextSplitter它们各有擅长。我个人的习惯是在数据量几千个文件以内先用本篇这套轻量脚本跑通流程理解自己的文档结构到底长什么样再决定要不要上重型工具。直接套框架容易陷入“工具很复杂但我根本没搞清楚自己的数据哪里出了问题”的尴尬。断断续续跑了半年RAG实验之后我的体会是解析链路值得花时间但别本末倒置。先用“txt → Markdown → 块对象”这条轻量管线把数据看懂把检索跑通再逐层替换组件也不迟。下一篇系列里我会接着讲PDF和扫描件这类更难处理的格式以及表格向量化的具体做法。如果你正在搭RAG知识库建议先从手头那批txt开始把上面的脚本跑一遍看看你手里的文档到底能还原出多少结构——这一步的收益往往比换一个更大的模型来得实在。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号