這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
關鍵點:
- Go語言讀取Excel
- Go語言Regex
- Go語言寄送電子郵件
案例情境
今天公司行政部小妹妹跑來問,有什麼辦法可以把工資條自動發送到每個員工的企業郵箱裡?公司每個員工的工資條以Excel的形式放在同一個文檔裡,之前用OA發送,複製粘貼,操作相當簡單,但是公司要求改用電子郵件發送工資條後,給行政部的同事增加了較大的工作量,而且每個月都需要做一次,這很浪費時間,於是爽快的答應幫忙解決。
情況梳理
公司工資條大概這個樣子的
為了方便,行政部門會把所有人的工資條按順序排列在同一個Excel檔案裡的同一個sheet裡。全貌會是以下這個樣子
Paste_Image.png
行政部的妹妹希望能夠自動的把每個人自己的工資條發送到各自的郵箱裡,所以至少得有個地方填寫郵箱號吧,於是我在每個人的工資條上增加了一行,標記每個人的郵箱,於是文檔成了這樣
Paste_Image.png
好了,Excel格式確定下來,對於程式員來說,他就只是個二維數組了。接下來,就開始寫代碼吧。
用Go讀取Excel
Go語言自己有個CSV庫,不過在這個情境裡,還是用github.com/tealeg/xlsx庫來處理xlsx檔案更合適。
建立go檔案,將工資表與go檔案放在同一個目錄,本文假設講工資表Excel 命名為 list.xlsx
package mainimport ( "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) } } }}
接下來,要進入關鍵點,把資料讀取出來後,要分隔沒個人的工資條,從之前的圖片上可以看出,當表格中出現一次電子郵件內容的儲存格的時候,就是新的一個人的工資條了。所以,需要通過Regex判斷有沒有讀取到電子郵件的儲存格,如果讀取到,就要用新的儲存空間儲存工資條的內容。
用Regex找到含有Email地址的儲存格
Regex判斷很簡單,建立一個函數,讀取整行的資料,如果其中出現了電子郵件,就返回真,以及電子郵件字串(這個地方可以不用穿反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 mainimport ( "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包實現Regex判斷
- 使用net/smtp包寄送電子郵件
- 使用交叉編譯命令產生不同系統上的可執行檔