Go语言基础:自动化办公之excel

本文介绍了如何使用Go语言来操作excel,来简化生活中的一些比较繁琐的机械操作。

从基础功能实现,到逐步修改,一共提供了四个版本的变化。其中基础功能和第四版提供了完整的代码。

在总结中给出了我最后使用的完整代码,各位如果有需要的话,可以作为参考并修改使用。

  • 起因
  • 思路
  • 第一版:基础功能
  • 第二版:正则匹配
  • 第三版:多次作业
  • 第四版:最终版
  • 总结

注意,第四版的最终版和最后我使用的版本还是有些许变化的。我最后使用的版本按照老师的需要,增加了多个班级登记和已处理的作业文件的归档处理。不需要这些功能的读者可以直接看第四版代码即可。

起因

昨天晚上回到实验室,发现同学在给老师记录本科生提交作业的情况。

大概就是有一个文件列表,文件名是学生的学号、姓名、第几次作业组成。

另外有一份学生名单的excel,需要将提交了作业的学生信息记录在excel里。

大概是下面这个图的样子:

1.png

2.png

这里用九个学生举例,实际上我同学要处理600多个同学的提交信息。

600多个同学,一个个去搜索,然后去打勾,想一想就头皮发麻。

程序员有程序员的手段嘛,说干就干。

思路

我们需要读取文件列表,然后在excel中将对应学号的学生的提交情况打勾记录。

步骤简单的分为两步:

  1. 读取文件列表,从中取出学号信息。
  2. 读取excel文件,在学号对应的行的单元格中进行记录。

有个优化的细节:

每次我们取得学号之后,需要搜索整个表,才能知道该学号在哪一行。

甚至当学生不在这个班级的时候,搜索完整个表之后没用任何收益,反而白白浪费了时间和计算资源。

所以,我们读取表格的时候将学号信息与行号信息进行缓存,key位学号,value位行号。

第一版:基础功能

读取excel文件,并缓存行号

操作excel需要用到"github.com/tealeg/xlsx"库,记得自行import,完整代码会在文章最后面给出。

为了方便读者理解实现思路,略去了所有的错误处理,有需要可以查看后面的完整代码。

tablePath := "E:\\Desktop\\新建文件夹\\登分表.xlsx"

// 获取表格sheet
table, err := xlsx.OpenFile(tablePath)
sheet := table.Sheets[0]

// 缓存表格内容,key为学号,value为行号
m := map[int]int{}
for index, row := range sheet.Rows{
        number, err := strconv.Atoi(row.Cells[0].String())
        if err != nil {
                    continue
	}
        m[number] = index
}
复制代码

读取文件列表,并提取学号

由于文件名是固定格式的,我们可以直接取到学号

homeworkPath := "E:\\Desktop\\新建文件夹\\作业"
files, err := os.ReadDir(homeworkPath)
for _, file := range files {
        number := file.Name()[15:27]
        fmt.Println(number)
}
复制代码

输出学号如下:

 12345678901
 12345678902
 12345678903
 12345678904
 12345678905
 12345678906
 12345678907
 12345678908
 12345678909
复制代码

填excel表

我们将上面代码中的fmt.Pirntln()函数改为填表的内容:

for _, file := range files {
	number, _ := strconv.Atoi(file.Name()[16:27])
	index, ok := m[number]
	if ok == false {
		continue
	}
	// 打勾
	sheet.Rows[index].Cells[5].SetString("√")
}
复制代码

保存excel

err = table.Save(tablePath)
复制代码

完整代码

package main

import (
	"github.com/tealeg/xlsx"
	"os"
	"strconv"
)

func main() {

	tablePath := "E:\\Desktop\\新建文件夹\\登分表.xlsx"

	// 获取表格sheet
	table, err := xlsx.OpenFile(tablePath)
	if err != nil {
		panic(err)
	}
	sheet := table.Sheets[0]

	// 缓存表格内容,key为学号,value为行号
	m := map[int]int{}
	for index, row := range sheet.Rows{
		number, err := strconv.Atoi(row.Cells[0].String())
		if err != nil {
			continue
		}
		m[number] = index
	}

	homeworkPath := "E:\\Desktop\\新建文件夹\\作业"
	files, err := os.ReadDir(homeworkPath)
	if err != nil {
		panic(err)
	}
	for _, file := range files {
		number, _ := strconv.Atoi(file.Name()[16:27])
		index, ok := m[number]
		if ok == false {
			continue
		}
		// 打勾
		sheet.Rows[index].Cells[5].SetString("√")
	}

	// 保存表格文件
	err = table.Save(tablePath)
	if err != nil {
		panic(err)
	}

}
复制代码

运行一下,我们看一下表格内容:

3.png

完美!直接将所有同学全部勾上了!

第二版:正则匹配

然而,理想很丰满,现实很骨感,并不是所有同学都是按照统一格式来提交作业的,实际上提交的作业形式可能是这样的。

4.png

可以看到,这位嘉夫1同学很有自己的想法,将自己的学号提取到了前面,这样我们就没法通过固定下表来提取到他的学号了。

所以,我们得采用一个新的办法从文件名中提取学号。

经过观察我们发现,学号都是固定长度的,比如都是11位的数字。

我们可以通过正则表达式来提取学号。

// 用正则表达式匹配11位学号
reg := regexp.MustCompile(`[0-9]{11}`)
for _, file := range files {
    number, _ := strconv.Atoi(reg.FindString(file.Name()))
    // ... 省略不变的部分
    
}
复制代码

这样,不管同学将学号放在哪里,我都可以正确的提取出学号了。

第三版:多次作业

细心的同学已经发现,在登分册里不光有第一次作业,还有第二次作业。

作为一个上面的代码只能登记第一次作业,要想登记第二次作业还得重新修改代码。

作为一个程序员,这种机械的工作当然是交给代码来完成。

假设现在学生提交的文件变多了,里面包含的第二次作业:

5.png

我们定义一个作业批次的数组或者切片,例如:["第一次作业", "第二次作业"]

这样,我们根据文件名包含哪个字符串就能知道是第几次作业了。

如果是第一次作业就往excel的第6列(索引下表为5)的单元格插入。

如果是第二次作业就往excel的第7列(索引下表为6)的单元格插入。

homeworks := []string{"第一次作业", "第二次作业"}

// 用正则表达式匹配11位学号
reg := regexp.MustCompile(`[0-9]{11}`)
for _, file := range files {
    fileName := file.Name()
    for homeworkIndex, homework := range homeworks {
        if strings.Contains(fileName, homework) {
            number, _ := strconv.Atoi(reg.FindString(file.Name()))
            index, ok := m[number]
            if ok == false {
                continue
            }
            // 打勾
            sheet.Rows[index].Cells[homeworkIndex + 5].SetString("√")
        }
    }
}
复制代码

完整的步骤解释:

  1. 对每个文件名判断是否包含第一次作业或者第二次作业的字段。
  2. 如果包含,则homeworkIndex用于记录插入的列,偏移量为5

运行结束如下图所示:

6.png

完美!不管学生把学号放在哪,我们只需要运行依次程序即可将两次的作业情况全部登记上了。

第四版:最终版

如果,你以为到第三版就结束了,那你可就大错特错了。

现在的学生讲究的是德智体美劳全面发展,发扬个性!

在我用上面的程序处理那600个学生的文件之后,发现还剩200多个学生没有登记上!

我一看,原来有的学生将第一次作业命名成第1次作业

7.png

好家伙,我直接好家伙!这都不按老师的要求来,这是我没有想到的。

继续修改代码,我们将作业的切片变为二维的,这样就不怕同学在一个批次的作业变着花命名了。

homeworkSlice := [][]string{{"第一次作业", "第1次作业"},
		{"第二次作业", "第2次作业"}}

// 用正则表达式匹配11位学号
reg := regexp.MustCompile(`[0-9]{11}`)
for _, file := range files {
    fileName := file.Name()
    // homeworkIndex表示第几次作业
    for homeworkIndex, homeworks := range homeworkSlice {
        // 同一批次作业中可能有不同的命名
        for _, homework := range homeworks {
            if strings.Contains(fileName, homework) {
                number, _ := strconv.Atoi(reg.FindString(file.Name()))
                index, ok := m[number]
                if ok == false {
                    continue
                }
                // 打勾
                sheet.Rows[index].Cells[homeworkIndex + 5].SetString("√")
            }
        }
    }
}
复制代码

再次运行以上代码,基本上处理了全部的作业文件了。

完整代码

package main

import (
	"github.com/tealeg/xlsx"
	"os"
	"regexp"
	"strconv"
	"strings"
)

func main() {

	tablePath := "E:\\Desktop\\新建文件夹\\登分表.xlsx"

	// 获取表格sheet
	table, err := xlsx.OpenFile(tablePath)
	if err != nil {
		panic(err)
	}
	sheet := table.Sheets[0]

	// 缓存表格内容,key为学号,value为行号
	m := map[int]int{}
	for index, row := range sheet.Rows{
		number, err := strconv.Atoi(row.Cells[0].String())
		if err != nil {
			continue
		}
		m[number] = index
	}

	homeworkPath := "E:\\Desktop\\新建文件夹\\作业"
	files, err := os.ReadDir(homeworkPath)
	if err != nil {
		panic(err)
	}


	homeworkSlice := [][]string{{"第一次作业", "第1次作业"},
		{"第二次作业", "第2次作业"}}

	// 用正则表达式匹配11位学号
	reg := regexp.MustCompile(`[0-9]{11}`)
	for _, file := range files {
		fileName := file.Name()
		for homeworkIndex, homeworks := range homeworkSlice {
			for _, homework := range homeworks {
				if strings.Contains(fileName, homework) {
					number, _ := strconv.Atoi(reg.FindString(file.Name()))
					index, ok := m[number]
					if ok == false {
						continue
					}
					// 打勾
					sheet.Rows[index].Cells[homeworkIndex + 5].SetString("√")
				}
			}
		}
	}

	// 保存表格文件
	err = table.Save(tablePath)
	if err != nil {
		panic(err)
	}

}
复制代码

总结

要写一个自动化工具,还是得考虑到各种各样的情况,不过因为Go语言本身用起来很方便,所以修改起来也很简单。

由于是临时使用一下,我并没有将各个参数提取到配置文件或者命令行参数当中,整个程序显得比较缺少灵活性,如果老师需要一个完善的工具的话,后面可能还会花时间再完善一下。

最后,奉上一份完整的代码,完整的功能如下:

  • 可以为多个班级记录,每个班级对应一张excel表
  • 可以登记多批次的作业
  • 登记完的作业会按班级和作业批次进行归档

如果有读者需要使用这个代码的话,请记得备份好自己的源文件,并且修改偏移量

package main

import (
	"fmt"
	"github.com/tealeg/xlsx"
	"os"
	"regexp"
	"strconv"
	"strings"
)

func main() {

	// 作业文件所在的文件夹
	homeworkPath := "E:\\Desktop\\作业"

	// 登分表格坐在的文件夹
	tablePath := "E:\\Desktop\\"

	// 登分表格名
	classSlice := []string{"2.xlsx", "1.xlsx"}

	// 作业的命名
	homeworkSlice := [][]string{{"第一次作业", "第1次作业", "第1次"},
		{"第二次作业", "第2次作业", "第2次"}}

	// 记录作业提交情况
	for _, className := range classSlice {
		for homeworkIndex, homeworks := range homeworkSlice {
			checkHomework(homeworkPath, tablePath + className, homeworks, homeworkIndex)
		}
	}

}

func checkHomework(homeworkPath string, tablePath string, homeworks []string, homeworkIndex int) {

	// 获取表格sheet
	table, err := xlsx.OpenFile(tablePath)
	if err != nil {
		panic(err)
	}
	sheet := table.Sheets[0]

	// 缓存表格内容,key为学号,value为行号
	m := map[int]int{}
	for index, row := range sheet.Rows{
		number, err := strconv.Atoi(row.Cells[0].String())
		if err != nil {
			continue
		}
		m[number] = index
	}

	// 读取作业文件列表
	files, err := os.ReadDir(homeworkPath)
	if err != nil {
		panic(err)
	}

	// 用于记录已经处理的文件个数
	count := 0

	// 用正则表达式匹配11位学号
	reg := regexp.MustCompile(`[0-9]{11}`)

	// 遍历作业文件列表
	for _, file := range files {
		fileName := file.Name()
		// 每一次作业可能有不同的命名,例如:第一次作业、第1次作业
		for _, homework := range homeworks {
			if strings.Contains(fileName, homework) {
				// 从文件名中取出学号
				number, _ := strconv.Atoi(reg.FindString(fileName))
				index, ok := m[number]
				if ok == false {
					continue
				}
				// 打勾
				sheet.Rows[index].Cells[homeworkIndex + 5].SetString("√")
				// 将处理完的文件移至对应班级作业批次的文件夹当中
				newPath := tablePath[:len(tablePath) - 5] + "班-第" + strconv.Itoa(homeworkIndex + 1) + "次作业"
				mkdir(newPath)
				err = os.Rename(homeworkPath + "\\" + fileName, newPath + "\\" + fileName)
				if err != nil {
					panic(err)
				}
				count++
				break
			}
		}

	}
	fmt.Println(count, tablePath, homeworks)

	// 保存表格文件
	err = table.Save(tablePath)
	if err != nil {
		panic(err)
	}
}

// 创建文件夹
func mkdir(path string) {
	_, err := os.Stat(path)
	if os.IsNotExist(err) {
		err = os.Mkdir(path, os.ModePerm)
		if err != nil {
			panic(err)
		}
	}
}
复制代码