package kb import ( "archive/zip" "bytes" "encoding/xml" "fmt" "io" "strings" ) // ======================================================================== // DOCX 解析器:从 Word (.docx) 字节流抽取纯文本 // ======================================================================== // 选型说明: // - docx 本质是 zip 包,正文全部在 word/document.xml 里, // 用标准库 archive/zip + encoding/xml 流式解析即可,零第三方依赖 // - 不用 unioffice(AGPL 许可证,商用需付费授权) // - 老版二进制 .doc 是 OLE 复合文档格式,纯 Go 解析成本极高, // 明确不支持,提示用户用 Word 另存为 .docx // ======================================================================== // extractDocxText 抽取 docx 正文文本 // // 解析规则(只认元素局部名,忽略 w: 命名空间前缀): // 文本节点,累加内容 // 段落结束时补双换行(配合 ChunkPlainText 按双换行切段) // 手动换行 → \n // 制表符 → \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 里的字符,跳过样式等噪音) 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 }