关键点:
- Go语言读取Excel
- Go语言正则表达式
- Go语言发送电子邮件
案例场景
今天公司行政部小妹妹跑来问,有什么办法可以把工资条自动发送到每个员工的企业邮箱里?公司每个员工的工资条以Excel的形式放在同一个文档里,之前用OA发送,复制粘贴,操作相当简单,但是公司要求改用电子邮件发送工资条后,给行政部的同事增加了较大的工作量,而且每个月都需要做一次,这很浪费时间,于是爽快的答应帮忙解决。
情况梳理
公司工资条大概这个样子的
为了方便,行政部门会把所有人的工资条按顺序排列在同一个Excel文件里的同一个sheet里。全貌会是以下这个样子
行政部的妹妹希望能够自动的把每个人自己的工资条发送到各自的邮箱里,所以至少得有个地方填写邮箱号吧,于是我在每个人的工资条上增加了一行,标记每个人的邮箱,于是文档成了这样
好了,Excel格式确定下来,对于程序员来说,他就只是个二维数组了。接下来,就开始写代码吧。
用Go读取Excel
Go语言自己有个CSV库,不过在这个场景里,还是用github.com/tealeg/xlsx库来处理xlsx文件更合适。
创建go文件,将工资表与go文件放在同一个目录,本文假设讲工资表Excel 命名为 list.xlsx
package main
import (
"bufio"
"fmt"
"github.com/tealeg/xlsx"
"log"
)
func main() {
excelFileName := "./list.xlsx"
xlFile, err := xlsx.OpenFile(excelFileName)
if err != nil {
log.Fatalln("err:", err.Error())
}
}
xlsx打开Excel文件成功,会返回一个xlsx.File对象,这个对象里除有一些基础的文件操作方法,还包含一个Sheets的对象,这个对象是Excel文件中Sheet的map集合,可以通过遍历获得所有Sheet。
Sheet中包含一个名叫Rows的对象,这个对象是Sheet中所有行的集合。
Rows中包含一个名叫Cells的对象,这个对象是行中所有格子的集合。
所以,一个xlsx.File对象不考虑其包含的方法的话就相当于一个三维数组。
我们只需要做三次嵌套的循环就可以获得其中的所有单元格数据,像这样
func main() {
excelFileName := "./list.xlsx"
xlFile, err := xlsx.OpenFile(excelFileName)
if err != nil {
log.Fatalln("err:", err.Error())
}
for _, sheet := range xlFile.Sheets {
for _, row := range sheet.Rows {
for _, cell := range row.Cells {
fmt.Printf("%s\n", cell.Value)
}
}
}
}
接下来,要进入关键点,把数据读取出来后,要分隔没个人的工资条,从之前的图片上可以看出,当表格中出现一次电子邮件内容的单元格的时候,就是新的一个人的工资条了。所以,需要通过正则表达式判断有没有读取到电子邮件的单元格,如果读取到,就要用新的存储空间保存工资条的内容。
用正则表达式找到含有Email地址的单元格
正则表达式判断很简单,创建一个函数,读取整行的数据,如果其中出现了电子邮件,就返回真,以及电子邮件字符串(这个地方可以不用穿反isEmail这个参数,只需要判断email是不是零值就可以了)
func isEmailRow(r []string) (isEmail bool, email string) {
reg := regexp.MustCompile(`^[a-zA-Z_0-9.-]{1,64}@([a-zA-Z0-9-]{1,200}.){1,5}[a-zA-Z]{1,6}$`)
for _, v := range r {
if reg.MatchString(v) {
return true, v
}
}
return false, ""
}
为了后面操作方便,我用getCellValues函数将行的Cells直接读取成字符串数组,并且过滤了空格和换行。
func getCellValues(r *xlsx.Row) (cells []string) {
for _, cell := range r.Cells {
txt := strings.Replace(strings.Replace(cell.Value, "\n", "", -1), " ", "", -1)
cells = append(cells, txt)
}
return
}
我用了一个map来统一存放不同的人的工资条数据,并且用电子邮件作为键值,然后将数据组装成一个HTML的表格行代码(因为需要发送HTML格式的电子邮件才能以表格的形式展现)。于是,main里的循环代码就变成了这样
for _, sheet := range xlFile.Sheets {
curMail := ""
for _, row := range sheet.Rows {
cells := getCellValues(row)
//如果行包含电子邮件,创建一个新字典项
if isEmail, emailStr := isEmailRow(cells); isEmail {
curMail = emailStr
}
sendList[curMail] += fmt.Sprintf("<tr><td>%s</td></tr>", strings.Join(cells, "</td><td>"))
}
}
用Go语言发送电子邮件(SMTP)
Go语言发送电子邮件很简单,用标准包 net/smtp就足够了。
先封装一个发送邮件的函数,用官方的例子改造一下。
func sendToMail(user, password, host, to, subject, body, mailtype string) error {
auth := smtp.PlainAuth("", user, password, strings.Split(host, ":")[0])
msg := []byte("To: " + to + "\r\nFrom: " + user + "\r\nSubject: " + subject + "\r\n" + "Content-Type: text/" + mailtype + "; charset=UTF-8" + "\r\n\r\n" + body)
sendto := strings.Split(to, ";")
err := smtp.SendMail(host, auth, user, sendto, msg)
return err
}
再创建一个函数,遍历所有内容并调用发送邮件函数发送出去
func sendMail(sendList map[string]string) {
fmt.Printf("共需要发送%d封邮件\n", len(sendList))
index := 1
for mail, content := range sendList {
fmt.Printf("发送第%d封", index)
if err := sendToMail("xxx@mybigcompany.com",
"thisismypassword",
"smtp.mybigcompany.com:25",
mail,
"工资条",
fmt.Sprintf("<table border='2'>%s</table>", content),
"html"); err != nil {
fmt.Printf(" ... 发送错误(X) %s %s \n", mail, err.Error())
} else {
fmt.Printf(" ... 发送成功(V) %s \n", mail)
}
index++
fmt.Printf("<table border='2'>%s</table> \n", content)
}
}
最后,将sendMail放在main函数中,for迭代读取出所有数据之后,就完成了。
行政的同事使用的是Windows,使用终端程序往往会让他们摸不着头脑,完全不知道发生什么事情,然而我也不可能花太多时间为这样的小程序开发界面,所以即便在终端运行,也尽量提供友善的用户体验,代码中关键的信息都尽量输出友好提示。程序结束后,做一个终端输入等待,让用户看到运行的结果。
fmt.Print("按下回车结束")
bufio.NewReader(os.Stdin).ReadLine()
完整代码
package main
import (
"bufio"
"fmt"
"net/smtp"
"os"
"regexp"
"strings"
"log"
"github.com/tealeg/xlsx"
)
func main() {
excelFileName := "./list.xlsx"
xlFile, err := xlsx.OpenFile(excelFileName)
if err != nil {
log.Fatalln("err:", err.Error())
}
sendList := make(map[string]string)
for _, sheet := range xlFile.Sheets {
curMail := ""
for _, row := range sheet.Rows {
cells := getCellValues(row)
//如果行包含电子邮件,创建一个新字典项
if isEmail, emailStr := isEmailRow(cells); isEmail {
curMail = emailStr
} else {
count := 0
for _, c := range cells {
if len(c) > 0 {
count++
}
}
if count > 1 {
sendList[curMail] += fmt.Sprintf("<tr><td>%s</td></tr>", strings.Join(cells, "</td><td>"))
} else {
sendList[curMail] += fmt.Sprintf("<tr><td colspan='%d'>%s</td></tr>", len(cells), strings.Join(cells, ""))
}
}
}
}
sendMail(sendList)
fmt.Print("按下回车结束")
bufio.NewReader(os.Stdin).ReadLine()
}
func getCellValues(r *xlsx.Row) (cells []string) {
for _, cell := range r.Cells {
txt := strings.Replace(strings.Replace(cell.Value, "\n", "", -1), " ", "", -1)
cells = append(cells, txt)
}
return
}
func isEmailRow(r []string) (isEmail bool, email string) {
reg := regexp.MustCompile(`^[a-zA-Z_0-9.-]{1,64}@([a-zA-Z0-9-]{1,200}.){1,5}[a-zA-Z]{1,6}$`)
for _, v := range r {
if reg.MatchString(v) {
return true, v
}
}
return false, ""
}
func sendMail(sendList map[string]string) {
fmt.Printf("共需要发送%d封邮件\n", len(sendList))
index := 1
for mail, content := range sendList {
fmt.Printf("发送第%d封", index)
if err := sendToMail("xxx@mybigcompany.com",
"thesismypassword",
"smtp.mybigcompany.com:25",
mail,
"工资条",
fmt.Sprintf("<table border='2'>%s</table>", content),
"html"); err != nil {
fmt.Printf(" ... 发送错误(X) %s %s \n", mail, err.Error())
} else {
fmt.Printf(" ... 发送成功(V) %s \n", mail)
}
index++
//fmt.Printf("<table border='2'>%s</table> \n", content)
}
}
func sendToMail(user, password, host, to, subject, body, mailtype string) error {
auth := smtp.PlainAuth("", user, password, strings.Split(host, ":")[0])
msg := []byte("To: " + to + "\r\nFrom: " + user + "\r\nSubject: " + subject + "\r\n" + "Content-Type: text/" + mailtype + "; charset=UTF-8" + "\r\n\r\n" + body)
sendto := strings.Split(to, ";")
err := smtp.SendMail(host, auth, user, sendto, msg)
return err
}
Go语言交叉编译,运行在不同的操作系统
我用的Mac 64位,需要编译一个Windows 32位的可执行程序,一句搞定
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build
GOOS设置目标系统,可以是 windows, linux,darwin
GOARCH设置目标系统是32位还是64位,分别对应 386和amd64
CGO_ENABLED设置是否需要使用CGO,本例子不需要,设置为0,如果需要使用CGO编译,设置为1
OK,任务完成,只要编辑一份如文中第三张图那样格式的文档,保存为list.xlsx,与编译好的可执行文件放在同一目录,双击执行,文档中的内容就会根据电子邮件单元格作为分割点分别发送到该电子邮箱里。
知识点总结
- 使用github.com/tealeg/xlsx包读取xlsx文件
- 使用regexp包实现正则表达式判断
- 使用net/smtp包发送电子邮件
- 使用交叉编译命令生成不同系统上的可执行文件