├── .gitignore ├── README.md ├── docs └── table.sql ├── downloader └── job.go ├── fake ├── headers.go └── ip.go ├── main.go ├── model └── model.go ├── pipeline └── job.go ├── pkg ├── convert │ ├── job.go │ └── time.go ├── page │ └── page.go └── uuid │ └── uuid.go ├── scheduler └── job.go └── spider └── job.go /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Go 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | *.so 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | etl.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | deploypkg/* 29 | .idea/ 30 | .DS_Store 31 | .vscode 32 | debug -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 了解一下Golang的市场行情 2 | 3 | 项目地址:https://github.com/go-crawler/go_jobs 4 | 5 | 如果对你有所帮助,欢迎 Star,图床使用的是国外的,如果看不到图片的话你可能需要翻墙。 6 | 7 | ## 目标 8 | 9 | 在工作中 Golang 已是一份子,想让大家了解一下 Golang 的市场行情,也想让更多的人熟悉它。因此主要是展示数据分析的结果,城市分别为 北京、上海、广州、深圳、杭州、成都,再得出一个结论。 10 | 11 | ## 数据 12 | 13 | ### 一、分布图 14 | 15 | 不同工作、工种,自然也会遍布在不同的工作区域,我们先了解一下各个城市的 Golang 工程师都主要在哪个区上班,心里留个底 16 | 17 | #### 北京 18 | 19 | ![image](https://i.loli.net/2018/04/27/5ae291859667c.jpeg) 20 | 21 | #### 上海 22 | 23 | ![image](https://i.loli.net/2018/04/27/5ae290856b774.jpeg) 24 | 25 | #### 广州 26 | 27 | ![image](https://i.loli.net/2018/04/27/5ae28f1ab3e0c.jpeg) 28 | 29 | #### 深圳 30 | 31 | ![image](https://i.loli.net/2018/04/27/5ae1fbebb1784.jpeg) 32 | 33 | #### 杭州 34 | 35 | ![image](https://i.loli.net/2018/04/27/5ae29218c91dc.jpeg) 36 | 37 | #### 成都 38 | 39 | ![image](https://i.loli.net/2018/04/27/5ae295b1059ed.jpeg) 40 | 41 | ### 二、招聘与职位数量对比 42 | 43 | ![image](https://i.loli.net/2018/04/27/5ae296b750dd8.png) 44 | 45 | 通过分析图中的数据,我们可以得知各城市的招聘职位数量 46 | - 北京:348 47 | - 上海:145 48 | - 广州:37 49 | - 成都:49 50 | - 杭州:45 51 | - 深圳:108 52 | 53 | 总共招聘的职位数量为 732 个,数量顺序分别为 北京 > 上海 > 深圳 > 成都 > 杭州 > 广州 54 | 55 | 还有另外一个关注点,就是招聘公司数量与职位的数量对比,可以看到 北京 招聘的职位数量为 348 个,而招聘的公司数量为 191 个,约为 1.82 的比例,也就是一家公司能提供两个 Golang 职位,它可能类别不同、(中级、中高级、高级)级别不同,具有一定可能性。而在广州,为 31 对比 37,虽然差额不大,但仍然存在这种现象 56 | 57 | 可以得出结果,Golang 在市场上具有一定的伸缩空间,也就是具有上升空间,一家公司会将 Golang 应用在多个不同的应用场景,也就是方向不同,需要的级别人才也就不同了 58 | 59 | 但是需要注意的是,Golang 的市场招聘人数目前份额还是较低,六个城市总数仅为 732 个,与其他大热语言相差有一定距离,需要谨慎 60 | 61 | 同时,面试 Golang 的人与其他大热语言相比会少些,职位的争夺是否小点呢? 62 | 63 | ### 三、招聘公司规模 64 | 65 | ![image](https://i.loli.net/2018/04/27/5ae2ab2babbd9.png) 66 | 67 | 通过查看招聘 Golang 工程师的公司规模,可以很直观的发现,微型公司使用 Golang 较少,其他类别的规模都有一定程度的应用,且差距不大。在 2000 人以上、50 - 150 人的公司规模中最受青睐 68 | 69 | 为什么呢,我认为有以下可能 70 | - 大型公司结合场景,想通过 Golang 的特性来解决一些痛点问题 71 | - 在小型公司 Golang 这颗新星实施起来更便捷,有一定的应用场景 72 | 73 | 你觉得呢,是不是应该有更多的选择它的原因? 74 | 75 | ### 四、学历要求 76 | 77 | ![image](https://i.imgur.com/dYOz2xB.png) 78 | 79 | 在招聘市场上,Golang 的招聘者更希望你是本科学历,大专和不限也有一定的份额,但市场份额相差较大 80 | 81 | 硕士学历要求的为两个,可以得出,在市场上 Golang 招聘者们对高学历的需求并不高,或者并不强制高学历 82 | 83 | ### 五、行业领域 84 | 85 | ![image](https://i.imgur.com/VdpmS5a.png) 86 | 87 | 在这里,重点关注 Golang 工程师的招聘公司都分别在什么行业领域,大头移动互联网是不容置疑的了,还可以惊喜的发现 88 | 89 | - 数据服务 90 | - 电子商务 91 | - 金融 92 | - 企业服务 93 | - 游戏 94 | 95 | Golang 在这几个方面都有所应用,说明了在市场上,Golang 的路子是比较广阔的,前景不错 96 | 97 | 同时,如果可以涉及多个领域的内容,想必身为工程师的你,肯定很激动 98 | 99 | ### 六、职位诱惑 100 | 101 | ![image](https://i.imgur.com/KlkGFI4.png) 102 | 103 | 职位诱惑是投简历时必看的一点了,可以看到高频词条基本都是 IT 从业者关心的话题了,这里你懂的... 104 | 105 | 重点,我看到了一个 “免费三餐” 的词条命中 7 次,分别来自北京的海淀区、东城区、朝阳区,上海的黄浦区的七家不同的公司,辛苦了 106 | 107 | ### 七、行业、职位标签 108 | 109 | ![image](https://i.imgur.com/8GGslq8.png) 110 | 111 | 在招聘JD中,描述和标签常用于给求职者了解这一职业的具体工作内容和其关联性 112 | 113 | 在图中你可以看到 Golang 常常和什么内容搭上边,这点很有意义哦 114 | 115 | 1、语言 116 | - Java 117 | - Python 118 | - C/C++ 119 | - PHP 120 | 121 | 在图中可以看出,Golang 与以上四种语言有一定关联性,而 Java 和 Python 分别第一、第二名,可以说明市场上对复合型人才的渴望度更高,也许你不懂也行,但你懂了就最好(加分项)。需要你自身有多语言的经验,也便于和其他人对接 122 | 123 | 同时 Golang 目前存在许多内部转语言写的情况,所以这一点可以参考 124 | 125 | 2、职称 126 | - 高级 127 | - 资深 128 | - 中级 129 | 130 | 特意将职称放在第二位,可以发现在市场上 Golang 标签的需求是 高级 > 资深 > 中级,关联第一项 “语言关联” 不难得出这个结论,因为语言只是解决问题的工具,到了中级及以上的工程师都是懂多门语言的居多,再采取不同的方案去解决应用场景上的问题 131 | 132 | 可得出结论,市场目前对 Golang 更期望是中高、高级、资深的人才,而中级的反而少一点点 133 | 134 | 大家可以努力再往上冲击冲击 135 | 136 | 3、组件 137 | - Linux 138 | - Redis 139 | - Mysql 140 | 141 | 4、行业 142 | - 云计算 143 | - 信息安全 144 | - 大数据 145 | - 金融 146 | - 软件开发 147 | 148 | #### 八、薪资与工作年限 149 | 150 | ![image](https://i.imgur.com/CEYqOau.jpg) 151 | 152 | 153 | 1、1-3年 154 | 155 | 一个(成长)特殊的阶段,有个位数也有双位数的,大头可以到15-30k,20-40k,而初级的也有8-16k 156 | 157 | 2、3-5年 158 | 159 | 厚积待发的阶段,薪酬范畴的跨度是较大,10-60k的薪酬都有,这充分说明能力决定你的上下 160 | 161 | 3、5-10年 162 | 163 | 核心,招聘网站上的招聘数量反而少,都会走内推或猎头,不需要特别介绍了 164 | 165 | ##### 小结 166 | 167 | 这一部分,相信是很多人关注的地方 168 | 169 | 在有的文章中会看到,他们的薪资部分是以平均值来展示的。我就很纳闷,因为对平均值并不是很关心,**重点是无法体现薪资幅度**。因此这里我会尽可能的把数据展现给你们看 170 | 171 | (正文)从图表来看,Golang 当前的薪酬水平还是很不错的,市场能根据不同阶段(水平)的人给出一个好的价位 172 | 173 | (题外话)看完之后希望你能知道以下内容 174 | - 你当前工作年限的最高、最低薪资范畴 175 | - 你的下一阶段的薪资范畴 176 | - 为什么有的人高,有的人低 177 | - 在大头部队还是小头,为什么 178 | - 不要满足于平均值 179 | 180 | ### 九、融资阶段 181 | 182 | ![image](https://i.imgur.com/H3R9TvZ.png) 183 | 184 | 选用 Golang 的公司大多数都较为稳定,有一部分比较刺激 :) 185 | 186 | #### 融资阶段与薪资范畴对比 187 | 188 | ##### 不需要融资 189 | 190 | ![image](https://i.imgur.com/FoiRau8.png) 191 | 192 | ##### 上市公司 193 | 194 | ![image](https://i.imgur.com/ok22AsZ.png) 195 | 196 | ##### A轮 197 | 198 | ![image](https://i.imgur.com/wA4YFmR.png) 199 | 200 | ##### B轮 201 | 202 | ![image](https://i.imgur.com/kng4LTf.png) 203 | 204 | ##### C轮 205 | 206 | ![image](https://i.imgur.com/pYvpJuH.png) 207 | 208 | ##### D轮以上 209 | 210 | ![image](https://i.imgur.com/oKEDxif.png) 211 | 212 | ### 十、附近的地铁 213 | 214 | Golang 工程师都驻扎在什么地铁站附近呢 215 | 216 | 经常在地铁上看到同行在看代码,来了解一下都分布在哪 :) 217 | 218 | #### 北京 219 | 220 | ![image](https://i.imgur.com/Bjzx8Nf.png) 221 | 222 | #### 上海 223 | 224 | ![image](https://i.imgur.com/CYHtWSi.png) 225 | 226 | #### 广州 227 | 228 | ![image](https://i.imgur.com/pFuFMNy.png) 229 | 230 | #### 深圳 231 | 232 | ![image](https://i.imgur.com/nPAoVil.png) 233 | 234 | #### 杭州 235 | 236 | ![image](https://i.imgur.com/7dGFfns.png) 237 | 238 | #### 成都 239 | 240 | ![image](https://i.imgur.com/DvsKJuu.png) 241 | 242 | 243 | ## 结论 244 | 245 | 如同官方所说 "Go has been on an amazing journey over the last 8+ years",作为一门新生语言,一直在不断地发展,**缺点肯定是有的,你要去识别它** 246 | 247 | ### 从数量来看 248 | 249 | 单从这个招聘网站上来看,数量方面,与大热语言的招聘职位数量仍然有一定的差距,但 Golang 存在许多内部转语言开发的情况,当前展现出来的数据,**招聘数量不多,但质量不错** 250 | 251 | ### 从分布图来看 252 | 253 | 一线城市基本都有 Golang 的职位,虽然其他城市较少,但对于新语言来说是需要持续关注的过程,不能一刀切 254 | 255 | ### 从职称级别来看 256 | 257 | Golang 中高、高级、资深仍然是占大头,给的薪资也基本符合市场行情 258 | 259 | ### 从方向来看 260 | 261 | Golang 涉及的行业领域广泛,移动互联网、数据服务、电子商务、金融、企业服务、云计算等都是它的战场之一 262 | 263 | ### 从开源项目来看 264 | 265 | docker、k8s、etcd、consul 都挺稳 266 | 267 | --- 268 | 269 | 总的来说,Golang 处于一个发展的阶段,市场行情也还行、应用场景较广,不过招聘数量不多,你又怎么看呢? 270 | 271 | 最后放上今天新发布的 Logo :) 272 | 273 | ![image](https://i.imgur.com/nWO32Bl.jpg) 274 | 275 | 276 | ## 参考 277 | 278 | - 项目地址:https://github.com/go-crawler/go_jobs -------------------------------------------------------------------------------- /docs/table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `sp_job` ( 2 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 3 | `city` varchar(255) DEFAULT NULL, 4 | `district` varchar(255) DEFAULT '', 5 | `company_short_name` varchar(255) DEFAULT '', 6 | `company_full_name` varchar(255) DEFAULT '', 7 | `company_label_list` varchar(255) DEFAULT '', 8 | `company_size` varchar(255) DEFAULT '', 9 | `industry_field` varchar(255) DEFAULT '', 10 | `industry_lables` varchar(255) DEFAULT '', 11 | `position_name` varchar(255) DEFAULT '', 12 | `position_lables` varchar(255) DEFAULT '', 13 | `position_advantage` varchar(255) DEFAULT '', 14 | `finance_stage` varchar(255) DEFAULT '', 15 | `work_year` varchar(255) DEFAULT '', 16 | `education` varchar(255) DEFAULT '', 17 | `salary` varchar(255) DEFAULT '', 18 | `longitude` varchar(255) DEFAULT '', 19 | `latitude` varchar(255) DEFAULT '', 20 | `linestaion` varchar(255) DEFAULT '', 21 | `create_time` int(10) unsigned DEFAULT '0', 22 | `add_time` int(10) unsigned DEFAULT '0', 23 | PRIMARY KEY (`id`) 24 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 25 | -------------------------------------------------------------------------------- /downloader/job.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | 9 | "fmt" 10 | "github.com/go-crawler/lagou_jobs/fake" 11 | "github.com/go-crawler/lagou_jobs/pkg/uuid" 12 | "net/url" 13 | "strings" 14 | ) 15 | 16 | var ( 17 | jobsApiUrl = "https://www.lagou.com/jobs/positionAjax.json?city=%s&needAddtionalResult=false" 18 | ) 19 | 20 | type ListResult struct { 21 | Code int 22 | Success bool 23 | Msg string 24 | Content Content 25 | } 26 | 27 | type Content struct { 28 | PositionResult PositionResult 29 | PageNo int 30 | PageSize int 31 | } 32 | 33 | type PositionResult struct { 34 | Result []Result 35 | TotalCount int 36 | } 37 | 38 | type Result struct { 39 | City string 40 | BusinessZones []string 41 | CompanyFullName string 42 | CompanyLabelList []string 43 | CompanyShortName string 44 | CompanySize string 45 | CreateTime string 46 | District string 47 | Education string 48 | FinanceStage string 49 | FirstType string 50 | IndustryField string 51 | IndustryLables []string 52 | JobNature string 53 | Latitude string 54 | Longitude string 55 | PositionAdvantage string 56 | PositionId int32 57 | PositionLables []string 58 | PositionName string 59 | Salary string 60 | SecondType string 61 | Stationname string 62 | Subwayline string 63 | Linestaion string 64 | WorkYear string 65 | } 66 | 67 | type jobService struct { 68 | City string 69 | } 70 | 71 | func NewJobService(city string) *jobService { 72 | return &jobService{City: city} 73 | } 74 | 75 | func (l *jobService) GetUrl() string { 76 | req := fmt.Sprintf(jobsApiUrl, l.City) 77 | url, _ := url.Parse(req) 78 | query := url.Query() 79 | url.RawQuery = query.Encode() 80 | 81 | return url.String() 82 | } 83 | 84 | func (l *jobService) GetJobs(pn int, kd string) (*ListResult, error) { 85 | //client := fake.ProxyAuth{License: "", SecretKey: ""}.GetProxyClient() 86 | client := http.Client{} 87 | postReader := strings.NewReader(fmt.Sprintf("first=false&pn=%d&kd=%s", pn, kd)) 88 | req, err := http.NewRequest("POST", l.GetUrl(), postReader) 89 | if err != nil { 90 | log.Printf("http.NewRequest err: %v", err) 91 | } 92 | 93 | //req.Header.Set("Proxy-Switch-Ip", "yes") 94 | 95 | req.Header.Add("Accept", "application/json, text/javascript, */*; q=0.01") 96 | req.Header.Add("Accept-Encoding", "gzip, deflate, br") 97 | req.Header.Add("Accept-Languag", "zh-CN,zh;q=0.9") 98 | req.Header.Add("Connection", "keep-alive") 99 | req.Header.Add("Content-Length", "25") 100 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") 101 | req.Header.Add("Cookie", "_ga=GA1.2.161331334.1522592243; "+ 102 | "user_trace_token=20180401221723-"+uuid.GetUUID()+"; "+ 103 | "LGUID=20180401221723-"+uuid.GetUUID()+"; "+ 104 | "index_location_city=%E6%B7%B1%E5%9C%B3; "+ 105 | "JSESSIONID="+uuid.GetUUID()+"; "+ 106 | "_gid=GA1.2.1140631185.1523090450; "+ 107 | "Hm_lvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1522592243,1523090450; "+ 108 | "TG-TRACK-CODE=index_search; _gat=1; "+ 109 | "LGSID=20180407221340-"+uuid.GetUUID()+"; "+ 110 | "PRE_UTM=; PRE_HOST=; PRE_SITE=https%3A%2F%2Fwww.lagou.com%2F; "+ 111 | "PRE_LAND=https%3A%2F%2Fwww.lagou.com%2Fjobs%2Flist_golang%3FlabelWords%3D%26fromSearch%3Dtrue%26suginput%3D; "+ 112 | "Hm_lpvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1523110425; "+ 113 | "LGRID=20180407221344-"+uuid.GetUUID()+"; "+ 114 | "SEARCH_ID="+uuid.GetUUID()+"") 115 | req.Header.Add("Host", "www.lagou.com") 116 | req.Header.Add("Origin", "https://www.lagou.com") 117 | req.Header.Add("Referer", "https://www.lagou.com/jobs/list_golang?labelWords=&fromSearch=true&suginput=") 118 | req.Header.Add("User-Agent", fake.GetUserAgent()) 119 | 120 | resp, err := client.Do(req) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | body, err := ioutil.ReadAll(resp.Body) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | var results ListResult 131 | err = json.Unmarshal([]byte(body), &results) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return &results, nil 137 | } 138 | -------------------------------------------------------------------------------- /fake/headers.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | var uas = [...]string{ 8 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", 9 | "Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36", 10 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36", 11 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1", 12 | "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3", 13 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24", 14 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3", 15 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", 16 | "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)", 17 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", 18 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11", 19 | "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.103 Safari/537.36", 20 | "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko", 21 | } 22 | 23 | func GetUserAgent() string { 24 | n := rand.Intn(len(uas)) 25 | return uas[n] 26 | } 27 | -------------------------------------------------------------------------------- /fake/ip.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | ) 7 | 8 | const PROXY_SERVER = "" 9 | 10 | type ProxyAuth struct { 11 | License string 12 | SecretKey string 13 | } 14 | 15 | func (p ProxyAuth) GetProxyClient() http.Client { 16 | proxyURL, _ := url.Parse("http://" + p.License + ":" + p.SecretKey + "@" + PROXY_SERVER) 17 | return http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}} 18 | } 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-crawler/lagou_jobs/pipeline" 5 | "github.com/go-crawler/lagou_jobs/spider" 6 | "log" 7 | "sync" 8 | ) 9 | 10 | var ( 11 | kds = []string{ 12 | "golang", 13 | } 14 | citys = []string{ 15 | "北京", 16 | "上海", 17 | "广州", 18 | "深圳", 19 | "杭州", 20 | "成都", 21 | } 22 | 23 | initResults = []spider.InitResult{} 24 | loopResults = []spider.LoopResult{} 25 | jobPipeline = pipeline.NewJobPipeline() 26 | 27 | wg sync.WaitGroup 28 | ) 29 | 30 | func main() { 31 | for _, kd := range kds { 32 | for _, city := range citys { 33 | wg.Add(1) 34 | go func(city string, kd string) { 35 | defer wg.Done() 36 | initResult, err := spider.InitJobs(city, 1, kd) 37 | if err != nil { 38 | log.Fatalln(err) 39 | } 40 | 41 | initResults = append(initResults, initResult...) 42 | loopResults = append(loopResults, spider.LoopJobs()) 43 | }(city, kd) 44 | } 45 | } 46 | 47 | wg.Wait() 48 | 49 | jobPipeline.Push() 50 | 51 | log.Printf("Init Results: %v", initResults) 52 | log.Printf("Loop Results: %v", loopResults) 53 | } 54 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/jinzhu/gorm" 8 | _ "github.com/jinzhu/gorm/dialects/mysql" 9 | ) 10 | 11 | var ( 12 | DB *gorm.DB 13 | 14 | username string = "root" 15 | password string = "rootroot" 16 | dbName string = "spiders" 17 | ) 18 | 19 | func init() { 20 | var err error 21 | DB, err = gorm.Open("mysql", fmt.Sprintf("%s:%s@/%s?charset=utf8&parseTime=True&loc=Local", username, password, dbName)) 22 | if err != nil { 23 | log.Fatalf(" gorm.Open.err: %v", err) 24 | } 25 | 26 | DB.SingularTable(true) 27 | gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string { 28 | return "sp_" + defaultTableName 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pipeline/job.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/go-crawler/lagou_jobs/model" 7 | ) 8 | 9 | var ( 10 | mutex sync.Mutex 11 | 12 | jobs []LgJob 13 | ) 14 | 15 | type LgJob struct { 16 | // 城市名称 17 | City string 18 | // 地区 19 | District string 20 | 21 | // 公司简称 22 | CompanyShortName string 23 | // 公司全称 24 | CompanyFullName string 25 | // 公司标签 26 | CompanyLabelList string 27 | // 公司规模 28 | CompanySize string 29 | // 融资阶段 30 | FinanceStage string 31 | 32 | // 行业领域 33 | IndustryField string 34 | // 行业标签 35 | IndustryLables string 36 | 37 | // 职位名称 38 | PositionName string 39 | // 职位标签 40 | PositionLables string 41 | // 职位诱惑 42 | PositionAdvantage string 43 | // 工作年限 44 | WorkYear string 45 | // 学历要求 46 | Education string 47 | // 薪资范畴 48 | Salary string 49 | 50 | // 经度 51 | Longitude float64 52 | // 纬度 53 | Latitude float64 54 | // 附近的地铁 55 | Linestaion string 56 | 57 | // 发布时间 58 | CreateTime int64 59 | // 新增时间 60 | AddTime int64 61 | } 62 | 63 | func NewJobPipeline() *LgJob { 64 | return &LgJob{} 65 | } 66 | 67 | func (j *LgJob) Append(js []LgJob) { 68 | mutex.Lock() 69 | jobs = append(jobs, js...) 70 | mutex.Unlock() 71 | } 72 | 73 | func (j *LgJob) Get() []LgJob { 74 | return jobs 75 | } 76 | 77 | func (j *LgJob) Push() error { 78 | for _, v := range j.Get() { 79 | if err := model.DB.Create(v).Error; err != nil { 80 | return err 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/convert/job.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/go-crawler/lagou_jobs/downloader" 8 | "github.com/go-crawler/lagou_jobs/pipeline" 9 | "time" 10 | ) 11 | 12 | func ToPipelineJobs(dJobs []downloader.Result) []pipeline.LgJob { 13 | var pJobs []pipeline.LgJob 14 | for _, v := range dJobs { 15 | longitude, _ := strconv.ParseFloat(v.Longitude, 64) 16 | latitude, _ := strconv.ParseFloat(v.Latitude, 64) 17 | pJobs = append(pJobs, pipeline.LgJob{ 18 | City: v.City, 19 | District: v.District, 20 | 21 | CompanyShortName: v.CompanyShortName, 22 | CompanyFullName: v.CompanyFullName, 23 | CompanyLabelList: strings.Join(v.CompanyLabelList, ","), 24 | CompanySize: v.CompanySize, 25 | FinanceStage: v.FinanceStage, 26 | 27 | PositionName: v.PositionName, 28 | PositionLables: strings.Join(v.PositionLables, ","), 29 | PositionAdvantage: v.PositionAdvantage, 30 | WorkYear: v.WorkYear, 31 | Education: v.Education, 32 | Salary: v.Salary, 33 | 34 | IndustryField: v.IndustryField, 35 | IndustryLables: strings.Join(v.IndustryLables, ","), 36 | 37 | Longitude: longitude, 38 | Latitude: latitude, 39 | Linestaion: v.Linestaion, 40 | 41 | CreateTime: MustDateToUnix(v.CreateTime), 42 | AddTime: time.Now().Unix(), 43 | }) 44 | } 45 | 46 | return pJobs 47 | } 48 | -------------------------------------------------------------------------------- /pkg/convert/time.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/now" 7 | ) 8 | 9 | func MustDateToUnix(date string) int64 { 10 | if date == `` || date == `0000-00-00` { 11 | return 0 12 | } 13 | 14 | loc, _ := time.LoadLocation("Asia/Chongqing") 15 | t, err := now.ParseInLocation(loc, date) 16 | if err != nil { 17 | return 0 18 | } 19 | 20 | return t.Unix() 21 | } 22 | -------------------------------------------------------------------------------- /pkg/page/page.go: -------------------------------------------------------------------------------- 1 | package page 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func CalculateTotalPage(totalCount, pageSize float64) int { 8 | totalPage := float64(totalCount) / float64(pageSize) 9 | 10 | return int(math.Ceil(totalPage)) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/uuid/uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "github.com/satori/go.uuid" 5 | ) 6 | 7 | func GetUUID() string { 8 | uid := uuid.Must(uuid.NewV4()) 9 | return uid.String() 10 | } 11 | -------------------------------------------------------------------------------- /scheduler/job.go: -------------------------------------------------------------------------------- 1 | package scheduler 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var ( 8 | mutex sync.Mutex 9 | JobParams = []JobParam{} 10 | ) 11 | 12 | type JobParam struct { 13 | City string 14 | Pn int 15 | Kd string 16 | } 17 | 18 | func NewJobScheduler() *JobParam { 19 | return &JobParam{} 20 | } 21 | 22 | func (j *JobParam) Pop() *JobParam { 23 | mutex.Lock() 24 | length := len(JobParams) 25 | if length < 1 { 26 | mutex.Unlock() 27 | return nil 28 | } 29 | 30 | job := JobParams[length-1] 31 | JobParams = JobParams[:length-1] 32 | 33 | mutex.Unlock() 34 | return &job 35 | } 36 | 37 | func (j *JobParam) Append(city string, pn int, kd string) { 38 | mutex.Lock() 39 | JobParams = append(JobParams, JobParam{ 40 | City: city, 41 | Pn: pn, 42 | Kd: kd, 43 | }) 44 | 45 | mutex.Unlock() 46 | } 47 | 48 | func (j *JobParam) Count() int { 49 | return len(JobParams) 50 | } 51 | -------------------------------------------------------------------------------- /spider/job.go: -------------------------------------------------------------------------------- 1 | package spider 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/go-crawler/lagou_jobs/downloader" 10 | "github.com/go-crawler/lagou_jobs/pipeline" 11 | "github.com/go-crawler/lagou_jobs/pkg/convert" 12 | "github.com/go-crawler/lagou_jobs/pkg/page" 13 | "github.com/go-crawler/lagou_jobs/scheduler" 14 | ) 15 | 16 | var ( 17 | delayTime = time.Tick(time.Millisecond * 500) 18 | 19 | jobScheduler = scheduler.NewJobScheduler() 20 | jobPipeline = pipeline.NewJobPipeline() 21 | ) 22 | 23 | type InitResult struct { 24 | City string 25 | Kd string 26 | TotalPage int 27 | TotalCount int 28 | } 29 | 30 | type LoopResult struct { 31 | Success int 32 | Error int 33 | Empty int 34 | Errors []string 35 | } 36 | 37 | func InitJobs(city string, pn int, kd string) ([]InitResult, error) { 38 | var ( 39 | jobs []downloader.Result 40 | totalPage int 41 | totalCount int 42 | results []InitResult 43 | 44 | err error 45 | ) 46 | 47 | jobs, totalPage, totalCount, err = GetJobs(city, pn, kd) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | results = append(results, InitResult{ 53 | City: city, 54 | Kd: kd, 55 | TotalPage: totalPage, 56 | TotalCount: totalCount, 57 | }) 58 | 59 | for i := 2; i <= totalPage; i++ { 60 | jobScheduler.Append(city, i, kd) 61 | } 62 | 63 | jobPipeline.Append(convert.ToPipelineJobs(jobs)) 64 | 65 | return results, nil 66 | } 67 | 68 | func LoopJobs() LoopResult { 69 | var ( 70 | result LoopResult 71 | output = jobScheduler.Count() 72 | 73 | params = make(chan []downloader.Result) 74 | ) 75 | 76 | for i := 0; i < output; i++ { 77 | <-delayTime 78 | go func() { 79 | if jobParam := jobScheduler.Pop(); jobParam != nil { 80 | jobs, _, _, err := GetJobs(jobParam.City, jobParam.Pn, jobParam.Kd) 81 | if err != nil { 82 | result.Error++ 83 | result.Errors = append(result.Errors, err.Error()) 84 | } else { 85 | params <- jobs 86 | } 87 | } else { 88 | result.Empty++ 89 | } 90 | }() 91 | } 92 | 93 | L: 94 | for { 95 | select { 96 | case p := <-params: 97 | result.Success++ 98 | jobPipeline.Append(convert.ToPipelineJobs(p)) 99 | default: 100 | if (result.Success + result.Error + result.Empty) >= output { 101 | log.Printf("Break...") 102 | break L 103 | } 104 | } 105 | } 106 | 107 | return result 108 | } 109 | 110 | func GetJobs(city string, pn int, kd string) ([]downloader.Result, int, int, error) { 111 | totalPage := 0 112 | jobService := downloader.NewJobService(city) 113 | result, err := jobService.GetJobs(pn, kd) 114 | if err != nil { 115 | return nil, 0, 0, err 116 | } 117 | 118 | log.Printf("GetJobs Code: %d, GetJobs City: %s, Pn: %d, Kd: %s", result.Code, city, pn, kd) 119 | 120 | if result.Code == 0 && result.Success == true { 121 | content := result.Content 122 | if content.PositionResult.TotalCount > 0 && content.PageSize > 0 { 123 | totalPage = page.CalculateTotalPage(float64(content.PositionResult.TotalCount), float64(content.PageSize)) 124 | } 125 | } else { 126 | return nil, 0, 0, errors.New(fmt.Sprintf("GetJobs City: %s, Pn: %d, Kd: %s, Result: %v", city, pn, kd, result)) 127 | } 128 | 129 | return result.Content.PositionResult.Result, totalPage, result.Content.PositionResult.TotalCount, nil 130 | } 131 | --------------------------------------------------------------------------------