586 lines
12 KiB
Go
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
|
|
}
|