├── getWinReg.exe ├── picture └── .DS_Store ├── driver ├── checkdriver_test.go ├── getWinReg.go └── checkdriver.go ├── .gitignore ├── utils └── request.go ├── main.go ├── page ├── element.json ├── location.go ├── launch.go └── message.go ├── config ├── data.json └── conf.go ├── verify └── code.go └── README.md /getWinReg.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eddylapis/goBoss/HEAD/getWinReg.exe -------------------------------------------------------------------------------- /picture/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eddylapis/goBoss/HEAD/picture/.DS_Store -------------------------------------------------------------------------------- /driver/checkdriver_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_DownloadDriver(t *testing.T) { 8 | SetDriver() 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | */.idea/ 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /driver/getWinReg.go: -------------------------------------------------------------------------------- 1 | // 下面是编译的源代码, 因为mac下无法编译通过registry库 2 | package driver // 去掉这行 3 | 4 | //package main 5 | // 6 | //import ( 7 | // "fmt" 8 | // "log" 9 | // "github.com/golang/sys/windows/registry" 10 | // "goBoss/config" 11 | // "strings" 12 | //) 13 | // 14 | //func main() { 15 | // k, err := registry.OpenKey(registry.CURRENT_USER, config.ChromeReg, registry.ALL_ACCESS) 16 | // if err != nil { 17 | // log.Fatal("获取Windows Chrome版本失败!请检查Chrome是否安装 Error: ", err) 18 | // } 19 | // s, _, err := k.GetStringValue("version") 20 | // if err != nil { 21 | // log.Fatal(err) 22 | // } 23 | // defer k.Close() 24 | // verList := strings.Split(s, ".") 25 | // ver := verList[0] 26 | // fmt.Println(ver) 27 | //} -------------------------------------------------------------------------------- /utils/request.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | type Request struct { 10 | Data string `json:"data"` 11 | Headers map[string]string `json:"headers"` 12 | Url string `json:"url"` 13 | Method string `json:"method"` 14 | } 15 | 16 | type H map[string]interface{} 17 | 18 | func (r *Request) Http() H { 19 | req, err := http.NewRequest(r.Method, r.Url, strings.NewReader(r.Data)) 20 | if err != nil { 21 | return H{ 22 | "status": false, 23 | "result": "请求出错! Error: " + err.Error(), 24 | } 25 | } 26 | for k, v := range r.Headers { 27 | req.Header.Add(k, v) 28 | } 29 | client := &http.Client{} 30 | resp, err := client.Do(req) 31 | if err != nil { 32 | return H{ 33 | "status": false, 34 | "result": "请求出错! Error: " + err.Error(), 35 | } 36 | } 37 | defer resp.Body.Close() 38 | res, _ := ioutil.ReadAll(resp.Body) 39 | return H{ 40 | "status": true, 41 | "result": res, 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | cf "goBoss/config" 6 | "goBoss/page" 7 | "log" 8 | "os" 9 | "goBoss/driver" 10 | "github.com/fedesog/webdriver" 11 | ) 12 | 13 | func main() { 14 | setLog() 15 | driver.SetDriver() // 自动获取浏览器驱动 16 | chromeDriver := webdriver.NewChromeDriver(fmt.Sprintf("%s/driver/%s", cf.Environ.Root, cf.Environ.DriverName)) 17 | lg := &page.Login{Driver: chromeDriver} 18 | 19 | lg.Start() 20 | lg.OpenBrowser() 21 | lg.Login() 22 | reply := make(map[string]bool) 23 | msgList := make([]map[string]string, 0) 24 | msg := &page.Message{ 25 | Driver: chromeDriver, Session: lg.Session, 26 | ReplyList: reply, MsgList: msgList, 27 | } 28 | msg.Listen() 29 | defer page.TearDown(lg) 30 | } 31 | 32 | func setLog() { 33 | //set logfile Stdout 34 | logFile, logErr := os.OpenFile(fmt.Sprintf("%s/boss.log", cf.Environ.Root), os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) 35 | if logErr != nil { 36 | fmt.Println("Fail to find", logFile, "cServer start Failed") 37 | os.Exit(1) 38 | } 39 | log.SetOutput(logFile) 40 | log.SetFlags(log.LstdFlags | log.Lshortfile) 41 | } 42 | -------------------------------------------------------------------------------- /page/element.json: -------------------------------------------------------------------------------- 1 | { 2 | "登录页面": { 3 | "用户名输入框": { 4 | "method": "css selector", 5 | "value": "input[name='account']" 6 | }, 7 | "密码输入框": { 8 | "method": "css selector", 9 | "value": "input[name='password']" 10 | }, 11 | "密码登录": { 12 | "method": "css selector", 13 | "value": "span.link-signin.cur" 14 | }, 15 | "验证码": { 16 | "method": "css selector", 17 | "value": ".sign-pwd .row-code img" 18 | }, 19 | "验证码输入框": { 20 | "method": "name", 21 | "value": "captcha" 22 | }, 23 | "登录": { 24 | "method": "css selector", 25 | "value": "button[type='submit']" 26 | }, 27 | "验证码错误": { 28 | "method": "css selector", 29 | "value": ".sign-form.sign-pwd .tip-error" 30 | } 31 | }, 32 | "首页": { 33 | "消息": { 34 | "method": "css selector", 35 | "value": "a[ka='header-message']" 36 | } 37 | }, 38 | "消息页面": { 39 | "消息列表": { 40 | "method": "css selector", 41 | "value": ".user-list ul li" 42 | }, 43 | "Boss信息": { 44 | "method": "css selector", 45 | "value": ".chat-record .fl span" 46 | }, 47 | "职位信息": { 48 | "method": "css selector", 49 | "value": ".chat-position-bar a span" 50 | }, 51 | "聊天内容": { 52 | "method": "css selector", 53 | "value": ".chat-message .item-friend .text span" 54 | }, 55 | "发送简历": { 56 | "method": "css selector", 57 | "value": ".btn-resume" 58 | }, 59 | "发送简历确认": { 60 | "method": "css selector", 61 | "value": ".sentence-popover.panel-resume .btn.btn-sure" 62 | }, 63 | "消息对话框": { 64 | "method": "css selector", 65 | "value": ".chat-input" 66 | }, 67 | "发送按钮": { 68 | "method": "css selector", 69 | "value": ".btn.btn-send" 70 | } 71 | 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /config/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "", 3 | "user": "17682345312", 4 | "password": "your pwd", 5 | "receiver": "619434176@qq.com", 6 | "sender": "wuranxu@126.com", 7 | "sender_pwd": "your pwd", 8 | "app_id": "10126357", 9 | "api_key": "8Kfi2hoCKe4ZqWtD70HDY2uB", 10 | "secret_key": "XSqY3T4G8gGmxSjFIkxTGHGtyR3RjdtE", 11 | "black_list": [ 12 | "直聘小秘书", "直聘助手", "柯莱特", "纬创", "博彦", "东软", "中软", "文思海辉", "软通" 13 | ], 14 | "retry": 3, 15 | "delay": 5, 16 | "headless": true, 17 | "auto_resume": true, 18 | "auto_download": true, 19 | "driver_url": "https://npm.taobao.org/mirrors/chromedriver/", 20 | "login_url": "https://login.zhipin.com/?ka=header-login", 21 | "host": "https://www.zhipin.com", 22 | "login_json": "https://login.zhipin.com/login/account.json", 23 | "msg_page": "https://www.zhipin.com/geek/new/userList.json?page=1", 24 | "job_json": "https://www.zhipin.com/geek/new/boss.json", 25 | "his_msg": "https://www.zhipin.com/geek/new/historyMsg.json", 26 | "resume_url": "https://www.zhipin.com/geek/new/requestSendResume.json", 27 | "web_timeout": 30, 28 | "baidu_token_url": "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials", 29 | "baidu_ocr_url": "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic", 30 | "star_company": [ 31 | "百度", "阿里", "口碑", "天猫", "盒马", "UC", "淘宝", "蚂蚁", "支付宝", "今日头条", "字节跳动", "腾讯", "滴滴", "bili", "美团", 32 | "点评", "饿了么", "京东", "喜马拉雅", "盛大", "拼多多", "链家", "58", "沪江", "bili", "哔哩", "二三四五", "2345", "猫眼", 33 | "陆金所", "小红书", "七牛", "musical", "虎扑", "小度", "唯品会", "苏宁", "平安", "携程", "有赞", "哈罗", "运满满", "蔚来", 34 | "巨人", "游族", "易果", "爱奇艺", "美味不用等", "号店", "360", "拍拍贷", "b站", "网易" 35 | ], 36 | "star_reply": "%s您好, 十分荣幸能受到大厂: %s的亲睐, 这是程序自动下发的消息, 如果您需要我的简历, 请在回复中带上\"简历\"字样。项目地址:https://github.com/wuranxu/goBoss", 37 | "black_reply": "您好, 暂时没有兴趣, 抱歉~", 38 | "common_reply": "您好, 这是一条由直聘机器人自动发送的消息, 请等待我本人查看..." 39 | } -------------------------------------------------------------------------------- /verify/code.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | cf "goBoss/config" 7 | "goBoss/utils" 8 | "log" 9 | "net/url" 10 | "strings" 11 | 12 | sp "github.com/bitly/go-simplejson" 13 | ) 14 | 15 | func GetCode(src string) string { 16 | token := GetBaiduToken() 17 | bs := GetPic(src) 18 | bs64 := Encode(base64.StdEncoding, bs) 19 | return SendPic(token, bs64) 20 | 21 | } 22 | 23 | func Encode(enc *base64.Encoding, bt []byte) string { 24 | // 编码 25 | encStr := enc.EncodeToString(bt) 26 | return encStr 27 | } 28 | 29 | func GetPic(src string) []byte { 30 | code_url := utils.Request{ 31 | Url: src, 32 | Method: "GET", 33 | } 34 | res := code_url.Http() 35 | bt_data := GetData(res) 36 | return bt_data 37 | } 38 | 39 | func SendPic(token, bs64 string) string { 40 | header := make(map[string]string) 41 | header["Content-Type"] = "application/x-www-form-urlencoded" 42 | values := url.Values{} 43 | values.Add("image", bs64) 44 | values.Add("language_type", "ENG") 45 | code_req := utils.Request{ 46 | Url: fmt.Sprintf("%s?access_token=%s", cf.Config.BaiduOcrUrl, token), 47 | Method: "POST", 48 | Headers: header, 49 | Data: values.Encode(), 50 | } 51 | res := code_req.Http() 52 | bt_data := GetData(res) 53 | data, _ := sp.NewJson(bt_data) 54 | word, e := data.GetPath("words_result").Array() 55 | if e != nil { 56 | log.Printf("调用百度API解析图片验证码失败, Error: %s", e.Error()) 57 | return "" 58 | } 59 | if len(word) > 0 { 60 | wd, _ := word[0].(map[string]interface{}) 61 | code := wd["words"] 62 | str := code.(string) 63 | str = strings.Replace(str, " ", "", -1) 64 | fmt.Printf("本次识别的验证码为: %s\n", str) 65 | return str 66 | } else { 67 | return "" 68 | } 69 | } 70 | 71 | func GetBaiduToken() string { 72 | token_request := utils.Request{ 73 | Url: cf.Environ.BaiduTokenUrl, 74 | Method: "GET", 75 | } 76 | res := token_request.Http() 77 | bt_data := GetData(res) 78 | data, _ := sp.NewJson(bt_data) 79 | token, err := data.Get("access_token").String() 80 | if err != nil { 81 | log.Panicf("获取百度api token出错, msg: %s", err.Error()) 82 | } 83 | return token 84 | } 85 | 86 | func GetData(res utils.H) []byte { 87 | if !res["status"].(bool) { 88 | log.Panicf("Http请求出错, Msg: %s", fmt.Sprintf("%v", res["result"])) 89 | } 90 | return res["result"].([]byte) 91 | } 92 | -------------------------------------------------------------------------------- /config/conf.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | Config UserConfig 16 | Environ Env 17 | ) 18 | 19 | const ( 20 | ChromeReg = `SOFTWARE\Google\Chrome\BLBeacon` 21 | ChromeApp = `/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome` 22 | ) 23 | 24 | type UserConfig struct { 25 | Directory string `json:"directory"` 26 | User string `json:"user"` 27 | Password string `json:"password"` 28 | Receiver string `json:"receiver"` 29 | Sender string `json:"sender"` 30 | SenderPwd string `json:"sender_pwd"` 31 | AppID string `json:"app_id"` 32 | APIKey string `json:"api_key"` 33 | SecretKey string `json:"secret_key"` 34 | BlackList []string `json:"black_list"` 35 | Retry int `json:"retry"` 36 | Delay int `json:"delay"` 37 | AutoResume bool `json:"auto_resume"` 38 | AutoDownload bool `json:"auto_download"` 39 | DriverUrl string `json:"driver_url"` 40 | LoginURL string `json:"login_url"` 41 | Host string `json:"host"` 42 | LoginJSON string `json:"login_json"` 43 | MsgPage string `json:"msg_page"` 44 | JobJSON string `json:"job_json"` 45 | HisMsg string `json:"his_msg"` 46 | ResumeURL string `json:"resume_url"` 47 | WebTimeout int `json:"web_timeout"` 48 | BaiduTokenUrl string `json:"baidu_token_url"` 49 | BaiduOcrUrl string `json:"baidu_ocr_url"` 50 | Headless bool `json:"headless"` 51 | StarCompany []string `json:"star_company"` 52 | StarReply string `json:"star_reply"` 53 | BlackReply string `json:"black_reply"` 54 | CommonReply string `json:"common_reply"` 55 | } 56 | 57 | type Env struct { 58 | Root string 59 | Sys string 60 | DriverName string 61 | DriverZip string 62 | BaiduTokenUrl string 63 | BaiduCode string 64 | } 65 | 66 | func GetCurrentDirectory() string { 67 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | return strings.Replace(dir, "\\", "/", -1) //将\替换成/ 72 | } 73 | 74 | func init() { 75 | Environ.Root = GetCurrentDirectory() 76 | // Environ.Root = "/Users/wuranxu/go/src/goBoss" 77 | // Environ.Root, _ = filepath.Abs(filepath.Dir(os.Args[0])) 78 | //Environ.Root = "C:/Users/Woody/go/src/goBoss" 79 | Environ.Sys = runtime.GOOS 80 | 81 | // 解析json 82 | data, err := ioutil.ReadFile(fmt.Sprintf("%s/config/data.json", Environ.Root)) 83 | if err != nil { 84 | log.Panicf("打开用户配置文件失败! Error: %s", err.Error()) 85 | } 86 | err = json.Unmarshal(data, &Config) 87 | if err != nil { 88 | log.Panicf("解析用户配置文件data.json失败!Error: %s", err.Error()) 89 | } 90 | 91 | Environ.BaiduTokenUrl = fmt.Sprintf(`%s&client_id=%s&client_secret=%s`, 92 | Config.BaiduTokenUrl, Config.APIKey, Config.SecretKey) 93 | Environ.BaiduCode = `https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic` 94 | 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goBoss 2 | 3 | 这是基于go语言编写的一款boss直聘机器人软件(牛人版)。附上[Python版本](https://github.com/wuranxu/Boss), 4 | 无需配置Go环境, 我会提供windows和macos的可执行程序。 5 | 6 | # 闪光点 7 | 8 | - 自动回复boss消息 9 | 10 | 回复消息有3种类型。可自行修改, 传入关键字即可(忽略大小写如b站)。消息同一个人只会回复一次。 11 | 12 | - 大厂 13 | 14 | - 普通 15 | 16 | - 黑名单 17 | 18 | - 自动发送简历 19 | 20 | 大厂专属, 先声明这里的大厂指的是心仪的公司, 而本人比较心仪这种公司, 所以改不了口了。 21 | 22 | 当自动回复以后, 大厂的回复中包含"简历"的子字符串, 则会自动发送您的附件简历。 23 | 24 | - 自动刷新消息 25 | 26 | 随时已读, 给人随时随地无时无刻不在的感觉。 27 | 28 | # 效果图 29 | 30 | - 自动回复(这里我特意注册了招聘者的号) 31 | 32 | ![image.png](https://upload-images.jianshu.io/upload_images/6053915-a571a172db5f84b4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 33 | 34 | ![image.png](https://upload-images.jianshu.io/upload_images/6053915-53b65f6096ece8ae.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 35 | 36 | ![image.png](https://upload-images.jianshu.io/upload_images/6053915-d4ee051d3a068c83.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 37 | 38 | map中key为boss名字, 可能会有重名情况。但是目前我只遍历前5条数据, 暂时还能用。value为发送消息/简历的状态, 如果key未找到说明没有回复过这个人, value为false代表简历未发送但是消息已发送, true代表消息和简历都已经发送。 39 | 40 | 41 | # 快速开始 42 | 43 | ### 下载 44 | - ```git clone https://github.com/wuranxu/goBoss.git``` 45 | 46 | - 下载zip文件并解压 47 | 48 | - 补充 49 | 50 | 如果本地go环境不全, 无法编译的胖友。可以进[release页面](https://github.com/wuranxu/goBoss/releases)下载goBoss.exe或goBoss 51 | 52 | 53 | ### 修改json配置文件(config/data.json) 54 | 55 | 百度API文字识别(每日500次免费),进入[官网](http://ai.baidu.com/tech/ocr/general)申请并配置, 配置文件目前是可用的, 供测试使用。 56 | 57 | - app_id 58 | 59 | - api_key 60 | 61 | - secret_key 62 | 63 | 用户密码配置 64 | 65 | - user(boss直聘手机号) 66 | 67 | - password(boss直聘登录密码) 68 | 69 | 其他配置 70 | 71 | 下面是我本人的配置, 注意, star_reply字段里的第一个%s代表对方姓名, 第二个%s代表对方公司名。如果去掉的话会报错(设计如此, 后续可修改), 黑名单我就不放出来了哈。O(∩_∩)O~ 72 | 73 | - black_list 74 | 75 | 黑名单公司关键字 76 | 77 | - delay 78 | 79 | 刷新页面获取消息间隔时间(单位: 秒) 80 | 81 | - headless 82 | 83 | true为无头模式, false为展示正常模式 84 | 85 | **其他配置项未使用或功能未完善** 86 | 87 | ```Javascript 88 | { 89 | 90 | "star_company": [ 91 | "百度", "阿里", "口碑", "天猫", "盒马", "UC", "淘宝", "蚂蚁", "支付宝", "今日头条", "字节跳动", "腾讯", "滴滴", "bili", "美团", 92 | "点评", "饿了么", "京东", "喜马拉雅", "盛大", "拼多多", "链家", "58", "沪江", "bili", "哔哩", "二三四五", "2345", "猫眼", 93 | "陆金所", "小红书", "七牛", "musical", "虎扑", "小度", "唯品会", "苏宁", "平安", "携程", "有赞", "哈罗", "运满满", "蔚来", 94 | "巨人", "游族", "易果", "爱奇艺", "美味不用等", "号店", "360", "拍拍贷", "b站", "网易" 95 | ], 96 | "star_reply": "%s您好, 十分荣幸能受到大厂: %s的亲睐, 这是程序自动下发的消息, 如果您需要我的简历, 请在回复中带上\"简历\"字样。项目地址:https://github.com/wuranxu/goBoss", 97 | "black_reply": "您好, 暂时没有兴趣, 抱歉~", 98 | "common_reply": "您好, 这是一条由直聘机器人自动发送的消息, 请等待我本人查看..." 99 | 100 | } 101 | 102 | ``` 103 | 104 | - 运行 105 | 106 | 之后就可以双击main.exe(windows)或者main挂起你的聊天机器人了。 107 | 108 | 注意: **windows下要用管理员身份开启main.exe, 而且最好杀毒软件信任。** 109 | 110 | 111 | # todolist 112 | 还有很多不完善, 没做好的。之后填坑, 首当其冲就是解决用户需要手动安装浏览器驱动的问题。 113 | 114 | - 发简历后邮件通知 115 | 116 | - 低薪过滤 117 | - 工作地点筛选 118 | - chromedriver自动下载(done) 119 | - 对方连续发送表情时会接收不到新消息的bug(因为表情不是文本, 在web页面属于icon) 120 | - 去除time.Sleep这种丑陋的等待元素方式 121 | 122 | 123 | -------------------------------------------------------------------------------- /page/location.go: -------------------------------------------------------------------------------- 1 | package page 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "goBoss/config" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "time" 12 | 13 | dr "github.com/fedesog/webdriver" 14 | ) 15 | 16 | type Element struct { 17 | Method dr.FindElementStrategy `json:"method,omitempty"` 18 | Value string `json:"value,omitempty"` 19 | } 20 | 21 | type Method interface { 22 | GetEle(s *dr.Session) (dr.WebElement, error) 23 | GetElements(s *dr.Session) ([]dr.WebElement, error) 24 | SendKeys(s *dr.Session, text string) error 25 | Click(s *dr.Session) error 26 | Attr(s *dr.Session, attribute string) (string, error) 27 | Text(s *dr.Session) (string, error) 28 | WaitAndGet(now time.Time, s *dr.Session) (dr.WebElement, error) 29 | } 30 | 31 | var Page = map[string]map[string]Element{} 32 | 33 | func GetElement(root, name string) *Element { 34 | ele, ok := Page[root][name] 35 | if !ok { 36 | log.Panicf("page/element.json未找到root: [%s] key: [%s]", root, name) 37 | } 38 | return &ele 39 | } 40 | 41 | func (e *Element) GetEle(s *dr.Session) (dr.WebElement, error) { 42 | ele, err := s.FindElement(e.Method, e.Value) 43 | return ele, err 44 | } 45 | 46 | func (e *Element) GetElements(s *dr.Session) ([]dr.WebElement, error) { 47 | ele, err := s.FindElements(e.Method, e.Value) 48 | return ele, err 49 | } 50 | 51 | func (e *Element) Click(s *dr.Session) error { 52 | ele, err := e.GetEle(s) 53 | if err != nil { 54 | return err 55 | } 56 | if status, e := ele.IsEnabled(); !status { 57 | return e 58 | } 59 | // ele, err := e.WaitAndGet(time.Now(), s) 60 | return ele.Click() 61 | } 62 | 63 | func (e *Element) SendKeys(s *dr.Session, text string) error { 64 | ele, err := e.GetEle(s) 65 | if err != nil { 66 | return err 67 | } 68 | return ele.SendKeys(text) 69 | } 70 | 71 | func (e *Element) Attr(s *dr.Session, attribute string) (string, error) { 72 | ele, err := e.GetEle(s) 73 | if err != nil { 74 | return "", err 75 | } 76 | return ele.GetAttribute(attribute) 77 | } 78 | 79 | func (e *Element) Text(s *dr.Session) (string, error) { 80 | ele, err := e.GetEle(s) 81 | if err != nil { 82 | return "", err 83 | } 84 | return ele.Text() 85 | } 86 | 87 | func (e *Element) WaitAndGet(now time.Time, s *dr.Session) (dr.WebElement, error) { 88 | tm := config.Config.WebTimeout 89 | defer func() { 90 | if err := recover(); err != nil { 91 | fmt.Printf("开始时间: %v 现在时间: %v元素暂时还未找到:Error: %v\n", now, time.Now(), err) 92 | e.WaitAndGet(now, s) 93 | } 94 | }() 95 | for { 96 | ele, err := e.GetEle(s) 97 | if err == nil { 98 | return ele, nil 99 | } 100 | if ses := time.Now().Sub(now).Seconds(); ses > float64(tm) { 101 | fmt.Printf("耗时: %v", ses) 102 | return dr.WebElement{}, errors.New(fmt.Sprintf("等待元素[%+v]超时: ", e)) 103 | } 104 | time.Sleep(1 * time.Second) // 等待1秒后继续寻找 105 | } 106 | 107 | } 108 | 109 | func TearDown(w *Login) { 110 | // 截图screen 111 | defer w.Close() 112 | pic, _ := w.Session.Screenshot() 113 | filename := fmt.Sprintf("%s_error.png", time.Now().Format("2006_01_02_15_04_05")) 114 | f, err := os.OpenFile(fmt.Sprintf("%s/picture/%s", config.Environ.Root, filename), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) 115 | if err != nil { 116 | log.Printf("截图失败!Error: %s", err.Error()) 117 | } 118 | f.Write(pic) 119 | defer f.Close() 120 | } 121 | 122 | func init() { 123 | data, err := ioutil.ReadFile(fmt.Sprintf("%s/page/element.json", config.Environ.Root)) 124 | if err != nil { 125 | log.Panicf("打开定位配置文件失败! Error: %s", err.Error()) 126 | } 127 | err = json.Unmarshal(data, &Page) 128 | if err != nil { 129 | log.Panicf("解析用户配置文件data.json失败!Error: %s", err.Error()) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /page/launch.go: -------------------------------------------------------------------------------- 1 | package page 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | cf "goBoss/config" 7 | "goBoss/utils" 8 | "goBoss/verify" 9 | "log" 10 | "time" 11 | 12 | "github.com/fedesog/webdriver" 13 | ) 14 | 15 | type Login struct { 16 | Driver *webdriver.ChromeDriver 17 | Session *webdriver.Session 18 | } 19 | 20 | func Assert(err error) { 21 | if err != nil { 22 | log.Printf("Error: %s", err.Error()) 23 | // panic("程序遇到问题啦, 请检查截图和日志...") 24 | } 25 | } 26 | 27 | func MaxWindow(w *Login) error { 28 | p := fmt.Sprintf(`{"windowHandle": "current", "sessionId": "%s"}`, w.Session.Id) 29 | req := utils.Request{ 30 | Data: p, 31 | Method: "POST", 32 | Url: fmt.Sprintf("http://127.0.0.1:%d/session/%s/window/current/maximize", w.Driver.Port, w.Session.Id), 33 | } 34 | 35 | res := req.Http() 36 | if !res["status"].(bool) { 37 | log.Printf("response: %+v", res) 38 | return errors.New(fmt.Sprintf(`最大化窗口失败, 请检查!%+v`, res["msg"])) 39 | } 40 | return nil 41 | } 42 | 43 | func SetWindow(w *Login, width, height int) error { 44 | p := fmt.Sprintf(`{"windowHandle": "current", "sessionId": "%s", "height": %d, "width": %d}`, w.Session.Id, height, width) 45 | req := utils.Request{ 46 | Data: p, 47 | Method: "POST", 48 | Url: fmt.Sprintf("http://127.0.0.1:%d/session/%s/window/current/size", w.Driver.Port, w.Session.Id), 49 | } 50 | 51 | res := req.Http() 52 | if !res["status"].(bool) { 53 | log.Printf("response: %+v", res) 54 | return errors.New(fmt.Sprintf(`设置浏览器窗口失败, 请检查!%+v`, res["msg"])) 55 | } 56 | return nil 57 | } 58 | 59 | func MaxWin(w *Login) error { 60 | args := make([]interface{}, 0) 61 | _, err := w.Session.ExecuteScript(`window.resizeTo( screen.availWidth, screen.availHeight );`, args) 62 | return err 63 | } 64 | 65 | func (w *Login) Start() { 66 | var err error 67 | w.Driver.Start() 68 | args := make([]string, 0) 69 | if cf.Config.Headless { 70 | args = append(args, "--headless") 71 | } 72 | desired := webdriver.Capabilities{ 73 | "Platform": "Mac", 74 | "goog:chromeOptions": map[string][]string{"args": args, "extensions": []string{}}, 75 | "browserName": "chrome", 76 | "version": "", 77 | "platform": "ANY", 78 | } 79 | required := webdriver.Capabilities{} 80 | w.Session, err = w.Driver.NewSession(desired, required) 81 | if err != nil { 82 | log.Printf("open browser failed: %s", err.Error()) 83 | } 84 | 85 | } 86 | 87 | func (w *Login) OpenBrowser() { 88 | w.Session.Url(cf.Config.LoginURL) 89 | // err := MaxWindow(w) 90 | // err := MaxWin(w) 91 | err := SetWindow(w, 1600, 900) 92 | if err != nil { 93 | log.Panicf("最大化浏览器失败!!!Msg: %s", err.Error()) 94 | } 95 | w.Session.SetTimeoutsImplicitWait(cf.Config.WebTimeout) 96 | } 97 | 98 | func (w *Login) sendCode() { 99 | // 识别验证码 100 | for { 101 | // image, err := w.Session.FindElement(lg["验证码"].Method, lg["验证码"].Value) 102 | image := GetElement("登录页面", "验证码") 103 | src, err := image.Attr(w.Session, "src") 104 | Assert(err) 105 | // src, _ := image.GetAttribute("src") 106 | code := verify.GetCode(src) 107 | if len(code) != 4 { 108 | // 验证码识别有误 109 | time.Sleep(3 * time.Second / 2) 110 | fmt.Println("验证码长度不为4, 重新获取!") 111 | image.Click(w.Session) 112 | continue 113 | } else { 114 | err = GetElement("登录页面", "验证码输入框").SendKeys(w.Session, code) 115 | Assert(err) 116 | 117 | err = GetElement("登录页面", "登录").Click(w.Session) 118 | Assert(err) 119 | time.Sleep(3 * time.Second / 2) 120 | text, _ := GetElement("登录页面", "验证码错误").Text(w.Session) 121 | // Assert(err) 122 | if text == "" { 123 | // 登录成功, break 124 | fmt.Println("恭喜您登录成功...") 125 | break 126 | } else { 127 | fmt.Println("验证码错误, 重新登录...") 128 | time.Sleep(3 * time.Second / 2) 129 | GetElement("登录页面", "验证码").Click(w.Session) 130 | continue 131 | } 132 | } 133 | 134 | } 135 | } 136 | 137 | func (w *Login) Login() { 138 | 139 | // 进入密码登录页面 140 | err := GetElement("登录页面", "密码登录").Click(w.Session) 141 | Assert(err) 142 | err = GetElement("登录页面", "用户名输入框").SendKeys(w.Session, cf.Config.User) 143 | Assert(err) 144 | err = GetElement("登录页面", "密码输入框").SendKeys(w.Session, cf.Config.Password) 145 | Assert(err) 146 | w.sendCode() 147 | 148 | } 149 | 150 | func (w *Login) Close() { 151 | w.Session.CloseCurrentWindow() 152 | w.Driver.Stop() 153 | 154 | } 155 | -------------------------------------------------------------------------------- /page/message.go: -------------------------------------------------------------------------------- 1 | package page 2 | 3 | import ( 4 | "fmt" 5 | cf "goBoss/config" 6 | "log" 7 | "time" 8 | 9 | "strings" 10 | 11 | "github.com/fedesog/webdriver" 12 | ) 13 | 14 | type Message struct { 15 | Driver *webdriver.ChromeDriver 16 | Session *webdriver.Session 17 | MsgList []map[string]string 18 | ReplyList map[string]bool 19 | } 20 | 21 | func (m *Message) Listen() { 22 | m.EnterMessage() 23 | m.Receive() 24 | 25 | } 26 | 27 | func (m *Message) Receive() { 28 | for { 29 | fmt.Printf("[%s]---正在获取消息列表\n", time.Now().Format("2006-01-02 15:04:05")) 30 | msgList, latest := m.GetMsgList() 31 | if len(msgList) > 0 { 32 | if msgList[0]["bossName"] == m.MsgList[0]["bossName"] && latest == m.MsgList[0]["latest"] { 33 | // 没有新boss消息 34 | fmt.Printf("您没有新消息哦\n最新职位为: %+v\n消息为: %s\n", msgList[0], latest) 35 | m.ReFetch() 36 | continue 37 | } else { 38 | bossName, company := msgList[0]["bossName"], msgList[0]["company"] 39 | star := m.IsStar(company) 40 | if status, ok := m.ReplyList[bossName]; !ok { 41 | // 发送消息 42 | m.SendMsg(star, bossName, company) 43 | m.ReFetch() 44 | continue 45 | } else { 46 | if star == "star" { 47 | // 回复包含简历且未发送过简历 48 | if strings.Contains(latest, "简历") && !status { 49 | // 发送简历 50 | m.SendInfo(bossName, company) 51 | m.ReplyList[bossName] = true 52 | } 53 | } 54 | // 非大厂不自动发送简历 55 | } 56 | } 57 | fmt.Printf("您的最新职位为: %+v\n消息为: %s\n", msgList[0], latest) 58 | } else { 59 | m.ReFetch() 60 | continue 61 | } 62 | fmt.Printf("[%s]---发送消息列表: %+v\n", time.Now().Format("2006-01-02 15:04:05"), m.ReplyList) 63 | } 64 | } 65 | 66 | func (m *Message) SendMsg(companyType, bossName, company string) { 67 | var reply string 68 | dialog := GetElement("消息页面", "消息对话框") 69 | switch { 70 | case companyType == "star": 71 | reply = fmt.Sprintf(cf.Config.StarReply, bossName, company) 72 | case companyType == "black": 73 | reply = cf.Config.BlackReply 74 | default: 75 | reply = cf.Config.CommonReply 76 | } 77 | err := dialog.SendKeys(m.Session, reply) 78 | //args := make([]interface{}, 0) 79 | //_, err := m.Session.ExecuteScript(fmt.Sprintf(`document.querySelector("%s").innerHTML="%s";`, dialog.Value, reply), args) 80 | time.Sleep(4 * time.Second) 81 | Assert(err) 82 | err = GetElement("消息页面", "发送按钮").Click(m.Session) 83 | if err != nil { 84 | fmt.Printf("自动回复失败!内容: %s, 接受者公司: %s, 接受者: %s\n Error: %s\n", reply, company, bossName, err.Error()) 85 | } 86 | fmt.Printf("自动回复成功!内容: %s, 接受者公司: %s, 接受者: %s\n", reply, company, bossName) 87 | m.ReplyList[bossName] = false 88 | } 89 | 90 | func (m *Message) IsStar(company string) string { 91 | // 判断是否是大厂 92 | stars := cf.Config.StarCompany 93 | black_list := cf.Config.BlackList 94 | for _, star := range stars { 95 | if strings.Contains(strings.ToUpper(company), strings.ToUpper(star)) { 96 | return "star" 97 | } 98 | } 99 | for _, black := range black_list { 100 | if strings.Contains(strings.ToUpper(company), strings.ToUpper(black)) { 101 | return "black" 102 | } 103 | } 104 | return "common" 105 | } 106 | 107 | func (m *Message) SendInfo(bossName, company string) { 108 | err := GetElement("消息页面", "发送简历").Click(m.Session) 109 | if err != nil { 110 | fmt.Printf("遇到问题: 发送简历给公司: %s Boss: %s 出错!Error: %s\n", company, bossName, err.Error()) 111 | } 112 | time.Sleep(2 * time.Second) 113 | err = GetElement("消息页面", "发送简历确认").Click(m.Session) 114 | Assert(err) 115 | fmt.Printf("发送简历给公司: %s Boss: %s 成功!", company, bossName) 116 | } 117 | 118 | func (m *Message) ReFetch() { 119 | //没有新消息或者没有消息 120 | fmt.Printf("[%s]---正在重新获取消息\n", time.Now().Format("2006-01-02 15:04:05")) 121 | m.Session.Refresh() 122 | time.Sleep(time.Duration(cf.Config.Delay) * time.Second) // 延迟Delay秒刷新 123 | } 124 | 125 | func (m *Message) EnterMessage() { 126 | time.Sleep(5 * time.Second) 127 | err := GetElement("首页", "消息").Click(m.Session) 128 | Assert(err) 129 | } 130 | 131 | func (m *Message) GetMsgList() ([]map[string]string, string) { 132 | var lt string 133 | time.Sleep(3 * time.Second) 134 | // 获取消息列表 135 | messageList, e := GetElement("消息页面", "消息列表").GetElements(m.Session) 136 | Assert(e) 137 | if len(messageList) == 0 { 138 | // 消息列表为空, 持续检查 139 | log.Printf("消息列表为空, 请检查!") 140 | return []map[string]string{}, "" 141 | } 142 | msgList := make([]map[string]string, 0) 143 | for i, ms := range messageList[:5] { 144 | ms.Click() 145 | // 输出前10条最新消息 146 | time.Sleep(2 * time.Second) 147 | info := m.getInfo() 148 | 149 | eles, _ := GetElement("消息页面", "聊天内容").GetElements(m.Session) 150 | var latest string 151 | if len(eles) > 0 { 152 | latest, _ = eles[len(eles)-1].Text() 153 | } 154 | if i == 0 { 155 | lt = latest 156 | } 157 | info["latest"] = latest 158 | msgList = append(msgList, info) 159 | time.Sleep(1 * time.Second) 160 | } 161 | // for _, msg := range msgList { 162 | // fmt.Printf("%+v\n", msg) 163 | // } 164 | if len(m.MsgList) == 0 { 165 | m.MsgList = msgList 166 | return msgList, lt 167 | } 168 | // 回到第一个对话 169 | messageList[0].Click() 170 | time.Sleep(2 * time.Second) 171 | return msgList, lt 172 | } 173 | 174 | func (m *Message) getInfo() map[string]string { 175 | info := make(map[string]string) 176 | bossEle, err := GetElement("消息页面", "Boss信息").GetElements(m.Session) 177 | Assert(err) 178 | if len(bossEle) > 0 { 179 | info["bossName"], _ = bossEle[0].Text() 180 | info["company"], _ = bossEle[1].Text() 181 | info["bossTitle"], _ = bossEle[2].Text() 182 | } 183 | jobEle, err := GetElement("消息页面", "职位信息").GetElements(m.Session) 184 | Assert(err) 185 | if len(jobEle) > 0 { 186 | info["position"], _ = jobEle[1].Text() 187 | info["money"], _ = jobEle[2].Text() 188 | info["base"], _ = jobEle[3].Text() 189 | } 190 | for k, v := range info { 191 | info[k] = strings.Replace(v, " ", "", -1) 192 | } 193 | return info 194 | } -------------------------------------------------------------------------------- /driver/checkdriver.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "goBoss/config" 6 | 7 | "archive/zip" 8 | "bytes" 9 | "goBoss/utils" 10 | "io" 11 | "log" 12 | "os" 13 | "os/exec" 14 | "regexp" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | func SetDriver() { 20 | ver := getChromeVer() 21 | getDriver(ver) 22 | execDriver() 23 | } 24 | 25 | func getDriver(ver string) { 26 | drVer := getDriverVer(ver) 27 | setDriverName(drVer) 28 | status, err := checkDriver() 29 | if !status { 30 | if err != nil { 31 | log.Fatal("查找driver目录下的", config.Environ.DriverName, "失败!") 32 | } else { 33 | downloadDriver(drVer) 34 | return 35 | } 36 | } 37 | log.Println("驱动已存在, 无需重新下载...") 38 | } 39 | 40 | func setDriverName(drVer string) { 41 | switch config.Environ.Sys { 42 | case "windows": 43 | config.Environ.DriverName = fmt.Sprintf("chromedriver%s.exe", drVer) 44 | config.Environ.DriverZip = "chromedriver_win32.zip" 45 | case "darwin": 46 | config.Environ.DriverName = fmt.Sprintf("chromedriver%s", drVer) 47 | config.Environ.DriverZip = "chromedriver_mac64.zip" 48 | default: 49 | config.Environ.DriverName = fmt.Sprintf("chromedriver%s", drVer) 50 | config.Environ.DriverZip = "chromedriver_linux64.zip" 51 | } 52 | } 53 | 54 | func execDriver() { 55 | // mac os 56 | if config.Environ.Sys == "darwin" { 57 | cmd := exec.Command("sh", "-c", fmt.Sprintf("chmod +x %s/driver/%s", config.Environ.Root, 58 | config.Environ.DriverName)) 59 | err := cmd.Run() 60 | if err != nil { 61 | log.Fatal("生成chromedriver失败..Error: ", err.Error()) 62 | } 63 | } 64 | } 65 | 66 | func downloadDriver(s string) { 67 | log.Println("正在下载chromedriver驱动, 版本: ", s) 68 | zipfileName := fmt.Sprintf("%s/driver/%s", config.Environ.Root, config.Environ.DriverZip) 69 | req := utils.Request{ 70 | Url: fmt.Sprintf("%s%s/%s", config.Config.DriverUrl, s, config.Environ.DriverZip), 71 | Method: "GET", 72 | } 73 | res := req.Http() 74 | if result := res["result"].([]byte); !res["status"].(bool) { 75 | log.Panicf("下载浏览器驱动版本失败, 请检查Url是否更换: %s!%s", req.Url, string(result)) 76 | } else { 77 | // 下载浏览器驱动zip 78 | f, _ := os.OpenFile(zipfileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 79 | f.Write(result) 80 | f.Close() 81 | } 82 | // 解压文件 83 | r, err := zip.OpenReader(zipfileName) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | defer r.Close() 88 | 89 | // 迭代压缩文件 90 | fmt.Println("正在解压", config.Environ.DriverZip) 91 | for _, fl := range r.File { 92 | if strings.Contains(fl.Name, "chromedriver") { 93 | rc, err := fl.Open() 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | f, err := os.OpenFile(fmt.Sprintf("%s/driver/%s", config.Environ.Root, config.Environ.DriverName), 98 | os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 99 | io.CopyN(f, rc, int64(fl.UncompressedSize64)) 100 | rc.Close() 101 | f.Close() 102 | fmt.Println("浏览器驱动准备就绪...") 103 | return 104 | } 105 | } 106 | 107 | } 108 | 109 | func checkDriver() (bool, error) { 110 | _, err := os.Stat(fmt.Sprintf("%s/driver/%s", config.Environ.Root, config.Environ.DriverName)) 111 | if err == nil { 112 | return true, nil 113 | } 114 | if os.IsNotExist(err) { 115 | return false, nil 116 | } 117 | return false, err 118 | } 119 | 120 | func getDriverVer(number string) string { 121 | var file_vr string 122 | req := utils.Request{ 123 | Url: fmt.Sprintf("%sLATEST_RELEASE", config.Config.DriverUrl), 124 | Method: "GET", 125 | } 126 | res := req.Http() 127 | var latest string 128 | if result := string(res["result"].([]byte)); !res["status"].(bool) { 129 | log.Panicf("获取浏览器驱动版本失败, 请检查Url是否更换: %s!%s", req.Url, result) 130 | } else { 131 | latest = result 132 | } 133 | req.Url = fmt.Sprintf("%s%s/notes.txt", config.Config.DriverUrl, latest) 134 | res = req.Http() 135 | info := res["result"].([]byte) 136 | reg, _ := regexp.Compile(`-+ChromeDriver\s+v(\d+\.+\d+)[\s|.|-|]+`) 137 | regSp, _ := regexp.Compile(`Supports\s+Chrome\s+v(\d+-\d+)`) 138 | dr := reg.FindAll(info, -1) 139 | sp := regSp.FindAll(info, -1) 140 | for i, s := range sp { 141 | vers := strings.Split(string(s), "-") 142 | small, bigger := vers[0], vers[1] 143 | small = delReg(small) 144 | vr := delReg(string(dr[i])) 145 | sm, _ := strconv.ParseInt(small, 10, 64) 146 | bg, _ := strconv.ParseInt(bigger, 10, 64) 147 | now, _ := strconv.ParseInt(string(number), 10, 64) 148 | if now >= sm && now <= bg { 149 | file_vr = vr 150 | log.Println("找到浏览器对应驱动版本号: ", vr) 151 | return file_vr 152 | } 153 | } 154 | return file_vr 155 | } 156 | 157 | func delReg(s string) string { 158 | smList := strings.Split(s, "v") 159 | str := smList[len(smList)-1] 160 | return strings.Replace(str, " ", "", -1) 161 | } 162 | 163 | func getChromeVer() string { 164 | var ver string 165 | switch config.Environ.Sys { 166 | case "windows": 167 | ver = getWinChromeVer() 168 | default: 169 | ver = getUnixChromeVer() 170 | } 171 | log.Println("成功获取到本机Chrome版本: ", ver) 172 | return ver 173 | } 174 | 175 | func getWinChromeVer() string { 176 | cmd := exec.Command(fmt.Sprintf("%s/getWinReg.exe", config.Environ.Root), "--version") 177 | cmdOutput := &bytes.Buffer{} 178 | cmd.Stdout = cmdOutput 179 | err := cmd.Run() 180 | if err != nil { 181 | log.Fatal("获取Windows Chrome版本失败!请检查Chrome是否安装 Error: ", err) 182 | } 183 | ver := string(cmdOutput.Bytes()) 184 | return strings.Replace(ver, "\n", "", -1) 185 | } 186 | 187 | func getUnixChromeVer() string { 188 | cmd := exec.Command("sh", "-c", fmt.Sprintf("%s --version", config.ChromeApp)) 189 | cmdOutput := &bytes.Buffer{} 190 | cmd.Stdout = cmdOutput 191 | err := cmd.Run() 192 | if err != nil { 193 | log.Fatal("获取Mac Chrome版本失败!请检查Chrome是否安装 Error: ", err) 194 | } 195 | ver := string(cmdOutput.Bytes()) 196 | verList := strings.Split(ver, ".") 197 | ver = verList[0] 198 | // fmt.Println(ver) 199 | verList = strings.Split(ver, " ") 200 | return verList[len(verList)-1] 201 | } 202 | --------------------------------------------------------------------------------