Files
wasm-go/internal/excel/loader.go
lychang 7f5350db62 update
2026-05-20 03:25:35 +08:00

586 lines
12 KiB
Go

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
}