excel导入导出代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package tool

import (
    "errors"
    "github.com/360EntSecGroup-Skylar/excelize/v2"
    "mime/multipart"
    "strconv"
)

const (
    maxCharCount     = 26       // maxCharCount 最多26个字符A-Z
    defaultSheetName = "Sheet1" // 默认Sheet名称
)

type Excel struct {
    SheetName string          // sheetName,默认为Sheet1
    Headers   []string        // 表头
    Rows      [][]interface{} // 表数据
}

func (m *Excel) SetDefaultSheetName() {
    if m.SheetName == "" {
        m.SheetName = defaultSheetName
    }
}

// 从文件流读取 Excel Sheet1 的所有数据
func (Excel) ReadExcelFromStream(f *multipart.FileHeader) ([][]string, error) {
    var rows [][]string

    src, err := f.Open()
    if err != nil {
        return rows, err
    }
    defer src.Close()

    // 兆字节转为字节进行比较大小
    maxFileSize := int64(2) // 默认最大上传 2 M
    if f.Size > maxFileSize*1e6 {
        return rows, errors.New("文件大小不能超过 " + strconv.FormatInt(maxFileSize, 10) + " M")
    }

    ff, err := excelize.OpenReader(src)
    if err != nil {
        if err.Error() == "zip: not a valid zip file" {
            return rows, errors.New("无效文件,注意不支持加密的EXCEL文件")
        }
        return rows, errors.New(err.Error())
    }

    // 获取 Sheet1 上所有单元格
    rows, err = ff.GetRows(ff.GetSheetName(0))
    if err != nil {
        return rows, errors.New(err.Error())
    }

    return rows, nil
}

// 导出excel到浏览器
func (m Excel) ExportToWeb() ([]byte, error) {
    f, err := m.exportExcel()
    if err != nil {
        return nil, err
    }
    buffer, err := f.WriteToBuffer()
    if err != nil {
        return nil, err
    }
    return buffer.Bytes(), nil
}

// 导出excel到本地服务器
// path 文件导出路径,建议使用绝对路径
// filename 文件名,需带上后缀
func (m Excel) ExportToPath(path, filename string) (string, error) {
    var filePath string
    f, err := m.exportExcel()
    if err != nil {
        return filePath, err
    }
    filePath = path + "/" + filename
    if err := f.SaveAs(filePath); err != nil {
        return filePath, err
    }
    return filePath, nil
}

// exportExcel 导出Excel文件
// sheetName 工作表名称
// headers 列名切片, 表头
// rows 数据切片,是一个二维数组
func (m Excel) exportExcel() (*excelize.File, error) {
    m.SetDefaultSheetName()

    f := excelize.NewFile()
    sheetIndex := f.NewSheet(m.SheetName)
    maxColumnRowNameLen := 1 + len(strconv.Itoa(len(m.Rows)))
    columnCount := len(m.Headers)
    if columnCount > maxCharCount {
        maxColumnRowNameLen++
    } else if columnCount > maxCharCount*maxCharCount {
        maxColumnRowNameLen += 2
    }
    columnNames := make([][]byte, 0, columnCount)
    for i, header := range m.Headers {
        columnName := m.getColumnName(i, maxColumnRowNameLen)
        columnNames = append(columnNames, columnName)
        // 初始化excel表头,这里的index从1开始要注意
        curColumnName := m.getColumnRowName(columnName, 1)
        err := f.SetCellValue(m.SheetName, curColumnName, header)
        if err != nil {
            return nil, err
        }
    }
    for rowIndex, row := range m.Rows {
        for columnIndex, columnName := range columnNames {
            // 从第二行开始
            err := f.SetCellValue(m.SheetName, m.getColumnRowName(columnName, rowIndex+2), row[columnIndex])
            if err != nil {
                return nil, err
            }
        }
    }
    f.SetActiveSheet(sheetIndex)
    return f, nil
}

// getColumnName 生成列名
// Excel的列名规则是从A-Z往后排;超过Z以后用两个字母表示,比如AA,AB,AC;两个字母不够以后用三个字母表示,比如AAA,AAB,AAC
// 这里做数字到列名的映射:0 -> A, 1 -> B, 2 -> C
// maxColumnRowNameLen 表示名称框的最大长度,假设数据是10行,1000列,则最后一个名称框是J1000(如果有表头,则是J1001),是4位
// 这里根据 maxColumnRowNameLen 生成切片,后面生成名称框的时候可以复用这个切片,而无需扩容
func (m Excel) getColumnName(column, maxColumnRowNameLen int) []byte {
    const A = 'A'
    if column < maxCharCount {
        // 第一次就分配好切片的容量
        slice := make([]byte, 0, maxColumnRowNameLen)
        return append(slice, byte(A+column))
    } else {
        // 递归生成类似AA,AB,AAA,AAB这种形式的列名
        return append(m.getColumnName(column/maxCharCount-1, maxColumnRowNameLen), byte(A+column%maxCharCount))
    }
}

// getColumnRowName 生成名称框
// Excel的名称框是用A1,A2,B1,B2来表示的,这里需要传入前一步生成的列名切片,然后直接加上行索引来生成名称框,就无需每次分配内存
func (Excel) getColumnRowName(columnName []byte, rowIndex int) (columnRowName string) {
    l := len(columnName)
    columnName = strconv.AppendInt(columnName, int64(rowIndex), 10)
    columnRowName = string(columnName)
    // 将列名恢复回去
    columnName = columnName[:l]
    return
}

WEB端调用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package controller

import (
    "bytes"
    "github.com/labstack/echo/v4"
    "xxx/lib/tool"
    "net/http"
)

type xxxController struct{}

var XxxController = new(xxxController)

func init() {
    Router.GET("/export", AuthCorpController.Export).Name = "导出excel"
}

func (xxxController) Export(c echo.Context) error {
    var (
        excelHeaders = []string{"姓名", "性别", "年龄"}
        excelRows    = [][]interface{}{
            {"张三", "女", 18},
            {"李四", "男", 19},
        }
    )
    b, _ := tool.Excel{Headers: excelHeaders, Rows: excelRows}.ExportToWeb()
    c.Response().Header().Set(echo.HeaderContentDisposition, "attachment; filename=students.xlsx")
    return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(b))
}

csv导入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package tool

import (
    "bufio"
    "bytes"
    "encoding/csv"
    "errors"
    "golang.org/x/text/encoding/simplifiedchinese"
    "golang.org/x/text/transform"
    "io"
    "mime/multipart"
    "strconv"
    "strings"
)

type Csv struct{}

// 从文件流读取 csv 的所有数据
func (m Csv) ReadCsvFromStream(f *multipart.FileHeader) ([][]string, error) {
    var rows [][]string

    src, err := f.Open()
    if err != nil {
        return rows, err
    }
    defer src.Close()

    // 兆字节转为字节进行比较大小
    maxFileSize := int64(2) // 默认最大上传 2 M
    if f.Size > maxFileSize*1e6 {
        return rows, errors.New("文件大小不能超过 " + strconv.FormatInt(maxFileSize, 10) + " M")
    }

    // 初始化一个 csv reader,并通过这个 reader 从 csv 文件读取数据
    reader := csv.NewReader(transform.NewReader(bufio.NewReader(src), simplifiedchinese.GBK.NewDecoder()))

    // 设置返回记录中每行数据期望的字段数,-1 表示返回所有字段
    reader.FieldsPerRecord = -1
    // 通过 readAll 方法返回 csv 文件中的所有内容
    rows, err = reader.ReadAll()
    if err != nil {
        return rows, err
    }

    return rows, nil
}

// 从文件流读取不规范 csv 的所有数据
func (m Csv) ReadIllegalCsvFromSteam(f *multipart.FileHeader) ([][]string, error) {
    var rows [][]string

    src, err := f.Open()
    if err != nil {
        return rows, err
    }
    defer src.Close()

    // 兆字节转为字节进行比较大小
    maxFileSize := int64(2) // 默认最大上传 2 M
    if f.Size > maxFileSize*1e6 {
        return rows, errors.New("文件大小不能超过 " + strconv.FormatInt(maxFileSize, 10) + " M")
    }

    // 直接复制文件内容
    var buf bytes.Buffer
    if _, err = io.Copy(&buf, transform.NewReader(bufio.NewReader(src), simplifiedchinese.GBK.NewDecoder())); err != nil {
        return rows, err
    }
    contents := buf.String()
    slice := strings.Split(contents, "\n")

    for _, v := range slice {
        records := strings.Split(strings.ReplaceAll(v, "=\"", "\""), ",") // 去掉不合法的等号
        for kk, vv := range records {
            records[kk] = strings.Trim(vv, "\"") // 去除前后的双引号
        }
        rows = append(rows, records)
    }
    return rows, nil
}