爬取拉勾网Java开发工程师招聘信息

爬取拉钩网热门城市,包括北京、上海、广州、深圳、杭州、成都、武汉、南京3600条Java开发工程师招聘信息


写在前面

上一篇文章介绍了如何爬取学校内网的电影信息,这一篇文章来讲一下爬取招聘信息,恰好最近我也需要找实习,所以就来扒一扒那些互联网公司对Java开发工程师这一岗位都有哪些要求,薪资待遇如何。这一次爬取的主要是一些热门城市,例如北上广深等等,数据一共爬了3600条。


分析页面

首先页面分析肯定是玩爬虫的前提条件,如何发现链接、如何抽取有效信息,这些都是需要事先解决的问题,只有找到页面的规律才能进行下一步的操作。以拉勾网为例,其页面的数据并不是由服务端渲染完成后再返回给前端的,而是通过返回json数据,由前端进行渲染展示。所以这就带来了一个问题,之前那种直接抓取页面链接,抽取信息的方法已经无法完成抓取任务了。接下来打开google浏览器的调试工具,我们访问Java相关的招聘页面,并限定工作地点为北京,首先先看Headers部分的信息,其中Response Headers(响应头)和Query String Parameters(查询的字符串参数)这两个部分的信息可不用理会,我们重点来看GeneralRequest HeadersFrom Data这是三个部分,General包含了请求的URL地址以及请求的方式,划线的部分是转码后的中文北京,不同的城市这一部分是不同的。

Request Headers主要是包含一些请求头信息,这一部分变化不大,主要是From Data中会有一些变化,如果是第一页,first的值为true

然后就是数据部分,服务端返回的json数据在Preview中的content可以找到,如下图:


设置站点信息

这里主要是设置请求头信息,其中的键值根据调试工具显示的来进行添加即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 设置站点信息
*/
private Site site = Site.me()
.setRetryTimes(5)
.setSleepTime(2000)
.addHeader("Accept", "application/json, text/javascript, */*; q=0.01")
.addHeader("Accept-Encoding", "gzip, deflate, br")
.addHeader("Accept-Language", "zh-CN,zh;q=0.9")
.addHeader("Connection", "keep-alive")
// .addHeader("Content-Length", "23")
.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.addHeader("Host", "www.lagou.com")
.addHeader("Origin", "https://www.lagou.com")
.addHeader("Referer", "https://www.lagou.com/jobs/list_Java?px=default&city=%E5%B9%BF%E5%B7%9E")
.addHeader("X-Anit-Forge-Code", "0")
.addHeader("X-Anit-Forge-Token", "None")
.addHeader("X-Requested-With", "XMLHttpRequest")
.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)" +
" Chrome/68.0.3440.84 Safari/537.36")
.addHeader("Cookie", "_ga=GA1.2.1280051714.1538720507; _gid=GA1.2.378872501.1538720507" +
"; user_trace_token=20181005142146-efa1cbc6-c866-11e8-bb68-5254005c3644" +
"; LGUID=20181005142146-efa1d0ce-c866-11e8-bb68-5254005c3644" +
"; WEBTJ-ID=20181006141809-1664806af9719e-0740f6b307c19b-182e1503-1049088-1664806af98e1" +
"; JSESSIONID=ABAAABAABEEAAJAAC617D5D8DA4D5C0C8BBE6E7DF7A4EDD" +
"; TG-TRACK-CODE=index_navigation" +
"; Hm_lvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1538744240,1538806690,1538806709,1538807934" +
"; index_location_city=%E5%B9%BF%E5%B7%9E" +
"; LGSID=20181006151619-b8fa5b95-c937-11e8-bb68-5254005c3644" +
"; LGRID=20181006154438-ad322db4-c93b-11e8-bb68-5254005c3644" +
"; Hm_lpvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1538811878" +
"; SEARCH_ID=2559e5cdd0714d7b9138df224db8e95a");

构造链接

由于服务器返回的是json数据,所以我门必须手动构造链接,模拟请求,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public void processHotCity(Page page) {
if (flag) {
Request[] requests = new Request[this.getCity().size() * 30];
Map<String, Object> map = new HashMap<>();
// 外层循环控制城市、里层循环控制链接
for (int m = 0; m < this.getCity().size(); m++) {
for (int n = 0; n < 30; n++) {
requests[(m * 30) + n] = new Request("https://www.lagou.com/jobs/positionAjax.json?px=default&city=" +
this.getCity().get(m) + "&needAddtionalResult=false");
requests[(m * 30) + n].setMethod(HttpConstant.Method.POST);
// 设置表单数据、请求体以及将请求添加到抓取队列中
if (m == 0) {
// 每个城市的第一个访问的URL中的From Data的first值为true
map.put("first", "true");
} else {
map.put("first", "false");
}
map.put("pn", n + 1);
map.put("kd", "java");
requests[(m * 30) + n].setRequestBody(HttpRequestBody.form(map, "UTF-8"));
page.addTargetRequest(requests[(m * 30) + n]);
}
}

}
// 改变标志,不再添加链接
flag = false;
}

public List<String> getCity() {
List<String> citys = new ArrayList<>();
// 北京
citys.add("%E5%8C%97%E4%BA%AC");
// 上海
citys.add("%E4%B8%8A%E6%B5%B7");
// 广州
citys.add("%E5%B9%BF%E5%B7%9E");
// 深圳
citys.add("%E6%B7%B1%E5%9C%B3");
// 杭州
citys.add("%E6%9D%AD%E5%B7%9E");
// 武汉
citys.add("%E6%AD%A6%E6%B1%89");
// 成都
citys.add("%E6%88%90%E9%83%BD");
// 南京
citys.add("%E5%8D%97%E4%BA%AC");
// 西安
//citys.add("%E8%A5%BF%E5%AE%89");
// 厦门
//citys.add("%E5%8E%A6%E9%97%A8");
// 长沙
//citys.add("%E9%95%BF%E6%B2%99");
// 天津
//citys.add("%E5%A4%A9%E6%B4%A5");
// 苏州
//citys.add("%E8%8B%8F%E5%B7%9E");
return citys;
}

封装对象

抽取服务器返回的json数据中自己需要的信息,并封装成对象,保存到数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* 封装数据并保存的方法
* @param page Page对象
*/
public void encapsulation(Page page) {
if (startFlag) {
// 职位名称列表
List<String> positionName = new JsonPathSelector("$.content.positionResult.result[*].positionName")
.selectList(page.getRawText());

// 年薪列表
List<String> salary = new JsonPathSelector("$.content.positionResult.result[*].salary")
.selectList(page.getRawText());

// 工作经验列表
List<String> workYear = new JsonPathSelector("$.content.positionResult.result[*].workYear")
.selectList(page.getRawText());

// 公司全名列表
List<String> companyFullName = new JsonPathSelector("$.content.positionResult.result[*].companyFullName")
.selectList(page.getRawText());

// 学历列表
List<String> degree = new JsonPathSelector("$.content.positionResult.result[*].education")
.selectList(page.getRawText());

// 融资规模列表
List<String> financeStage = new JsonPathSelector("$.content.positionResult.result[*].financeStage")
.selectList(page.getRawText());

// 行业领域列表
List<String> industryField = new JsonPathSelector("$.content.positionResult.result[*].industryField")
.selectList(page.getRawText());

// 职位福利列表
List<String> positionAdvantage = new JsonPathSelector("$.content.positionResult.result[*].positionAdvantage")
.selectList(page.getRawText());

// 城市列表
List<String> city = new JsonPathSelector("$.content.positionResult.result[*].city")
.selectList(page.getRawText());

// 区域列表,例如天河区
List<String> district = new JsonPathSelector("$.content.positionResult.result[*].district")
.selectList(page.getRawText());

// 公司规模列表
List<String> companySize = new JsonPathSelector("$.content.positionResult.result[*].companySize")
.selectList(page.getRawText());

// 封装对象并保存
for (int i = 0; i < positionName.size(); i++) {
Lagou lagou = new Lagou();
lagou.setPositionName(positionName.get(i));
lagou.setSalary(salary.get(i));
lagou.setWorkYear(workYear.get(i));
lagou.setCompanyFullName(companyFullName.get(i));
lagou.setDegree(degree.get(i));
lagou.setFinanceStage(financeStage.get(i));
lagou.setIndustryField(industryField.get(i));
lagou.setPositionAdvantage(positionAdvantage.get(i));
lagou.setCity(city.get(i));
lagou.setDistrict(district.get(i));
lagou.setCompanySize(companySize.get(i));
save(lagou);
}
}
startFlag = true;
}

爬取的结果

一共爬取到3600条数据


数据的可视化

数据的可视化使用了百度的开源JavaScript可视化库echarts,由于在Markdown中直接引入js没有办法渲染,所以这里使用一个插件: hexo-tag-echarts来进行可视化,这个插件有两个版本,一个是hexo-tag-echarts,另一个是hexo-tag-echarts3,前者是原作者的作品,但已经不再维护了,所以我用的是hexo-tag-echarts3,具体的用法可以参考:http://zhoulvjun.github.io/2016/02/07/hexo-tag-echarts/

从公司的融资情况来看,不需要融资的公司提供的岗位数量会多一些,大概占到三分之一,其余融资阶段,包括未融资的公司提供的岗位数量差别并不算太大。


从学历与职位数量的关系图来看,在这3600个招聘岗位中,要求本科学历的岗位数量达到了2795个,占了大部分,看来本科学历在企业招聘中还是有一定优势的。


从上面工作经验与岗位数量的关系图来看,似乎大部分的公司提供的职位,都需要求职者有3-5年工作经验,这个比例占到了57.25%,而1-3年工作经验以及5-10年工作经验的比例加起来也达到了36.78%,那么对于像我这样的应届毕业生来说,就只能躲在角落里瑟瑟发抖吗?那倒也未必,毕竟这只是像北上广深等一些热门城市的招聘岗位,而且仅限于拉勾网,数据样本的数量也有限,只有3600条数据,所以数据还是欠缺说服力(自我安慰joy)。看来应届毕业生在拉钩找工作不太合适,合适的岗位数量太少。


Reference Paper

Deqing Li, Honghui Mei, Yi Shen, Shuang Su, Wenli Zhang, Junting Wang, Ming Zu, Wei Chen. ECharts: A Declarative Framework for Rapid Construction of Wed-based Visualization. Visual Informatics, 2018.

结束语

这里有一点要注意的就是,以上爬取的城市,其职位列表都是30页的,所以我注释了一部分分页不足30页的城市,如果要构造这些城市的职位列表链接,有点麻烦,暂时还没想到比较好的办法,而且以上构造链接的代码用到了两个for循环,显然性能上会有一些问题,如果各位看官有什么比较好见解,欢迎留言告诉我,共同交流!


如果您觉得我的文章对您有帮助,请随意赞赏,您的支持将鼓励我继续创作!
0%