1. Markdown格式简介 #
1.1 什么是Markdown #
Markdown是一种轻量级标记语言(Markup Language),由John Gruber于2004年创建。它允许用户使用简单的语法编写结构化文档,而无需依赖复杂的排版工具。
简单理解:
- Markdown是纯文本文件(
.md或.markdown扩展名) - 使用特殊符号(如
#、*、-等)来表示格式 - 可以转换为HTML、PDF等格式
- 广泛用于文档编写、README文件、博客等
1.2 Markdown基本语法 #
下面介绍Markdown的常用语法,这些知识有助于理解解析过程。
1.2.1 标题 #
Markdown使用#符号表示标题,#的数量表示标题级别:
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题重要特点:
- 标题有明显的层级关系(一级 > 二级 > 三级...)
- 层级关系对于文档分块非常重要
- 相同层级的标题通常属于同一主题
1.2.2 文本样式 #
**加粗文本**
*斜体文本*
***加粗+斜体***
~~删除线~~
`行内代码`1.2.3 列表 #
无序列表(使用-、*或+):
- 项目 1
- 项目 2
- 子项目 2.1
- 子项目 2.2有序列表(使用数字):
1. 第一步
2. 第二步
1. 子步骤 2.1
2. 子步骤 2.21.2.4 表格 #
Markdown表格使用|分隔列,-分隔表头和数据:
| 列1 | 列2 | 列3 |
|----|----|----|
| 数据1 | 数据2 | 数据3 |
| 数据4 | 数据5 | 数据6 |表格特点:
- 表格是Markdown中的重要结构化数据
- 需要单独提取和处理
- 可以转换为HTML格式
1.2.5 代码块 #
行内代码:使用单个反引号
这是`行内代码`示例代码块:使用三个反引号
def hello():
print("Hello, Markdown!")1.2.6 其他元素 #
- 链接:
[文本](URL) - 图片:
 - 引用:使用
>符号 - 分割线:使用
、***或___
1.3 为什么需要了解Markdown语法 #
了解Markdown语法有助于:
- 理解解析过程:知道解析器在识别什么
- 调试问题:遇到解析问题时知道可能的原因
- 优化分块:利用层级关系进行更好的分块
2. RAGFlow Markdown解析基础 #
2.1 什么是Markdown解析器 #
RAGFlow的Markdown解析器使用markdown库来解析Markdown文件。它可以将Markdown文档转换为结构化的数据,提取文本、表格等元素。
主要功能:
- 读取Markdown文件内容
- 提取表格数据
- 分离文本和表格
- 支持分块处理
2.2 基础使用示例 #
下面是一个完整的使用示例:
# 导入必要的库
import os
import sys
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.abspath('.'))
# 导入RAGFlow的Markdown解析器
from rag.app.naive import Markdown, chunk
# 定义回调函数(用于显示进度,可选)
def dummy(prog=None, msg=""):
if msg:
print(f"进度: {msg}")
# 定义Markdown文件路径(请替换为你的Markdown文件路径)
md_file = './data/markdown_ai.md'
# 方法一:直接解析(不进行分块)
print("方法一:直接解析")
md_parser = Markdown()
# 解析Markdown文件
# 返回格式:(sections, tables)
# sections: 段落列表,每个元素是(文本, 样式)的元组
# tables: 表格列表
sections, tbls = md_parser(md_file)
# 处理段落内容
print("\n========== 段落内容 ==========")
texts = []
for sec in sections:
if sec[0]: # 只处理非空段落
texts.append(sec[0])
# 打印所有文本段落
for idx, text in enumerate(texts, 1):
print(f"\n 段落 {idx} ")
print(text[:200] + "..." if len(text) > 200 else text) # 只显示前200个字符
# 处理表格内容
print("\n========== 表格内容 ==========")
print(f"共找到 {len(tbls)} 个表格")
for idx, tbl in enumerate(tbls, 1):
print(f"\n 表格 {idx} ")
# tbl格式:((None, HTML表格), "")
if tbl[0] and tbl[0][1]:
print(tbl[0][1][:300] + "..." if len(tbl[0][1]) > 300 else tbl[0][1])
# 方法二:解析并自动分块
print("\n方法二:解析并分块")
parser_config = {
"chunk_token_num": 256, # 每个chunk的token数量
"delimiter": "\n!?。;!?" # 分块的分隔符
}
chunks = chunk(md_file, callback=dummy, parser_config=parser_config)
# 查看分块结果
print(f"\n共生成 {len(chunks)} 个chunk")
for idx, data in enumerate(chunks[:5], 1): # 只显示前5个
print(f"\n Chunk {idx} ")
print(data['content_with_weight'][:200] + "...") # 只显示前200个字符代码说明:
- 直接解析:使用
Markdown类直接解析,返回段落和表格 - 解析并分块:使用
chunk函数,自动进行分块处理 - 结果格式:
sections:段落列表tbls:表格列表(HTML格式)
2.3 重要注意事项 #
2.3.1 层级关系未充分利用 #
RAGFlow的基础Markdown解析器在文本分块时没有充分利用Markdown的层级关系。这意味着:
- 不同层级的标题内容可能被分到同一个chunk
- 相同层级的标题内容可能被分开
- 可能破坏文档的逻辑结构
解决方案:使用自定义解析器,按标题层级进行分块(见后面章节)。
2.3.2 表格处理 #
基础解析器会将表格转换为HTML格式,但表格和文本是分开处理的,可能缺失上下文。
3. 自定义Markdown解析器 #
3.1 为什么需要自定义解析器 #
虽然RAGFlow提供了基础Markdown解析器,但在实际应用中,你可能需要:
- 利用层级关系:按标题层级进行分块,保持文档结构
- 表格总结:使用LLM对表格进行总结,支持分析型问题
- 表格明细:将表格的每一行作为独立的chunk,支持精确检索
- 智能分块:先按层级分块,如果块太大再递归分割
3.2 自定义解析器的实现思路 #
我们的自定义解析器将实现以下功能:
- 提取表格:从Markdown文本中提取表格
- 分离文本和表格:将文本和表格分开处理
- 表格处理:对表格进行总结和按行分块
- 层级分块:使用
MarkdownHeaderTextSplitter按标题层级分块 - 递归分割:如果某个层级块太大,使用
RecursiveCharacterTextSplitter进一步分割
3.3 完整实现示例 #
下面是一个完整的自定义Markdown解析器实现:
# 导入必要的库
import os
import sys
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.abspath('.'))
# 导入RAGFlow的基础解析器
from deepdoc.parser import MarkdownParser
# 导入Markdown处理相关
from markdown import markdown
# 导入LangChain相关
from langchain_core.documents import Document as LDocument
from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
# 导入工具函数(需要自己实现或从utils导入)
from utils import table_to_summary
from rag.utils import num_tokens_from_string
from rag.nlp import find_codec
class MyMarkDown(MarkdownParser):
"""
自定义Markdown解析器
功能:
1. 提取表格并总结
2. 按标题层级分块
3. 智能递归分割
"""
def __call__(self, filename, binary=None):
"""
解析Markdown文件,提取文本和表格
参数:
filename: Markdown文件路径
binary: Markdown二进制数据(可选)
返回:
tuple: (段落列表, 表格列表)
"""
# 步骤1: 读取Markdown文件内容
txt = ""
if binary:
# 如果是二进制数据,需要先检测编码
encoding = find_codec(binary)
txt = binary.decode(encoding, errors="ignore")
else:
# 从文件读取
with open(filename, "r", encoding="utf-8") as f:
txt = f.read()
# 步骤2: 提取表格和剩余文本
# extract_tables_and_remainder方法会:
# - 识别Markdown表格(标准格式和HTML格式)
# - 从文本中移除表格
# - 返回剩余文本和表格列表
remainder, tables = self.extract_tables_and_remainder(f'{txt}\n')
# 步骤3: 处理剩余文本,按行分割
sections = []
for sec in remainder.split("\n"):
# 如果某行文本太长(超过10倍chunk大小),进行分割
if num_tokens_from_string(sec) > 10 * self.chunk_token_num:
# 将长文本分成两半
sections.append((sec[:int(len(sec)/2)], ""))
sections.append((sec[int(len(sec)/2):], ""))
else:
# 正常长度的文本直接添加
sections.append((sec, ""))
# 步骤4: 处理表格,转换为HTML格式
tbls = []
for table in tables:
# 使用markdown库将Markdown表格转换为HTML
# extensions=['markdown.extensions.tables']启用表格扩展
html_table = markdown(table, extensions=['markdown.extensions.tables'])
# 格式:((None, HTML表格), "")
tbls.append(((None, html_table), ""))
return sections, tbls
def chunk(self, filename, binary=None, llm=None,
chunk_size=512, chunk_overlap=30):
"""
解析Markdown文件并生成Document列表
参数:
filename: Markdown文件路径
binary: Markdown二进制数据
llm: 语言模型实例(用于表格总结)
chunk_size: 文本chunk大小
chunk_overlap: chunk重叠大小
返回:
list: LangChain Document列表
"""
# 步骤1: 解析Markdown文件,获取段落和表格
sections, tbls = self(filename, binary)
# 步骤2: 提取文本段落
txt_chunks = []
for sec in sections:
if sec[0]: # 只处理非空段落
txt_chunks.append(sec[0])
# 步骤3: 提取表格内容
table_chunks = []
for sec in tbls:
# sec格式:((None, HTML表格), "")
if sec[0] and sec[0][1]:
table_chunks.append(sec[0][1])
# 如果没有提供LLM,创建一个(需要从model导入)
if llm is None:
from model import RagLLM
llm = RagLLM()
docs = []
# 步骤4: 处理表格(总结和按行分块)
print("正在处理表格...")
for table_info in table_chunks:
# 使用LLM对表格进行总结
table_summary = table_to_summary(table_info, llm)
# 创建表格总结Document
# page_content是总结,metadata中保存原始表格内容
doc = LDocument(
page_content=table_summary,
metadata={
"source": "table",
"content": table_info # 原始表格内容保存在metadata中
}
)
docs.append(doc)
# 步骤5: 处理文本(按标题层级分块)
print("正在处理文本...")
# 步骤5.1: 定义要分割的标题层级
# headers_to_split_on是一个列表,每个元素是(标题符号, 标题名称)的元组
# 这里定义了一级、二级、三级标题
headers_to_split_on = [
("#", "Header 1"), # 一级标题
("##", "Header 2"), # 二级标题
("###", "Header 3"), # 三级标题
]
# 步骤5.2: 合并所有文本段落
all_texts = "\n".join(txt_chunks)
# 步骤5.3: 使用MarkdownHeaderTextSplitter按标题层级分块
# strip_headers=False表示保留标题在chunk中
r_spliter = MarkdownHeaderTextSplitter(
headers_to_split_on,
strip_headers=False
)
# 按标题层级分割文本
# 返回的每个Document会包含:
# - page_content: 该标题下的内容
# - metadata: 包含标题信息(Header 1, Header 2等)
md_header_splits = r_spliter.split_text(all_texts)
# 步骤5.4: 如果某个层级块太大,进行递归分割
# 使用RecursiveCharacterTextSplitter进行进一步分割
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size, # 每个chunk的最大大小
chunk_overlap=chunk_overlap # chunk之间的重叠大小
)
# 对每个层级块进行分割
# 如果块小于chunk_size,则保持不变
# 如果块大于chunk_size,则进一步分割
chunks = text_splitter.split_documents(md_header_splits)
# 步骤5.5: 为每个chunk添加元数据
for chunk in chunks:
chunk.metadata['source'] = 'text'
chunk.metadata['content'] = ''
docs.append(chunk)
print(f"处理完成!共生成 {len(docs)} 个Document")
return docs
# 使用示例
if __name__ == "__main__":
# 定义Markdown文件路径
md_file = './data/markdown_ai.md'
# 创建自定义解析器实例
md_parser = MyMarkDown()
# 解析并分块
from model import RagLLM
llm = RagLLM()
print("开始解析Markdown文件...")
documents = md_parser.chunk(
md_file,
llm=llm,
chunk_size=512,
chunk_overlap=30
)
# 查看结果统计
text_count = sum(1 for doc in documents if doc.metadata['source'] == 'text')
table_count = sum(1 for doc in documents if doc.metadata['source'] == 'table')
print(f"\n解析完成!")
print(f"文本chunks: {text_count} 个")
print(f"表格chunks: {table_count} 个")
print(f"总计: {len(documents)} 个Document")
# 查看示例结果
print("\n========== 示例结果 ==========")
for idx, doc in enumerate(documents[:5], 1):
print(f"\n Document {idx} ({doc.metadata['source']}) ")
# 显示标题信息(如果有)
if 'Header 1' in doc.metadata:
print(f"一级标题: {doc.metadata.get('Header 1', '')}")
if 'Header 2' in doc.metadata:
print(f"二级标题: {doc.metadata.get('Header 2', '')}")
if 'Header 3' in doc.metadata:
print(f"三级标题: {doc.metadata.get('Header 3', '')}")
content_preview = doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content
print(f"内容: {content_preview}")3.4 关键功能说明 #
3.4.1 表格提取 #
Markdown解析器使用正则表达式提取表格:
- 标准Markdown表格:识别
|列1|列2|格式的表格 - HTML表格:识别
<table>...</table>格式的表格 - 提取后移除:从文本中移除表格,避免重复处理
3.4.2 层级分块 #
MarkdownHeaderTextSplitter的工作原理:
- 识别标题:根据定义的标题符号(
#、##等)识别标题 - 按层级分割:在每个标题处分割文本
- 保留标题:将标题信息保存在metadata中
- 层级关系:相同层级的标题内容会被分在一起
示例:
# 第一章
这是第一章的内容
## 1.1 节
这是1.1节的内容
## 1.2 节
这是1.2节的内容
# 第二章
这是第二章的内容分块结果:
- Chunk 1: "第一章" + "这是第一章的内容" + "1.1 节" + "这是1.1节的内容" + "1.2 节" + "这是1.2节的内容"
- Chunk 2: "第二章" + "这是第二章的内容"
3.4.3 递归分割 #
如果某个层级块太大(超过chunk_size),RecursiveCharacterTextSplitter会进一步分割:
- 优先在分隔符处分割:如
\n\n、\n、.等 - 保持重叠:相邻chunk之间有
chunk_overlap大小的重叠 - 保持语义:尽量在语义边界处分割
4. 完整使用示例 #
下面是一个完整的示例,演示如何使用自定义解析器:
# 导入必要的库
import os
import sys
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.abspath('.'))
# 导入自定义解析器(假设已经保存为my_markdown_parser.py)
from my_markdown_parser import MyMarkDown
from model import RagLLM
# 定义Markdown文件路径
md_file = './data/markdown_ai.md'
# 创建LLM实例(用于表格总结)
llm = RagLLM()
# 创建自定义解析器实例
md_parser = MyMarkDown()
# 解析Markdown文件并生成Document列表
print("开始解析Markdown文件...")
documents = md_parser.chunk(
md_file,
llm=llm,
chunk_size=512,
chunk_overlap=30
)
# 查看结果统计
text_count = sum(1 for doc in documents if doc.metadata['source'] == 'text')
table_count = sum(1 for doc in documents if doc.metadata['source'] == 'table')
print(f"\n解析完成!")
print(f"文本chunks: {text_count} 个")
print(f"表格chunks: {table_count} 个")
print(f"总计: {len(documents)} 个Document")
# 查看示例结果
print("\n========== 示例结果 ==========")
for idx, doc in enumerate(documents[:5], 1):
print(f"\n Document {idx} ({doc.metadata['source']}) ")
# 显示标题层级信息
if 'Header 1' in doc.metadata:
print(f"一级标题: {doc.metadata.get('Header 1', '')}")
if 'Header 2' in doc.metadata:
print(f"二级标题: {doc.metadata.get('Header 2', '')}")
if 'Header 3' in doc.metadata:
print(f"三级标题: {doc.metadata.get('Header 3', '')}")
content_preview = doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content
print(f"内容: {content_preview}")
if doc.metadata.get('content'):
print(f"原始内容: {doc.metadata['content'][:100]}...")
# 可以保存到向量数据库
# from langchain_chroma import Chroma
# from model import RagEmbedding
#
# embedding_model = RagEmbedding()
# vector_db = Chroma.from_documents(
# documents,
# embedding_model.get_embedding_fun(),
# collection_name="my_markdown_collection"
# )5. MarkdownHeaderTextSplitter详解 #
5.1 什么是MarkdownHeaderTextSplitter #
MarkdownHeaderTextSplitter是LangChain提供的专门用于按Markdown标题层级分块的工具。
工作原理:
- 识别Markdown文档中的标题(
#、##等) - 在每个标题处分割文档
- 将标题信息保存在Document的metadata中
- 保持标题层级关系
5.2 使用示例 #
# 导入必要的库
from langchain_text_splitters import MarkdownHeaderTextSplitter
# 定义要分割的标题层级
# 格式:(标题符号, 标题名称)
headers_to_split_on = [
("#", "Header 1"), # 一级标题
("##", "Header 2"), # 二级标题
("###", "Header 3"), # 三级标题
]
# 创建分割器
# strip_headers=False表示保留标题在chunk中
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=False # 如果设为True,会移除标题,只保留内容
)
# 要分割的Markdown文本
markdown_text = """
# 第一章 介绍
这是第一章的内容。
## 1.1 背景
这是背景部分的内容。
## 1.2 目标
这是目标部分的内容。
# 第二章 方法
这是第二章的内容。
"""
# 执行分割
documents = splitter.split_text(markdown_text)
# 查看结果
for idx, doc in enumerate(documents, 1):
print(f"\n Document {idx} ")
print(f"标题信息: {doc.metadata}")
print(f"内容: {doc.page_content}")输出示例:
Document 1
标题信息: {'Header 1': '第一章 介绍'}
内容: # 第一章 介绍
这是第一章的内容。
## 1.1 背景
这是背景部分的内容。
## 1.2 目标
这是目标部分的内容。
Document 2
标题信息: {'Header 1': '第二章 方法'}
内容: # 第二章 方法
这是第二章的内容。5.3 参数说明 #
headers_to_split_on:要分割的标题层级列表
- 格式:
[("#", "Header 1"), ("##", "Header 2"), ...] - 只会在定义的层级处分割
- 格式:
strip_headers:是否移除标题
False:保留标题在chunk中True:移除标题,只保留内容
6. 总结与最佳实践 #
6.1 Markdown解析 vs 其他格式解析 #
| 特性 | Markdown解析 | PDF解析 | Word解析 | Excel解析 | ||-||-|--| | 数据格式 | 纯文本 | 渲染后的图片 | 结构化文档 | 结构化表格 | | OCR需求 | 不需要 | 需要 | 不需要 | 不需要 | | 层级关系 | 明确(标题) | 需要识别 | 需要识别 | 无 | | 表格处理 | 文本格式 | 需要识别 | 直接读取 | 直接读取 | | 分块策略 | 可按层级 | 按位置 | 按段落 | 按行 |
6.2 常见问题处理 #
问题1:层级关系未充分利用
- 原因:基础解析器按行分割,不考虑层级
- 解决方案:使用
MarkdownHeaderTextSplitter按标题层级分块
问题2:表格缺失上下文
- 原因:表格和文本分开处理
- 解决方案:在自定义解析器中为表格添加前后文本作为上下文
问题3:代码块处理
- 原因:代码块可能被当作普通文本处理
- 解决方案:可以在解析时识别代码块,单独处理或跳过
6.3 分块策略建议 #
按标题层级分块:
- 优点:保持文档逻辑结构
- 适用:有明确层级结构的文档
递归分割:
- 优点:处理大块内容
- 适用:层级块太大的情况
表格单独处理:
- 优点:支持总结和明细两种检索方式
- 适用:包含表格的文档
6.4 性能优化建议 #
- 选择合适的层级:只分割需要的标题层级,避免过度分割
- 调整chunk_size:根据实际需求调整chunk大小
- 缓存结果:解析结果可以缓存,避免重复解析