3 Star 0 Fork 1

Gitee 极速下载 / tealeg-xlsx

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
此仓库是为了提升国内下载速度的镜像仓库,每日同步一次。 原始仓库: https://github.com/tealeg/xlsx
克隆/下载
file.go 20.37 KB
一键复制 编辑 原始数据 按行查看 历史
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
package xlsx
import (
"archive/zip"
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
)
// File is a high level structure providing a slice of Sheet structs
// to the user.
type File struct {
worksheets map[string]*zip.File
worksheetRels map[string]*zip.File
referenceTable *RefTable
Date1904 bool
styles *xlsxStyleSheet
Sheets []*Sheet
Sheet map[string]*Sheet
theme *theme
DefinedNames []*xlsxDefinedName
cellStoreConstructor CellStoreConstructor
rowLimit int
colLimit int
valueOnly bool
}
const NoRowLimit int = -1
const NoColLimit int = -1
type FileOption func(f *File)
// RowLimit will limit the rows handled in any given sheet to the
// first n, where n is the number of rows.
func RowLimit(n int) FileOption {
return func(f *File) {
f.rowLimit = n
}
}
// ColLimit will limit the columns handled in any given sheet to the
// first n, where n is the number of columns
func ColLimit(n int) FileOption {
return func(f *File) {
f.colLimit = n
}
}
// ValueOnly treats all NULL values as meaningless and it will delete all NULL value cells,
// before decode worksheet.xml. this option can save memory and time when parsing files
// with a large number of NULL values. But it may also cause accidental injury,
// because NULL may not really be meaningless. Use with caution
func ValueOnly() FileOption {
return func(f *File) {
f.valueOnly = true
}
}
// NewFile creates a new File struct. You may pass it zero, one or
// many FileOption functions that affect the behaviour of the file.
func NewFile(options ...FileOption) *File {
f := &File{
Sheet: make(map[string]*Sheet),
Sheets: make([]*Sheet, 0),
DefinedNames: make([]*xlsxDefinedName, 0),
rowLimit: NoRowLimit,
colLimit: NoColLimit,
cellStoreConstructor: NewMemoryCellStore,
}
for _, opt := range options {
opt(f)
}
return f
}
// OpenFile will take the name of an XLSX file and returns a populated
// xlsx.File struct for it. You may pass it zero, one or
// many FileOption functions that affect the behaviour of the file.
func OpenFile(fileName string, options ...FileOption) (file *File, err error) {
wrap := func(err error) (*File, error) {
return nil, fmt.Errorf("OpenFile: %w", err)
}
var z *zip.ReadCloser
z, err = zip.OpenReader(fileName)
if err != nil {
return wrap(err)
}
defer z.Close()
file, err = ReadZip(z, options...)
if err != nil {
return wrap(err)
}
return file, nil
}
// OpenBinary() take bytes of an XLSX file and returns a populated
// xlsx.File struct for it.
func OpenBinary(bs []byte, options ...FileOption) (*File, error) {
r := bytes.NewReader(bs)
return OpenReaderAt(r, int64(r.Len()), options...)
}
// OpenReaderAt() take io.ReaderAt of an XLSX file and returns a populated
// xlsx.File struct for it.
func OpenReaderAt(r io.ReaderAt, size int64, options ...FileOption) (*File, error) {
file, err := zip.NewReader(r, size)
if err != nil {
return nil, err
}
return ReadZipReader(file, options...)
}
// A convenient wrapper around File.ToSlice, FileToSlice will
// return the raw data contained in an Excel XLSX file as three
// dimensional slice. The first index represents the sheet number,
// the second the row number, and the third the cell number.
//
// For example:
//
// var mySlice [][][]string
// var value string
// mySlice = xlsx.FileToSlice("myXLSX.xlsx")
// value = mySlice[0][0][0]
//
// Here, value would be set to the raw value of the cell A1 in the
// first sheet in the XLSX file.
func FileToSlice(path string, options ...FileOption) ([][][]string, error) {
f, err := OpenFile(path, options...)
if err != nil {
return nil, err
}
return f.ToSlice()
}
// FileToSliceUnmerged is a wrapper around File.ToSliceUnmerged.
// It returns the raw data contained in an Excel XLSX file as three
// dimensional slice. Merged cells will be unmerged. Covered cells become the
// values of theirs origins.
func FileToSliceUnmerged(path string, options ...FileOption) ([][][]string, error) {
f, err := OpenFile(path, options...)
if err != nil {
return nil, err
}
return f.ToSliceUnmerged()
}
// Save the File to an xlsx file at the provided path.
func (f *File) Save(path string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("File.Save(%s): %w", path, err)
}
}()
target, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if ie := target.Close(); ie != nil {
err = fmt.Errorf("write:%+v close:%w", err, ie)
}
}()
err = f.Write(target)
return
}
// Write the File to io.Writer as xlsx
func (f *File) Write(writer io.Writer) error {
wrap := func(err error) error {
return fmt.Errorf("File.Write: %w", err)
}
zipWriter := zip.NewWriter(writer)
err := f.MarshallParts(zipWriter)
if err != nil {
return wrap(err)
}
err = zipWriter.Close()
if err != nil {
return wrap(err)
}
return nil
}
// AddSheet Add a new Sheet, with the provided name, to a File.
// The minimum sheet name length is 1 character. If the sheet name length is less an error is thrown.
// The maximum sheet name length is 31 characters. If the sheet name length is exceeded an error is thrown.
// These special characters are also not allowed: : \ / ? * [ ]
func (f *File) AddSheet(sheetName string) (*Sheet, error) {
return f.AddSheetWithCellStore(sheetName, f.cellStoreConstructor)
}
func (f *File) AddSheetWithCellStore(sheetName string, constructor CellStoreConstructor) (*Sheet, error) {
var err error
if _, exists := f.Sheet[sheetName]; exists {
return nil, fmt.Errorf("duplicate sheet name '%s'.", sheetName)
}
if err := IsSaneSheetName(sheetName); err != nil {
return nil, fmt.Errorf("sheet name is not valid: %w", err)
}
sheet := &Sheet{
Name: sheetName,
File: f,
Selected: len(f.Sheets) == 0,
Cols: &ColStore{},
cellStoreName: sheetName,
}
sheet.cellStore, err = constructor()
if err != nil {
return nil, err
}
f.Sheet[sheetName] = sheet
f.Sheets = append(f.Sheets, sheet)
return sheet, nil
}
// Appends an existing Sheet, with the provided name, to a File
func (f *File) AppendSheet(sheet Sheet, sheetName string) (*Sheet, error) {
if _, exists := f.Sheet[sheetName]; exists {
return nil, fmt.Errorf("duplicate sheet name '%s'.", sheetName)
}
if err := IsSaneSheetName(sheetName); err != nil {
return nil, fmt.Errorf("sheet name is not valid: %w", err)
}
sheet.Name = sheetName
sheet.File = f
sheet.Selected = len(f.Sheets) == 0
f.Sheet[sheetName] = &sheet
f.Sheets = append(f.Sheets, &sheet)
return &sheet, nil
}
func (f *File) makeWorkbook() xlsxWorkbook {
return xlsxWorkbook{
FileVersion: xlsxFileVersion{AppName: "Go XLSX"},
WorkbookPr: xlsxWorkbookPr{ShowObjects: "all"},
BookViews: xlsxBookViews{
WorkBookView: []xlsxWorkBookView{
{
ShowHorizontalScroll: true,
ShowSheetTabs: true,
ShowVerticalScroll: true,
TabRatio: 204,
WindowHeight: 8192,
WindowWidth: 16384,
XWindow: "0",
YWindow: "0",
},
},
},
Sheets: xlsxSheets{Sheet: make([]xlsxSheet, len(f.Sheets))},
CalcPr: xlsxCalcPr{
IterateCount: 100,
RefMode: "A1",
Iterate: false,
IterateDelta: 0.001,
},
}
}
// Some tools that read XLSX files have very strict requirements about
// the structure of the input XML. In particular both Numbers on the Mac
// and SAS dislike inline XML namespace declarations, or namespace
// prefixes that don't match the ones that Excel itself uses. This is a
// problem because the Go XML library doesn't multiple namespace
// declarations in a single element of a document. This function is a
// horrible hack to fix that after the XML marshalling is completed.
func replaceRelationshipsNameSpace(workbookMarshal string) string {
newWorkbook := strings.Replace(workbookMarshal, `xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships:id`, `r:id`, -1)
// Dirty hack to fix issues #63 and #91; encoding/xml currently
// "doesn't allow for additional namespaces to be defined in the
// root element of the document," as described by @tealeg in the
// comments for #63.
oldXmlns := `<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`
newXmlns := `<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">`
return strings.Replace(newWorkbook, oldXmlns, newXmlns, 1)
}
func addRelationshipNameSpaceToWorksheet(worksheetMarshal string) string {
oldXmlns := `<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`
newXmlns := `<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">`
newSheetMarshall := strings.Replace(worksheetMarshal, oldXmlns, newXmlns, 1)
oldHyperlink := `<hyperlink id=`
newHyperlink := `<hyperlink r:id=`
newSheetMarshall = strings.Replace(newSheetMarshall, oldHyperlink, newHyperlink, -1)
return newSheetMarshall
}
func cellIDStringWithFixed(cellIDString string) string {
letterPart := strings.Map(letterOnlyMapF, cellIDString)
intPart := strings.Map(intOnlyMapF, cellIDString)
if letterPart != "" && intPart == "" {
return fixedCellRefChar + letterPart
} else if letterPart != "" && intPart != "" {
return fixedCellRefChar + letterPart + fixedCellRefChar + intPart
}
return ""
}
// AutoFilter doesn't work in LibreOffice unless a special "FilterDatabase" tag
// is present in the "DefinedNames" array. See:
// - https://github.com/SheetJS/sheetjs/issues/1165
// - https://bugs.documentfoundation.org/show_bug.cgi?id=118592
func autoFilterDefinedName(sheet *Sheet, sheetIndex int) (*xlsxDefinedName, error) {
if sheet.AutoFilter == nil {
return nil, nil
}
return &xlsxDefinedName{
Data: fmt.Sprintf(
"'%s'!%v:%v",
strings.ReplaceAll(sheet.Name, "'", "''"),
cellIDStringWithFixed(sheet.AutoFilter.TopLeftCell),
cellIDStringWithFixed(sheet.AutoFilter.BottomRightCell),
),
Name: "_xlnm._FilterDatabase",
LocalSheetID: sheetIndex - 1,
Hidden: true,
}, nil
}
// MakeStreamParts constructs a map of file name to XML content
// representing the file in terms of the structure of an XLSX file.
func (f *File) MakeStreamParts() (map[string]string, error) {
var parts map[string]string
var refTable *RefTable = NewSharedStringRefTable()
refTable.isWrite = true
var workbookRels WorkBookRels = make(WorkBookRels)
var err error
var workbook xlsxWorkbook
var types xlsxTypes = MakeDefaultContentTypes()
marshal := func(thing interface{}) (string, error) {
body, err := xml.Marshal(thing)
if err != nil {
return "", err
}
return xml.Header + string(body), nil
}
parts = make(map[string]string)
workbook = f.makeWorkbook()
sheetIndex := 1
if f.styles == nil {
f.styles = newXlsxStyleSheet(f.theme)
}
f.styles.reset()
if len(f.Sheets) == 0 {
err := errors.New("Workbook must contains atleast one worksheet")
return nil, err
}
for _, sheet := range f.Sheets {
// Make sure we don't lose the current state!
err := sheet.cellStore.WriteRow(sheet.currentRow)
if err != nil {
return nil, err
}
xSheetRels := sheet.makeXLSXSheetRelations()
xSheet := sheet.makeXLSXSheet(refTable, f.styles, xSheetRels)
rId := fmt.Sprintf("rId%d", sheetIndex)
sheetId := strconv.Itoa(sheetIndex)
sheetPath := fmt.Sprintf("worksheets/sheet%d.xml", sheetIndex)
partName := "xl/" + sheetPath
relPartName := fmt.Sprintf("xl/worksheets/_rels/sheet%d.xml.rels", sheetIndex)
sheetState := sheetStateVisible
if sheet.Hidden {
sheetState = sheetStateHidden
}
types.Overrides = append(
types.Overrides,
xlsxOverride{
PartName: "/" + partName,
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"})
workbookRels[rId] = sheetPath
workbook.Sheets.Sheet[sheetIndex-1] = xlsxSheet{
Name: sheet.Name,
SheetId: sheetId,
Id: rId,
State: sheetState}
worksheetMarshal, err := marshal(xSheet)
if err != nil {
return parts, err
}
worksheetMarshal = addRelationshipNameSpaceToWorksheet(worksheetMarshal)
parts[partName] = worksheetMarshal
if xSheetRels != nil {
parts[relPartName], err = marshal(xSheetRels)
if err != nil {
return parts, err
}
}
definedName, err := autoFilterDefinedName(sheet, sheetIndex)
if err != nil {
return parts, err
} else if definedName != nil {
workbook.DefinedNames.DefinedName = append(workbook.DefinedNames.DefinedName, *definedName)
}
sheetIndex++
}
for _, dn := range f.DefinedNames {
workbook.DefinedNames.DefinedName = append(workbook.DefinedNames.DefinedName, *dn)
}
workbookMarshal, err := marshal(workbook)
if err != nil {
return parts, err
}
workbookMarshal = replaceRelationshipsNameSpace(workbookMarshal)
parts["xl/workbook.xml"] = workbookMarshal
if err != nil {
return parts, err
}
parts["_rels/.rels"] = TEMPLATE__RELS_DOT_RELS
parts["docProps/app.xml"] = TEMPLATE_DOCPROPS_APP
// TODO - do this properly, modification and revision information
parts["docProps/core.xml"] = TEMPLATE_DOCPROPS_CORE
parts["xl/theme/theme1.xml"] = TEMPLATE_XL_THEME_THEME
xSST := refTable.makeXLSXSST()
parts["xl/sharedStrings.xml"], err = marshal(xSST)
if err != nil {
return parts, err
}
xWRel := workbookRels.MakeXLSXWorkbookRels()
parts["xl/_rels/workbook.xml.rels"], err = marshal(xWRel)
if err != nil {
return parts, err
}
parts["[Content_Types].xml"], err = marshal(types)
if err != nil {
return parts, err
}
parts["xl/styles.xml"], err = f.styles.Marshal()
if err != nil {
return parts, err
}
return parts, nil
}
// MarshallParts constructs a map of file name to XML content representing the file
// in terms of the structure of an XLSX file.
func (f *File) MarshallParts(zipWriter *zip.Writer) error {
var refTable *RefTable = NewSharedStringRefTable()
refTable.isWrite = true
var workbookRels WorkBookRels = make(WorkBookRels)
var err error
var workbook xlsxWorkbook
var types xlsxTypes = MakeDefaultContentTypes()
wrap := func(err error) error {
return fmt.Errorf("MarshallParts: %w", err)
}
marshal := func(thing interface{}) (string, error) {
body, err := xml.Marshal(thing)
if err != nil {
return "", fmt.Errorf("xml.Marshal: %w", err)
}
return xml.Header + string(body), nil
}
writePart := func(partName, part string) error {
w, err := zipWriter.Create(partName)
if err != nil {
return fmt.Errorf("zipwriter.Create(%s): %w", partName, err)
}
_, err = w.Write([]byte(part))
if err != nil {
return fmt.Errorf("zipwriter.Write(%s): %w", part, err)
}
return nil
}
// parts = make(map[string]string)
workbook = f.makeWorkbook()
sheetIndex := 1
if f.styles == nil {
f.styles = newXlsxStyleSheet(f.theme)
}
f.styles.reset()
if len(f.Sheets) == 0 {
err := errors.New("MarshalParts: Workbook must contain at least one worksheet")
return wrap(err)
}
for _, sheet := range f.Sheets {
if sheet.currentRow != nil {
// Make sure we don't lose the current state!
err := sheet.cellStore.WriteRow(sheet.currentRow)
if err != nil {
return wrap(err)
}
}
xSheetRels := sheet.makeXLSXSheetRelations()
rId := fmt.Sprintf("rId%d", sheetIndex)
sheetId := strconv.Itoa(sheetIndex)
sheetPath := fmt.Sprintf("worksheets/sheet%d.xml", sheetIndex)
partName := "xl/" + sheetPath
relPartName := fmt.Sprintf("xl/worksheets/_rels/sheet%d.xml.rels", sheetIndex)
types.Overrides = append(
types.Overrides,
xlsxOverride{
PartName: "/" + partName,
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"})
workbookRels[rId] = sheetPath
workbook.Sheets.Sheet[sheetIndex-1] = xlsxSheet{
Name: sheet.Name,
SheetId: sheetId,
Id: rId,
State: sheet.getState()}
w, err := zipWriter.Create(partName)
if err != nil {
return wrap(err)
}
err = sheet.MarshalSheet(w, refTable, f.styles, xSheetRels)
if err != nil {
return wrap(err)
}
if xSheetRels != nil {
relPart, err := marshal(xSheetRels)
if err != nil {
return wrap(err)
}
err = writePart(relPartName, relPart)
if err != nil {
return wrap(err)
}
}
definedName, err := autoFilterDefinedName(sheet, sheetIndex)
if err != nil {
return wrap(err)
} else if definedName != nil {
workbook.DefinedNames.DefinedName = append(workbook.DefinedNames.DefinedName, *definedName)
}
sheetIndex++
}
for _, dn := range f.DefinedNames {
workbook.DefinedNames.DefinedName = append(workbook.DefinedNames.DefinedName, *dn)
}
workbookMarshal, err := marshal(workbook)
if err != nil {
return err
}
workbookMarshal = replaceRelationshipsNameSpace(workbookMarshal)
err = writePart("xl/workbook.xml", workbookMarshal)
if err != nil {
return err
}
err = writePart("_rels/.rels", TEMPLATE__RELS_DOT_RELS)
if err != nil {
return err
}
err = writePart("docProps/app.xml", TEMPLATE_DOCPROPS_APP)
if err != nil {
return err
}
// TODO - do this properly, modification and revision information
err = writePart("docProps/core.xml", TEMPLATE_DOCPROPS_CORE)
if err != nil {
return err
}
err = writePart("xl/theme/theme1.xml", TEMPLATE_XL_THEME_THEME)
if err != nil {
return err
}
xSST := refTable.makeXLSXSST()
sharedStrings, err := marshal(xSST)
if err != nil {
return err
}
err = writePart("xl/sharedStrings.xml", sharedStrings)
if err != nil {
return err
}
xWRel := workbookRels.MakeXLSXWorkbookRels()
relPart, err := marshal(xWRel)
if err != nil {
return err
}
err = writePart("xl/_rels/workbook.xml.rels", relPart)
if err != nil {
return err
}
typesS, err := marshal(types)
if err != nil {
return err
}
err = writePart("[Content_Types].xml", typesS)
if err != nil {
return err
}
styles, err := f.styles.Marshal()
if err != nil {
return err
}
return writePart("xl/styles.xml", styles)
}
// Return the raw data contained in the File as three
// dimensional slice. The first index represents the sheet number,
// the second the row number, and the third the cell number.
//
// For example:
//
// var mySlice [][][]string
// var value string
// mySlice = xlsx.FileToSlice("myXLSX.xlsx")
// value = mySlice[0][0][0]
//
// Here, value would be set to the raw value of the cell A1 in the
// first sheet in the XLSX file.
func (f *File) ToSlice() (output [][][]string, err error) {
output = [][][]string{}
for _, sheet := range f.Sheets {
s := [][]string{}
err := sheet.ForEachRow(func(row *Row) error {
r := []string{}
err := row.ForEachCell(func(cell *Cell) error {
str, err := cell.FormattedValue()
if err != nil {
// Recover from strconv.NumError if the value is an empty string,
// and insert an empty string in the output.
if numErr, ok := err.(*strconv.NumError); ok && numErr.Num == "" {
str = ""
} else {
return err
}
}
r = append(r, str)
return nil
})
if err != nil {
return err
}
s = append(s, r)
return nil
})
if err != nil {
return output, err
}
output = append(output, s)
}
return output, nil
}
// ToSliceUnmerged returns the raw data contained in the File as three
// dimensional slice (s. method ToSlice).
// A covered cell become the value of its origin cell.
// Example: table where A1:A2 at row 0 and row 1 are merged.
// | 2011 | Bread | 20 |
// | | Fish | 70 |
// | 2012 | 2013 | Egg | 80 |
// This sheet will be converted to the slice:
// [
//
// [2011 2011 Bread 20]
// [2011 2011 Fish 70]
// [2012 2013 Egg 80]
//
// ]
func (f *File) ToSliceUnmerged() (output [][][]string, err error) {
output, err = f.ToSlice()
if err != nil {
return nil, err
}
for s, sheet := range f.Sheets {
err := sheet.ForEachRow(func(row *Row) error {
return row.ForEachCell(func(cell *Cell) error {
if cell.HMerge > 0 || cell.VMerge > 0 {
c, r := cell.GetCoordinates()
v := output[s][r][c]
for i := r; i <= r+cell.VMerge; i++ {
for j := c; j <= c+cell.HMerge; j++ {
if i != r || j != c {
output[s][i][j] = v
}
}
}
}
return nil
})
})
if err != nil {
return output, err
}
}
return output, nil
}
type DefinedName xlsxDefinedName
// AddDefinedName adds a new Name definition to the workbook.
func (f *File) AddDefinedName(name DefinedName) error {
definedName := xlsxDefinedName(name)
f.DefinedNames = append(f.DefinedNames, &definedName)
return nil
}
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/mirrors/tealeg-xlsx.git
git@gitee.com:mirrors/tealeg-xlsx.git
mirrors
tealeg-xlsx
tealeg-xlsx
master

搜索帮助

344bd9b3 5694891 D2dac590 5694891