本文介绍了如何使用Go语言来操作excel,来简化生活中的一些比较繁琐的机械操作。
从基础功能实现,到逐步修改,一共提供了四个版本的变化。其中基础功能和第四版提供了完整的代码。
在总结中给出了我最后使用的完整代码,各位如果有需要的话,可以作为参考并修改使用。
- 起因
- 思路
- 第一版:基础功能
- 第二版:正则匹配
- 第三版:多次作业
- 第四版:最终版
- 总结
注意,第四版的最终版和最后我使用的版本还是有些许变化的。我最后使用的版本按照老师的需要,增加了多个班级登记和已处理的作业文件的归档处理。不需要这些功能的读者可以直接看第四版代码即可。
起因
昨天晚上回到实验室,发现同学在给老师记录本科生提交作业的情况。
大概就是有一个文件列表,文件名是学生的学号、姓名、第几次作业组成。
另外有一份学生名单的excel,需要将提交了作业的学生信息记录在excel里。
大概是下面这个图的样子:
这里用九个学生举例,实际上我同学要处理600多个同学的提交信息。
600多个同学,一个个去搜索,然后去打勾,想一想就头皮发麻。
程序员有程序员的手段嘛,说干就干。
思路
我们需要读取文件列表,然后在excel中将对应学号的学生的提交情况打勾记录。
步骤简单的分为两步:
- 读取文件列表,从中取出学号信息。
- 读取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)
}
}
复制代码
运行一下,我们看一下表格内容:
完美!直接将所有同学全部勾上了!
第二版:正则匹配
然而,理想很丰满,现实很骨感,并不是所有同学都是按照统一格式来提交作业的,实际上提交的作业形式可能是这样的。
可以看到,这位嘉夫1同学很有自己的想法,将自己的学号提取到了前面,这样我们就没法通过固定下表来提取到他的学号了。
所以,我们得采用一个新的办法从文件名中提取学号。
经过观察我们发现,学号都是固定长度的,比如都是11位的数字。
我们可以通过正则表达式来提取学号。
// 用正则表达式匹配11位学号
reg := regexp.MustCompile(`[0-9]{11}`)
for _, file := range files {
number, _ := strconv.Atoi(reg.FindString(file.Name()))
// ... 省略不变的部分
}
复制代码
这样,不管同学将学号放在哪里,我都可以正确的提取出学号了。
第三版:多次作业
细心的同学已经发现,在登分册里不光有第一次作业,还有第二次作业。
作为一个上面的代码只能登记第一次作业,要想登记第二次作业还得重新修改代码。
作为一个程序员,这种机械的工作当然是交给代码来完成。
假设现在学生提交的文件变多了,里面包含的第二次作业:
我们定义一个作业批次的数组或者切片,例如:["第一次作业", "第二次作业"]
这样,我们根据文件名包含哪个字符串就能知道是第几次作业了。
如果是第一次作业就往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("√")
}
}
}
复制代码
完整的步骤解释:
- 对每个文件名判断是否包含第一次作业或者第二次作业的字段。
- 如果包含,则
homeworkIndex
用于记录插入的列,偏移量为5
。
运行结束如下图所示:
完美!不管学生把学号放在哪,我们只需要运行依次程序即可将两次的作业情况全部登记上了。
第四版:最终版
如果,你以为到第三版就结束了,那你可就大错特错了。
现在的学生讲究的是德智体美劳全面发展,发扬个性!
在我用上面的程序处理那600个学生的文件之后,发现还剩200多个学生没有登记上!
我一看,原来有的学生将第一次作业命名成第1次作业。
好家伙,我直接好家伙!这都不按老师的要求来,这是我没有想到的。
继续修改代码,我们将作业的切片变为二维的,这样就不怕同学在一个批次的作业变着花命名了。
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)
}
}
}
复制代码