關(guān)鍵點(diǎn):
- Go語(yǔ)言讀取Excel
- Go語(yǔ)言正則表達(dá)式
- Go語(yǔ)言發(fā)送電子郵件
案例場(chǎng)景
今天公司行政部小妹妹跑來(lái)問(wèn)屡立,有什么辦法可以把工資條自動(dòng)發(fā)送到每個(gè)員工的企業(yè)郵箱里谎倔?公司每個(gè)員工的工資條以Excel的形式放在同一個(gè)文檔里坎拐,之前用OA發(fā)送,復(fù)制粘貼,操作相當(dāng)簡(jiǎn)單脓诡,但是公司要求改用電子郵件發(fā)送工資條后摇肌,給行政部的同事增加了較大的工作量擂红,而且每個(gè)月都需要做一次,這很浪費(fèi)時(shí)間围小,于是爽快的答應(yīng)幫忙解決昵骤。
情況梳理
公司工資條大概這個(gè)樣子的
為了方便,行政部門會(huì)把所有人的工資條按順序排列在同一個(gè)Excel文件里的同一個(gè)sheet里肯适。全貌會(huì)是以下這個(gè)樣子
行政部的妹妹希望能夠自動(dòng)的把每個(gè)人自己的工資條發(fā)送到各自的郵箱里涉茧,所以至少得有個(gè)地方填寫郵箱號(hào)吧,于是我在每個(gè)人的工資條上增加了一行疹娶,標(biāo)記每個(gè)人的郵箱伴栓,于是文檔成了這樣
好了,Excel格式確定下來(lái)雨饺,對(duì)于程序員來(lái)說(shuō)钳垮,他就只是個(gè)二維數(shù)組了。接下來(lái)额港,就開(kāi)始寫代碼吧饺窿。
用Go讀取Excel
Go語(yǔ)言自己有個(gè)CSV庫(kù),不過(guò)在這個(gè)場(chǎng)景里移斩,還是用github.com/tealeg/xlsx庫(kù)來(lái)處理xlsx文件更合適肚医。
創(chuàng)建go文件,將工資表與go文件放在同一個(gè)目錄向瓷,本文假設(shè)講工資表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打開(kāi)Excel文件成功肠套,會(huì)返回一個(gè)xlsx.File對(duì)象,這個(gè)對(duì)象里除有一些基礎(chǔ)的文件操作方法猖任,還包含一個(gè)Sheets的對(duì)象你稚,這個(gè)對(duì)象是Excel文件中Sheet的map集合,可以通過(guò)遍歷獲得所有Sheet朱躺。
Sheet中包含一個(gè)名叫Rows的對(duì)象刁赖,這個(gè)對(duì)象是Sheet中所有行的集合。
Rows中包含一個(gè)名叫Cells的對(duì)象长搀,這個(gè)對(duì)象是行中所有格子的集合宇弛。
所以,一個(gè)xlsx.File對(duì)象不考慮其包含的方法的話就相當(dāng)于一個(gè)三維數(shù)組源请。
我們只需要做三次嵌套的循環(huán)就可以獲得其中的所有單元格數(shù)據(jù)枪芒,像這樣
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)
}
}
}
}
接下來(lái)轿钠,要進(jìn)入關(guān)鍵點(diǎn),把數(shù)據(jù)讀取出來(lái)后病苗,要分隔沒(méi)個(gè)人的工資條疗垛,從之前的圖片上可以看出,當(dāng)表格中出現(xiàn)一次電子郵件內(nèi)容的單元格的時(shí)候硫朦,就是新的一個(gè)人的工資條了贷腕。所以,需要通過(guò)正則表達(dá)式判斷有沒(méi)有讀取到電子郵件的單元格咬展,如果讀取到泽裳,就要用新的存儲(chǔ)空間保存工資條的內(nèi)容。
用正則表達(dá)式找到含有Email地址的單元格
正則表達(dá)式判斷很簡(jiǎn)單破婆,創(chuàng)建一個(gè)函數(shù)涮总,讀取整行的數(shù)據(jù),如果其中出現(xiàn)了電子郵件祷舀,就返回真瀑梗,以及電子郵件字符串(這個(gè)地方可以不用穿反isEmail這個(gè)參數(shù),只需要判斷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函數(shù)將行的Cells直接讀取成字符串?dāng)?shù)組抛丽,并且過(guò)濾了空格和換行。
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
}
我用了一個(gè)map來(lái)統(tǒng)一存放不同的人的工資條數(shù)據(jù)饰豺,并且用電子郵件作為鍵值亿鲜,然后將數(shù)據(jù)組裝成一個(gè)HTML的表格行代碼(因?yàn)樾枰l(fā)送HTML格式的電子郵件才能以表格的形式展現(xiàn))。于是冤吨,main里的循環(huán)代碼就變成了這樣
for _, sheet := range xlFile.Sheets {
curMail := ""
for _, row := range sheet.Rows {
cells := getCellValues(row)
//如果行包含電子郵件蒿柳,創(chuàng)建一個(gè)新字典項(xiàng)
if isEmail, emailStr := isEmailRow(cells); isEmail {
curMail = emailStr
}
sendList[curMail] += fmt.Sprintf("<tr><td>%s</td></tr>", strings.Join(cells, "</td><td>"))
}
}
用Go語(yǔ)言發(fā)送電子郵件(SMTP)
Go語(yǔ)言發(fā)送電子郵件很簡(jiǎn)單,用標(biāo)準(zhǔn)包 net/smtp就足夠了漩蟆。
先封裝一個(gè)發(fā)送郵件的函數(shù),用官方的例子改造一下垒探。
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
}
再創(chuàng)建一個(gè)函數(shù),遍歷所有內(nèi)容并調(diào)用發(fā)送郵件函數(shù)發(fā)送出去
func sendMail(sendList map[string]string) {
fmt.Printf("共需要發(fā)送%d封郵件\n", len(sendList))
index := 1
for mail, content := range sendList {
fmt.Printf("發(fā)送第%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(" ... 發(fā)送錯(cuò)誤(X) %s %s \n", mail, err.Error())
} else {
fmt.Printf(" ... 發(fā)送成功(V) %s \n", mail)
}
index++
fmt.Printf("<table border='2'>%s</table> \n", content)
}
}
最后爆安,將sendMail放在main函數(shù)中叛复,for迭代讀取出所有數(shù)據(jù)之后,就完成了扔仓。
行政的同事使用的是Windows,使用終端程序往往會(huì)讓他們摸不著頭腦咖耘,完全不知道發(fā)生什么事情翘簇,然而我也不可能花太多時(shí)間為這樣的小程序開(kāi)發(fā)界面,所以即便在終端運(yùn)行儿倒,也盡量提供友善的用戶體驗(yàn)版保,代碼中關(guān)鍵的信息都盡量輸出友好提示呜笑。程序結(jié)束后,做一個(gè)終端輸入等待彻犁,讓用戶看到運(yùn)行的結(jié)果叫胁。
fmt.Print("按下回車結(jié)束")
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)
//如果行包含電子郵件,創(chuàng)建一個(gè)新字典項(xiàng)
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("按下回車結(jié)束")
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("共需要發(fā)送%d封郵件\n", len(sendList))
index := 1
for mail, content := range sendList {
fmt.Printf("發(fā)送第%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(" ... 發(fā)送錯(cuò)誤(X) %s %s \n", mail, err.Error())
} else {
fmt.Printf(" ... 發(fā)送成功(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語(yǔ)言交叉編譯汞幢,運(yùn)行在不同的操作系統(tǒng)
我用的Mac 64位驼鹅,需要編譯一個(gè)Windows 32位的可執(zhí)行程序,一句搞定
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build
GOOS設(shè)置目標(biāo)系統(tǒng),可以是 windows, linux,darwin
GOARCH設(shè)置目標(biāo)系統(tǒng)是32位還是64位森篷,分別對(duì)應(yīng) 386和amd64
CGO_ENABLED設(shè)置是否需要使用CGO输钩,本例子不需要,設(shè)置為0仲智,如果需要使用CGO編譯买乃,設(shè)置為1
OK,任務(wù)完成钓辆,只要編輯一份如文中第三張圖那樣格式的文檔剪验,保存為list.xlsx,與編譯好的可執(zhí)行文件放在同一目錄前联,雙擊執(zhí)行碉咆,文檔中的內(nèi)容就會(huì)根據(jù)電子郵件單元格作為分割點(diǎn)分別發(fā)送到該電子郵箱里。
知識(shí)點(diǎn)總結(jié)
- 使用github.com/tealeg/xlsx包讀取xlsx文件
- 使用regexp包實(shí)現(xiàn)正則表達(dá)式判斷
- 使用net/smtp包發(fā)送電子郵件
- 使用交叉編譯命令生成不同系統(tǒng)上的可執(zhí)行文件
歡迎大家簡(jiǎn)書或我的個(gè)人博客與我交流