Files
xk-ai-agent/internal/kb/parser_docx.go
2026-08-14 21:50:48 +08:00

94 lines
2.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package kb
import (
"archive/zip"
"bytes"
"encoding/xml"
"fmt"
"io"
"strings"
)
// ========================================================================
// DOCX 解析器:从 Word (.docx) 字节流抽取纯文本
// ========================================================================
// 选型说明:
// - docx 本质是 zip 包,正文全部在 word/document.xml 里,
// 用标准库 archive/zip + encoding/xml 流式解析即可,零第三方依赖
// - 不用 uniofficeAGPL 许可证,商用需付费授权)
// - 老版二进制 .doc 是 OLE 复合文档格式,纯 Go 解析成本极高,
// 明确不支持,提示用户用 Word 另存为 .docx
// ========================================================================
// extractDocxText 抽取 docx 正文文本
//
// 解析规则(只认元素局部名,忽略 w: 命名空间前缀):
// <w:t> 文本节点,累加内容
// <w:p> 段落结束时补双换行(配合 ChunkPlainText 按双换行切段)
// <w:br> 手动换行 → \n
// <w:tab> 制表符 → \t
//
// 表格w:tbl里的单元格内容也是 w:p 段落,天然会被上面的规则覆盖,
// 不需要单独处理表格结构
func extractDocxText(data []byte) (string, error) {
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return "", fmt.Errorf("kb: 打开 docx 失败(若为老版 .doc 请用 Word 另存为 .docx: %w", err)
}
// 定位正文文件 word/document.xml
var docFile *zip.File
for _, f := range zr.File {
if f.Name == "word/document.xml" {
docFile = f
break
}
}
if docFile == nil {
return "", fmt.Errorf("kb: docx 中找不到 word/document.xml不是有效的 Word 文档")
}
rc, err := docFile.Open()
if err != nil {
return "", fmt.Errorf("kb: 读取 docx 正文失败: %w", err)
}
defer rc.Close()
dec := xml.NewDecoder(rc)
var sb strings.Builder
inText := false // 是否处于 <w:t> 内部(只收集 w:t 里的字符,跳过样式等噪音)
for {
tok, terr := dec.Token()
if terr == io.EOF {
break
}
if terr != nil {
return "", fmt.Errorf("kb: 解析 docx XML 失败: %w", terr)
}
switch t := tok.(type) {
case xml.StartElement:
switch t.Name.Local {
case "t":
inText = true
case "br", "cr":
sb.WriteString("\n")
case "tab":
sb.WriteString("\t")
}
case xml.EndElement:
switch t.Name.Local {
case "t":
inText = false
case "p":
// 段落边界 → 空行,供 ChunkPlainText 按段落切分
sb.WriteString("\n\n")
}
case xml.CharData:
if inText {
sb.Write(t)
}
}
}
return strings.TrimSpace(sb.String()), nil
}