update
This commit is contained in:
585
internal/excel/loader.go
Normal file
585
internal/excel/loader.go
Normal file
@@ -0,0 +1,585 @@
|
||||
package excel
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SheetInfo struct {
|
||||
Name string
|
||||
SheetID int
|
||||
RID string
|
||||
Path string
|
||||
}
|
||||
|
||||
type XLSXReader struct {
|
||||
Files map[string][]byte
|
||||
SheetMap map[string]SheetInfo
|
||||
}
|
||||
|
||||
type SharedStrings struct {
|
||||
Strings []string `xml:"si>t"`
|
||||
}
|
||||
|
||||
type Sheet struct {
|
||||
Name string `xml:"name,attr"`
|
||||
SheetID int `xml:"sheetId,attr"`
|
||||
Ref string `xml:"id,attr"`
|
||||
}
|
||||
|
||||
type Sheets struct {
|
||||
Sheets []Sheet `xml:"sheet"`
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
Cells []Cell `xml:"c"`
|
||||
RowNum int `xml:"r,attr"`
|
||||
}
|
||||
|
||||
type Cell struct {
|
||||
Ref string `xml:"r,attr"`
|
||||
Type string `xml:"t,attr"`
|
||||
Val string `xml:"v"`
|
||||
}
|
||||
|
||||
type Worksheet struct {
|
||||
Rows []Row `xml:"sheetData>row"`
|
||||
}
|
||||
|
||||
func sanitizeFileName(name string) string {
|
||||
name = strings.ReplaceAll(name, "/", "_")
|
||||
name = strings.ReplaceAll(name, "\\", "_")
|
||||
name = strings.ReplaceAll(name, ":", "_")
|
||||
name = strings.ReplaceAll(name, "*", "_")
|
||||
name = strings.ReplaceAll(name, "?", "_")
|
||||
name = strings.ReplaceAll(name, "\"", "_")
|
||||
name = strings.ReplaceAll(name, "<", "_")
|
||||
name = strings.ReplaceAll(name, ">", "_")
|
||||
name = strings.ReplaceAll(name, "|", "_")
|
||||
return name
|
||||
}
|
||||
|
||||
func NewXLSXReader(filePath string) (*XLSXReader, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开文件失败: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取文件信息失败: %v", err)
|
||||
}
|
||||
|
||||
fileBytes, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取文件失败: %v", err)
|
||||
}
|
||||
|
||||
return NewXLSXReaderFromBytes(fileBytes)
|
||||
}
|
||||
|
||||
func NewXLSXReaderFromBytes(data []byte) (*XLSXReader, error) {
|
||||
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建zip读取器失败: %v", err)
|
||||
}
|
||||
|
||||
files := make(map[string][]byte)
|
||||
for _, file := range zr.File {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开内部文件失败: %v", err)
|
||||
}
|
||||
content, err := io.ReadAll(rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取内部文件失败: %v", err)
|
||||
}
|
||||
files[file.Name] = content
|
||||
}
|
||||
|
||||
reader := &XLSXReader{Files: files}
|
||||
reader.parseWorkbook()
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (r *XLSXReader) parseWorkbook() {
|
||||
r.SheetMap = make(map[string]SheetInfo)
|
||||
|
||||
// 收集所有可用的worksheet文件
|
||||
var availableSheets []string
|
||||
for file := range r.Files {
|
||||
if strings.HasPrefix(file, "xl/worksheets/sheet") && strings.HasSuffix(file, ".xml") {
|
||||
availableSheets = append(availableSheets, file)
|
||||
}
|
||||
}
|
||||
fmt.Printf("✓ 发现 %d 个可用的worksheet文件: %v\n", len(availableSheets), availableSheets)
|
||||
|
||||
// 读取rels文件获取正确的映射关系
|
||||
ridToPath := r.readWorkbookRels()
|
||||
fmt.Printf("✓ 从rels文件读取到 %d 个映射关系\n", len(ridToPath))
|
||||
|
||||
content, ok := r.Files["xl/workbook.xml"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
var sheetNames []string
|
||||
idx := 0
|
||||
for {
|
||||
sheetStart := strings.Index(contentStr[idx:], "<sheet ")
|
||||
if sheetStart == -1 {
|
||||
break
|
||||
}
|
||||
sheetStart += idx
|
||||
|
||||
nameAttrStart := strings.Index(contentStr[sheetStart:], `name="`)
|
||||
if nameAttrStart == -1 {
|
||||
idx = sheetStart + 6
|
||||
continue
|
||||
}
|
||||
nameAttrStart += sheetStart + 6
|
||||
nameAttrEnd := strings.Index(contentStr[nameAttrStart:], `"`)
|
||||
if nameAttrEnd == -1 {
|
||||
idx = nameAttrStart + 1
|
||||
continue
|
||||
}
|
||||
nameAttrEnd += nameAttrStart
|
||||
name := contentStr[nameAttrStart:nameAttrEnd]
|
||||
|
||||
sheetID := 0
|
||||
sheetIdAttr := strings.Index(contentStr[nameAttrEnd:], `sheetId="`)
|
||||
if sheetIdAttr != -1 {
|
||||
sheetIdAttr += nameAttrEnd + 9
|
||||
sheetIdEnd := strings.Index(contentStr[sheetIdAttr:], `"`)
|
||||
if sheetIdEnd != -1 {
|
||||
sheetIdEnd += sheetIdAttr
|
||||
if id, err := strconv.Atoi(contentStr[sheetIdAttr:sheetIdEnd]); err == nil {
|
||||
sheetID = id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rid := ""
|
||||
ridAttr := strings.Index(contentStr[nameAttrEnd:], `r:id="`)
|
||||
if ridAttr != -1 {
|
||||
ridAttr += nameAttrEnd + 6
|
||||
ridEnd := strings.Index(contentStr[ridAttr:], `"`)
|
||||
if ridEnd != -1 {
|
||||
ridEnd += ridAttr
|
||||
rid = contentStr[ridAttr:ridEnd]
|
||||
}
|
||||
}
|
||||
|
||||
path := ""
|
||||
if rid != "" && ridToPath[rid] != "" {
|
||||
path = ridToPath[rid]
|
||||
if r.Files[path] == nil {
|
||||
path = ""
|
||||
}
|
||||
}
|
||||
|
||||
if path == "" && rid != "" {
|
||||
sheetNum := strings.TrimPrefix(rid, "rId")
|
||||
path = fmt.Sprintf("xl/worksheets/sheet%s.xml", sheetNum)
|
||||
if r.Files[path] == nil {
|
||||
path = ""
|
||||
}
|
||||
}
|
||||
|
||||
if path == "" && sheetID > 0 {
|
||||
path = fmt.Sprintf("xl/worksheets/sheet%d.xml", sheetID)
|
||||
if r.Files[path] == nil {
|
||||
path = ""
|
||||
}
|
||||
}
|
||||
|
||||
if path == "" && len(availableSheets) > 0 {
|
||||
usedPaths := make(map[string]bool)
|
||||
for _, info := range r.SheetMap {
|
||||
usedPaths[info.Path] = true
|
||||
}
|
||||
for _, availPath := range availableSheets {
|
||||
if !usedPaths[availPath] {
|
||||
path = availPath
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sheetNames = append(sheetNames, name)
|
||||
r.SheetMap[name] = SheetInfo{
|
||||
Name: name,
|
||||
SheetID: sheetID,
|
||||
RID: rid,
|
||||
Path: path,
|
||||
}
|
||||
|
||||
idx = nameAttrEnd + 1
|
||||
}
|
||||
|
||||
fmt.Printf("✓ 工作簿中定义了 %d 个工作表\n", len(sheetNames))
|
||||
}
|
||||
|
||||
func (r *XLSXReader) readWorkbookRels() map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
content, ok := r.Files["xl/_rels/workbook.xml.rels"]
|
||||
if !ok {
|
||||
fmt.Println("⚠️ 找不到 xl/_rels/workbook.xml.rels 文件")
|
||||
return result
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
idx := 0
|
||||
for {
|
||||
relStart := strings.Index(contentStr[idx:], `<Relationship `)
|
||||
if relStart == -1 {
|
||||
break
|
||||
}
|
||||
relStart += idx
|
||||
|
||||
idAttr := strings.Index(contentStr[relStart:], `Id="`)
|
||||
if idAttr == -1 {
|
||||
idx = relStart + 14
|
||||
continue
|
||||
}
|
||||
idAttr += relStart + 4
|
||||
idEnd := strings.Index(contentStr[idAttr:], `"`)
|
||||
if idEnd == -1 {
|
||||
idx = idAttr + 1
|
||||
continue
|
||||
}
|
||||
idEnd += idAttr
|
||||
rid := contentStr[idAttr:idEnd]
|
||||
|
||||
targetAttr := strings.Index(contentStr[idEnd:], `Target="`)
|
||||
if targetAttr == -1 {
|
||||
idx = idEnd + 1
|
||||
continue
|
||||
}
|
||||
targetAttr += idEnd + 8
|
||||
targetEnd := strings.Index(contentStr[targetAttr:], `"`)
|
||||
if targetEnd == -1 {
|
||||
idx = targetAttr + 1
|
||||
continue
|
||||
}
|
||||
targetEnd += targetAttr
|
||||
target := contentStr[targetAttr:targetEnd]
|
||||
|
||||
if strings.HasPrefix(target, "worksheets/") {
|
||||
result[rid] = "xl/" + target
|
||||
}
|
||||
|
||||
idx = targetEnd + 1
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *XLSXReader) GetSheetNames() ([]string, error) {
|
||||
var names []string
|
||||
for name := range r.SheetMap {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (r *XLSXReader) GetSheetData(sheetName string) ([][]string, error) {
|
||||
sheetInfo, ok := r.SheetMap[sheetName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("找不到工作表: %s", sheetName)
|
||||
}
|
||||
|
||||
if sheetInfo.Path == "" {
|
||||
return nil, fmt.Errorf("无法确定工作表 %s 的文件路径", sheetName)
|
||||
}
|
||||
|
||||
sheetContent, ok := r.Files[sheetInfo.Path]
|
||||
if !ok {
|
||||
// 尝试查找所有worksheet文件
|
||||
fmt.Printf("⚠️ 找不到文件: %s\n", sheetInfo.Path)
|
||||
fmt.Println(" 可用的worksheet文件:")
|
||||
for file := range r.Files {
|
||||
if strings.HasPrefix(file, "xl/worksheets/") {
|
||||
fmt.Printf(" - %s\n", file)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("找不到工作表文件: %s", sheetInfo.Path)
|
||||
}
|
||||
|
||||
sharedStrings := r.loadSharedStrings()
|
||||
fmt.Printf("✓ 加载到 %d 个共享字符串\n", len(sharedStrings))
|
||||
|
||||
return r.parseSheetData(string(sheetContent), sharedStrings), nil
|
||||
}
|
||||
|
||||
func (r *XLSXReader) loadSharedStrings() []string {
|
||||
content, ok := r.Files["xl/sharedStrings.xml"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
var sharedStrings []string
|
||||
|
||||
siIdx := 0
|
||||
for {
|
||||
siStart := strings.Index(contentStr[siIdx:], "<si>")
|
||||
if siStart == -1 {
|
||||
break
|
||||
}
|
||||
siStart += siIdx
|
||||
|
||||
siEnd := strings.Index(contentStr[siStart:], "</si>")
|
||||
if siEnd == -1 {
|
||||
break
|
||||
}
|
||||
siEnd += siStart
|
||||
|
||||
siContent := contentStr[siStart+4 : siEnd]
|
||||
|
||||
var text strings.Builder
|
||||
tIdx := 0
|
||||
for {
|
||||
tStart := strings.Index(siContent[tIdx:], "<t")
|
||||
if tStart == -1 {
|
||||
break
|
||||
}
|
||||
tStart += tIdx
|
||||
|
||||
tCloseTag := strings.Index(siContent[tStart:], ">")
|
||||
if tCloseTag == -1 {
|
||||
break
|
||||
}
|
||||
tCloseTag += tStart
|
||||
|
||||
tEnd := strings.Index(siContent[tCloseTag:], "</t>")
|
||||
if tEnd == -1 {
|
||||
break
|
||||
}
|
||||
tEnd += tCloseTag
|
||||
|
||||
text.WriteString(siContent[tCloseTag+1 : tEnd])
|
||||
tIdx = tEnd + 4
|
||||
}
|
||||
|
||||
sharedStrings = append(sharedStrings, text.String())
|
||||
siIdx = siEnd + 5
|
||||
}
|
||||
|
||||
return sharedStrings
|
||||
}
|
||||
|
||||
func (r *XLSXReader) parseSheetData(contentStr string, sharedStrings []string) [][]string {
|
||||
sheetDataStart := strings.Index(contentStr, "<sheetData>")
|
||||
if sheetDataStart == -1 {
|
||||
return nil
|
||||
}
|
||||
sheetDataEnd := strings.Index(contentStr[sheetDataStart:], "</sheetData>")
|
||||
if sheetDataEnd == -1 {
|
||||
return nil
|
||||
}
|
||||
sheetDataEnd += sheetDataStart
|
||||
sheetData := contentStr[sheetDataStart+11 : sheetDataEnd]
|
||||
|
||||
var result [][]string
|
||||
maxCol := 0
|
||||
|
||||
rowIdx := 0
|
||||
for {
|
||||
rowStart := strings.Index(sheetData[rowIdx:], "<row ")
|
||||
if rowStart == -1 {
|
||||
break
|
||||
}
|
||||
rowStart += rowIdx
|
||||
|
||||
rowEnd := strings.Index(sheetData[rowStart:], "</row>")
|
||||
if rowEnd == -1 {
|
||||
break
|
||||
}
|
||||
rowEnd += rowStart
|
||||
|
||||
rowContent := sheetData[rowStart : rowEnd+6]
|
||||
|
||||
rowNumber := len(result) + 1
|
||||
rowNumAttr := strings.Index(rowContent, ` r="`)
|
||||
if rowNumAttr != -1 {
|
||||
rowNumAttr += 4
|
||||
rowNumEnd := strings.Index(rowContent[rowNumAttr:], `"`)
|
||||
if rowNumEnd != -1 {
|
||||
if rn, err := strconv.Atoi(rowContent[rowNumAttr : rowNumAttr+rowNumEnd]); err == nil {
|
||||
for len(result) < rn-1 {
|
||||
result = append(result, []string{})
|
||||
}
|
||||
rowNumber = rn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rowData := []string{}
|
||||
|
||||
cellIdx := 0
|
||||
for {
|
||||
cellStart := strings.Index(rowContent[cellIdx:], "<c ")
|
||||
if cellStart == -1 {
|
||||
break
|
||||
}
|
||||
cellStart += cellIdx
|
||||
|
||||
cellEnd := strings.Index(rowContent[cellStart:], "</c>")
|
||||
if cellEnd == -1 {
|
||||
break
|
||||
}
|
||||
cellEnd += cellStart
|
||||
|
||||
cellContent := rowContent[cellStart : cellEnd+4]
|
||||
|
||||
cellRef := ""
|
||||
refAttr := strings.Index(cellContent, ` r="`)
|
||||
if refAttr != -1 {
|
||||
refAttr += 4
|
||||
refEnd := strings.Index(cellContent[refAttr:], `"`)
|
||||
if refEnd != -1 {
|
||||
cellRef = cellContent[refAttr : refAttr+refEnd]
|
||||
}
|
||||
}
|
||||
|
||||
if cellRef == "" {
|
||||
cellIdx = cellEnd + 4
|
||||
continue
|
||||
}
|
||||
|
||||
colIndex := getColumnIndex(cellRef)
|
||||
|
||||
cellType := ""
|
||||
typeAttr := strings.Index(cellContent, ` t="`)
|
||||
if typeAttr != -1 {
|
||||
typeAttr += 4
|
||||
typeEnd := strings.Index(cellContent[typeAttr:], `"`)
|
||||
if typeEnd != -1 {
|
||||
cellType = cellContent[typeAttr : typeAttr+typeEnd]
|
||||
}
|
||||
}
|
||||
|
||||
val := ""
|
||||
vStart := strings.Index(cellContent, "<v>")
|
||||
if vStart != -1 {
|
||||
vStart += 3
|
||||
vEnd := strings.Index(cellContent[vStart:], "</v>")
|
||||
if vEnd != -1 {
|
||||
val = cellContent[vStart : vStart+vEnd]
|
||||
|
||||
if cellType == "s" && sharedStrings != nil {
|
||||
if idx, err := strconv.Atoi(val); err == nil && idx >= 0 && idx < len(sharedStrings) {
|
||||
val = sharedStrings[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for len(rowData) <= colIndex {
|
||||
rowData = append(rowData, "")
|
||||
}
|
||||
rowData[colIndex] = val
|
||||
|
||||
if colIndex > maxCol {
|
||||
maxCol = colIndex
|
||||
}
|
||||
|
||||
cellIdx = cellEnd + 4
|
||||
}
|
||||
|
||||
for len(result) < rowNumber {
|
||||
result = append(result, []string{})
|
||||
}
|
||||
result[rowNumber-1] = rowData
|
||||
|
||||
rowIdx = rowEnd + 6
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
for len(result[i]) <= maxCol {
|
||||
result[i] = append(result[i], "")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getColumnIndex(cellRef string) int {
|
||||
i := 0
|
||||
for i < len(cellRef) {
|
||||
c := cellRef[i]
|
||||
if c < 'A' || c > 'Z' {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
colStr := cellRef[:i]
|
||||
|
||||
col := 0
|
||||
for _, c := range colStr {
|
||||
col = col*26 + int(c-'A') + 1
|
||||
}
|
||||
return col - 1
|
||||
}
|
||||
|
||||
func ConvertToMarkdown(data [][]string) string {
|
||||
if len(data) == 0 {
|
||||
return "表格为空"
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
|
||||
numCols := 0
|
||||
for _, row := range data {
|
||||
if len(row) > numCols {
|
||||
numCols = len(row)
|
||||
}
|
||||
}
|
||||
|
||||
for rowIdx, row := range data {
|
||||
result.WriteString("|")
|
||||
for colIdx := 0; colIdx < numCols; colIdx++ {
|
||||
val := ""
|
||||
if colIdx < len(row) {
|
||||
val = escapeMarkdown(row[colIdx])
|
||||
}
|
||||
result.WriteString(fmt.Sprintf(" %s |", val))
|
||||
}
|
||||
result.WriteString("\n")
|
||||
|
||||
if rowIdx == 0 {
|
||||
result.WriteString("|")
|
||||
for colIdx := 0; colIdx < numCols; colIdx++ {
|
||||
result.WriteString(" --- |")
|
||||
}
|
||||
result.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func escapeMarkdown(text string) string {
|
||||
text = strings.ReplaceAll(text, "\\", "\\\\")
|
||||
text = strings.ReplaceAll(text, "|", "\\|")
|
||||
text = strings.ReplaceAll(text, "[", "\\[")
|
||||
text = strings.ReplaceAll(text, "]", "\\]")
|
||||
text = strings.ReplaceAll(text, "(", "\\(")
|
||||
text = strings.ReplaceAll(text, ")", "\\)")
|
||||
text = strings.ReplaceAll(text, "#", "\\#")
|
||||
text = strings.ReplaceAll(text, "*", "\\*")
|
||||
text = strings.ReplaceAll(text, "_", "\\_")
|
||||
return text
|
||||
}
|
||||
Reference in New Issue
Block a user