文档的加载和转换

2026年4月16日 · 437 字 · 3 分钟

如何使用eino框架加载和转换不同格式的文档

为什么需要文档加载和转换

在构建RAG(检索增强生成)系统时,第一步就是要处理各种格式的文档。企业内部的文档可能是Word、PDF、Excel、HTML等多种格式,我们需要将这些文档加载并转换为统一的文本格式,才能进行后续的向量化和检索。

文档解析

创建解析器

首先创建各种格式的解析器实例:

ctx := context.Background()

// 文本解析器(默认)
textParser := parser.TextParser{}

// Word文档解析器
docParser, err := docx.NewDocxParser(ctx, &docx.Config{
	IncludeTables:  true,   // 包含表格
	IncludeFooters: false,  // 不包含页脚
	IncludeHeaders: false,  // 不包含页眉
})

// HTML解析器
selector := "body"
htmlParser, err := html.NewParser(ctx, &html.Config{
	Selector: &selector,  // 只解析body部分
})

// PDF解析器
pdfParser, err := pdf.NewPDFParser(ctx, &pdf.Config{})

// Excel解析器
excelParser, err := xlsx.NewXlsxParser(ctx, &xlsx.Config{})

统一解析器

使用 ExtParser 将所有解析器统一管理:

// 创建扩展解析器
extParser, err := parser.NewExtParser(ctx,
	&parser.ExtParserConfig{
		Parsers: map[string]parser.Parser{
			".docx": docParser,
			".html": htmlParser,
			".pdf":  pdfParser,
			".xlsx": excelParser,
		},
		FallbackParser: textParser, // 默认使用文本解析器
	},
)

设计优势:

  • 统一接口: 通过文件扩展名自动选择对应的解析器
  • 容错机制: 未配置格式的文档会使用默认的文本解析器
  • 易于扩展: 可以轻松添加新的解析器

解析文档

使用 ParseFile 函数解析文档:

func ParseFile(docPath string) {
	ctx := context.Background()

	// ... 创建解析器代码 ...

	// 打开文件
	file, err := os.Open(docPath)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	// 解析文档
	doc, err := extParser.Parse(ctx, file, parser.WithURI(docPath))
	if err != nil {
		log.Fatal(err)
	}

	// 输出解析结果
	for _, v := range doc {
		fmt.Println(v.Content)
	}
}

测试结果

运行测试可以看到不同格式文档的解析结果:

go test -v -run TestLoadDocument

Markdown文档解析:

# 美国用工政策
## 美国的工资
全国(联邦)最低工资为每小时7.25USD或每月1,160USD。
## 美国的工作时间
标准工作时间为每天8小时,每周40小时,标准工作周为周一至周五。
...

HTML文档解析:

HTML文档会过滤掉标签,只提取文本内容:

美国用工政策 
美国的工资 
全国(联邦)最低工资为每小时7.25USD或每月1,160USD。
美国的工作时间 
标准工作时间为每天8小时,每周40小时,标准工作周为周一至周五。
...

PDF文档解析:

PDF文档会保留原有的排版格式:

美国用工政策
美国的工资
全国(联邦)最低工资为每小时7.25USD或每月1,160USD。
美国的工作时间
标准工作时间为每天8小时,每周40小时,标准工作周为周一至周五。
...

Excel表格解析:

表格数据会以行列形式输出:

Tom	1	4	7
Lily	2	5	8
Jerry	3	6	9

Word文档解析:

Word文档会区分正文和表格:

=== MAIN CONTENT ===
### 观沧海
树木丛生,百草丰茂。
秋风萧瑟,洪波涌起。
=== TABLES ===
| 观沧海  |         |
| -------- | -------- |
|         |         |
| 树木丛生 | 百草丰茂 |
| 秋风萧瑟 | 洪波涌起 |

文档加载

使用FileLoader

除了直接解析,还可以使用 FileLoader 来加载文档:

func LoadDoc(docPath string) {
	ctx := context.Background()

	// ... 创建解析器代码 ...

	// 创建文件加载器
	loader, err := file.NewFileLoader(ctx, &file.FileLoaderConfig{
		Parser:      extParser,
		UseNameAsID: true,  // 使用文件名作为文档ID
	})

	// 加载文档
	docs, err := loader.Load(ctx, document.Source{
		URI: docPath,
	})

	// 输出加载结果
	for _, v := range docs {
		fmt.Println(v.Content)
	}
}

FileLoader vs Parse的区别:

  • Parse: 直接解析文件流,返回文档列表
  • Load: 从指定URI加载文档,可以添加更多元数据信息

文档转换

为什么需要文档分割

在RAG系统中,长文档需要分割成小块才能进行有效的检索:

  1. 提高检索精度: 小块文本的语义更明确
  2. 控制token数量: 避免超出模型的上下文窗口
  3. 提高响应质量: 只检索相关片段,减少噪音

Markdown标题分割

使用 HeaderSplitter 按Markdown标题分割文档:

func TransformDoc(docPath string) {
	// 读取文件内容
	file, err := os.Open(docPath)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	content, err := io.ReadAll(file)
	if err != nil {
		log.Fatal(err)
	}

	// 创建文档对象
	doc := schema.Document{
		Content: string(content),
	}

	ctx := context.Background()

	// 创建标题分割器
	splitTransformer, _ := markdown.NewHeaderSplitter(ctx, &markdown.HeaderConfig{
		Headers: map[string]string{
			"##": "",  // 按##标题分割
		},
		TrimHeaders: true,  // 不包含标题本身
	})

	// 执行分割
	transformedDocs, _ := splitTransformer.Transform(ctx, []*schema.Document{&doc})

	// 输出分割结果
	for k, v := range transformedDocs {
		fmt.Printf("segment %d, content : %s\n", k, v.Content)
	}
}

测试结果

运行测试可以看到文档被按标题分割:

go test -v -run TestTransformDocument

输出结果:

segment 0, content : # 美国用工政策
segment 1, content : 全国(联邦)最低工资为每小时7.25USD或每月1,160USD。
segment 2, content : 标准工作时间为每天8小时,每周40小时,标准工作周为周一至周五。
segment 3, content : 美国9个国家公共假日,元旦:1月1日;马丁路德金日:一月的第三个星期一...
...
segment 14, content : 没有永久居民身份或工作签证的外国人不得在美国工作...

分割原理:

  • 每个二级标题(##)作为分割点
  • TrimHeaders: true 表示分割后的内容不包含标题本身
  • 分割后的每个片段都是一个独立的 schema.Document 对象