nodejs爬取某联数据,为年末准备找工作的成都前端ers送上一份小礼(附成都UI,北京前端)
年末准备换工作投简历,却发现招聘网上的搜索功能不是那么好用。心血来潮,爬取某联的数据做一个分析。
需要的小伙伴可直接上
https://github.com/leaon4/recruit-data
for_download文件夹下载所需资源(附成都UI,北京前端)。
如果觉得还不错,请star!!!
nodejs爬取过程
某联的数据真是良心,非常好爬取,用的是GET请求,也未作任何加密,甚至不用登陆
分析请求
只需输入“前端"二字,其他条件都不用填,便可获取数据
data.numFound是条目总数,data.results是具体数据
分析HEADER
点击一下”下一页“,分析header
由于是GET请求,非常简单,只需将headers全部复制进代码中就行
只需注意url中,pageSize指条目数,即limit,最大为100,start即是offset
爬取试探
先引入https模块
const https = require('https');
写个option。option的写法很简单,直接将上图的字段复制进去就行
const options = {
hostname: 'fe-api.zhaopin.com',
path: `/c/i/sou?start=0&pageSize=100&cityId=801&workExperience=-1&education=-1&companyType=-1&employmentType=-1&jobWelfareTag=-1&kw=%E5%89%8D%E7%AB%AF&kt=3&_v=0.94922515&x-zp-page-request-id=cfca3a09c69d457395df0ae072a48404-1543657761205-287220`,
port: 443,
method: 'GET',
headers: {
Accept: 'application/json, text/plain, */*',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36',
Host: 'fe-api.zhaopin.com',
Origin: 'https://sou.zhaopin.com',
Cookie: 'sts_deviceid=167559a12ee6d-0206f0b0dd604b-3f674604-1327104-167559a12f019f; jobRiskWarning=true; ZP_OLD_FLAG=false; LastCity=%E6%88%90%E9%83%BD; LastCity%5Fid=801; sts_sg=1; sts_sid=167559a16cb67e-006c5655e76b99-3f674604-1327104-167559a16cc524; sts_chnlsid=Unknown; zp_src_url=https%3A%2F%2Fwww.zhaopin.com%2F; GUID=373c679c84bf466dbe1cade6b1bf9be1; sajssdk_2015_cross_new_user=1; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%22167559a1721a58-0c14d9b5792b16-3f674604-1327104-167559a172214a%22%2C%22%24device_id%22%3A%22167559a1721a58-0c14d9b5792b16-3f674604-1327104-167559a172214a%22%2C%22props%22%3A%7B%7D%7D; Hm_lvt_38ba284938d5eddca645bb5e02a02006=1543329421; Hm_lpvt_38ba284938d5eddca645bb5e02a02006=1543329421; ZL_REPORT_GLOBAL={%22sou%22:{%22actionid%22:%228920c23d-e10d-4039-beee-84a21fc14432-sou%22%2C%22funczone%22:%22smart_matching%22}}; sts_evtseq=7',
}
};
创建一个request对象,传入options,并在回调函数中注册’data’和’end’事件,接收数据
const request = https.request(options, res => {
let arr = [];
res.on('data', chunk => {
arr.push(chunk);
});
res.on('end', err => {
if (err) throw err;
let data = Buffer.concat(arr).toString();
console.log(data);
});
});
发送请求
request.end();
爬取成功!!
就是这么简单。那现在让我们开始正式的项目吧
创建项目
创建一个项目,结构如图所示。只需引入一个包:
npm init
npm install mysql --save
这里使用了mysql数据库。其实数据量很小,不用mysql,用纯js都可以。既然是练习项目,就一块儿练习了。
先写一个file.js模块做为准备
只为了读写文件用
const fs = require('fs');
module.exports={
readFile,
writeFile
};
async function readFile(fileName) {
return new Promise((resolve, reject) => {
fs.readFile(fileName, 'utf8', (err, file) => {
if (err) reject(err);
resolve(file);
});
});
}
async function writeFile(fileName, data) {
return new Promise((resolve, reject) => {
fs.writeFile(fileName, data, err => {
if (err) reject(err);
resolve();
});
});
}
数据库准备
新建一个库,并建立一个名为"compony"的表
CREATE TABLE compony (
id INT AUTO_INCREMENT,
name VARCHAR(150) DEFAULT NULL,
size VARCHAR(30) DEFAULT NULL,
url VARCHAR(255) DEFAULT NULL,
position_url VARCHAR(255) DEFAULT NULL,
working_exp VARCHAR(10) DEFAULT NULL,
salary_min FLOAT DEFAULT 0,
salary_max FLOAT DEFAULT 0,
job_name VARCHAR(100) DEFAULT NULL,
lat VARCHAR(15) DEFAULT NULL,
lon VARCHAR(15) DEFAULT NULL,
city VARCHAR(10) DEFAULT NULL,
area VARCHAR(10) DEFAULT NULL,
`update` DATETIME DEFAULT NULL,
PRIMARY KEY (id)
);
编写一个数据库配置json文件,写入你的数据库名和密码
{
"connectionLimit":5,// 连接池最大连接数
"host":"localhost",
"user":"root",
"password":"yourPassword",
"database":"yourDatabase"
}
编写db.js,导出三个方法,以便我们以后使用
const mysql = require('mysql');// 引入mysql模块
const fs = require('fs');
const path = require('path');
// 导出方法
module.exports = {
insert,
select,
query
};
// 获取数据库配置文件
let option = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'dbOption.json')));
// 创建连接池
const pool = mysql.createPool(option);
/**
* 封装一条插入多条数据的方法
* @param {Array} arr |待写入数据
* @param {String} tableName |数据表名
* @return {Promise} |mysql执行结果
*/
async function insert(arr, tableName) {
// values,这一整段都是拼接sql语句的方法
let values = arr.map(item => {
return JSON.stringify(item).replace(/^ ?\[/, '(').replace(/ ?\]$/, ')');
});
values = values.join(',');
let sql = `
INSERT INTO ${tableName}(name,size,url,position_url,working_exp,salary_min,salary_max,job_name,lat,lon,city,area,\`update\`)
VALUES${values};
`;
return new Promise((resolve, reject) => {
// 将sql语句传入
pool.query(sql, (err, results, fields) => {
if (err) reject(err);
// 将mysql执行成功结果返回
resolve(results);
});
});
}
/**
* 简单封装一条不带其他子句的查询方法
*/
async function select(column = '*', tableName) {
let sql = `
SELECT ${column} from ${tableName};
`;
return new Promise((resolve, reject) => {
pool.query(sql, (err, results, fields) => {
if (err) reject(err);
resolve(results);
});
});
}
/**
* 简单封装一条只接收完整sql语句的方法
*/
async function query(sql) {
return new Promise((resolve, reject) => {
pool.query(sql, (err, results, fields) => {
if (err) reject(err);
resolve(results);
});
});
}
编写spider.js,将刚才的爬取过程封装
const https = require('https');
module.exports = {
fetch
};
const options = {
hostname: 'fe-api.zhaopin.com',
path: '',
port: 443,
method: 'GET',
headers: {
Accept: 'application/json, text/plain, */*',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36',
Host: 'fe-api.zhaopin.com',
Origin: 'https://sou.zhaopin.com',
Cookie: 'sts_deviceid=167559a12ee6d-0206f0b0dd604b-3f674604-1327104-167559a12f019f; jobRiskWarning=true; ZP_OLD_FLAG=false; LastCity=%E6%88%90%E9%83%BD; LastCity%5Fid=801; sts_sg=1; sts_sid=167559a16cb67e-006c5655e76b99-3f674604-1327104-167559a16cc524; sts_chnlsid=Unknown; zp_src_url=https%3A%2F%2Fwww.zhaopin.com%2F; GUID=373c679c84bf466dbe1cade6b1bf9be1; sajssdk_2015_cross_new_user=1; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%22167559a1721a58-0c14d9b5792b16-3f674604-1327104-167559a172214a%22%2C%22%24device_id%22%3A%22167559a1721a58-0c14d9b5792b16-3f674604-1327104-167559a172214a%22%2C%22props%22%3A%7B%7D%7D; Hm_lvt_38ba284938d5eddca645bb5e02a02006=1543329421; Hm_lpvt_38ba284938d5eddca645bb5e02a02006=1543329421; ZL_REPORT_GLOBAL={%22sou%22:{%22actionid%22:%228920c23d-e10d-4039-beee-84a21fc14432-sou%22%2C%22funczone%22:%22smart_matching%22}}; sts_evtseq=7',
}
};
/**
* 根据start的值,将获取的数据返回
*/
async function fetch(start = 0) {
setOptionPath(start);
let data = await request(options);
return data;
}
/**
* 根据start动态修改options.path
*/
function setOptionPath(start){
options.path=`/c/i/sou?start=${start}&pageSize=100&cityId=801&workExperience=-1&education=-1&companyType=-1&employmentType=-1&jobWelfareTag=-1&kw=%E5%89%8D%E7%AB%AF&kt=3&_v=0.94922515&x-zp-page-request-id=cfca3a09c69d457395df0ae072a48404-1543657761205-287220`;
}
/**
* 将异步爬取请求封装成promise
*/
async function request(options) {
return new Promise((resolve, reject) => {
const req = https.request(options, res => {
let arr = [];
res.on('data', chunk => {
arr.push(chunk);
});
res.on('end', err => {
if (err) reject(err);
let data = Buffer.concat(arr).toString();
resolve(data);
});
});
// 多注册一个eeror事件
req.on('error', e => {
console.error(e);
reject(e);
});
req.end();
});
}
先分析将要爬取的字段,将我们感兴趣的字段提出
以下是我们将要提取的字段
{
"company": {
"url": "",
"name": "",
"size": {
"name": "1000-9999人"
},
},
"positionURL": "",
"workingExp": {
"name": "1-3年"
},
"salary": "4K-6K",
"jobName": "web前端开发工程师",
"geo": {
"lat": "30.528291",
"lon": "103.987321"
},
"city": {
"items": [{
"name": "成都"
}, {
"name": "高新区"
}],
},
"updateDate": "2018-11-26 15:07:04",
}
为此编写一个format函数
其中,item.salary原本是’10K-15K’的格式,为了便于计算,分别存为salary_min,salary_max
为方便存入数据库,全部采取数组的形式,使数据扁平化
注意数据的顺序,需和刚才创建的数据库字段一一对应
function format(results) {
return results.map(item => {
// item.salary是'10K-15K'的格式
let salarys = item.salary.split('-');
return [
item.company.name,
item.company.size.name,
item.company.url,
item.positionURL,
item.workingExp.name,
parseFloat(salarys[0]),
parseFloat(salarys[1]),
item.jobName,
item.geo.lat,
item.geo.lon,
item.city.items[0].name,
item.city.items[1] ? item.city.items[1].name : '',
item.updateDate
]
});
}
编写index.js,作为入口文件
引入刚才写好的模块
const db = require('./work_module/db');
const spider = require('./work_module/spider');
const file = require('./work_module/file');
定义两个全局变量,方便随时修改
start值做为爬取的起始数。如果中途出现错误而中断,可以修改start值继续爬取
const tableName = 'compony';// 数据库表名
let start = 0;// 爬取的url之start值
编写一个启动函数
用try,catch处理错误
如果出错,因为错误信息非常多,控制台查看非常不方便
记录进文件中,方便找原因
作为一枚有底线的爬虫,当然要设置一点间隔时间。。。
async function run() {
let data, json, dbResult;
try {
// 爬取数据
data = await spider.fetch(start);
json = JSON.parse(data);
// 格式化数据
let arr = format(json.data.results);
// 写入数据库
dbResult = await db.insert(arr, tableName);
} catch (e) {
// 如果出现错误,将错误写入文件
console.error(e);
await file.writeFile('error.txt', e.stack);
let log = json.data.results;
await file.writeFile('error.json', JSON.stringify(log));
return;
}
// 成功后start自增100
start += 100;
// 控制台输出成功信息
console.log(dbResult);
// 输出start值,用于记录。如果出错则可根据此修改初始start值
console.log(start);
if (start <= json.data.numFound) {
// 作为一枚有底线的爬虫,当然要设置一点间隔时间
setTimeout(run, 1000);
} else {
// 爬取完毕
console.log('done!');
process.exit();
}
}
接下来就启动吧
run().catch(e => {
console.error(e);
});
爬取开始
node index
爬取成功
到数据库里看一看吧
select name,job_name from compony;
可以看到,虽然爬取成功,但这个数据非常不靠谱,需要做一次数据清洗
数据备份
清洗前先做一次备份,以免误操作数据库而将数据丢失
sql语句关键字用大写是好习惯。不过sublime也有代码提示,我就懒得用大写了,别向我学习——捂脸
create table compony_bak like compony;
insert into compony_bak select * from compony;
数据清洗
先来一次大清洗!
记住每次清洗前,先select出来看看,以免误伤
select job_name,position_url from compony
where job_name not regexp '(前端|web|h5|html5|script|js|微信小程序)';
mysql说,有如此多的垃圾数据!
1393 rows in set (0.07 sec)
不放心的话,可以复制position_url,打开链接去检查是否确实该删掉
确认可以删掉后,执行delete
按这个方法,逐次检查,删除垃圾数据
以下是可供参考的删除语句
delete from compony
where job_name not regexp '(前端|web|h5|html5|script|js|微信小程序)';
delete from compony
where name regexp '(达内|培训)';
delete from compony
where job_name regexp '(学徒|实习|助理|实训)';
delete from compony
where job_name regexp 'webgis';
/* 这句误伤有点大 */
delete from compony
where job_name regexp 'web' and job_name not regexp '前';
/****************/
delete from compony
where job_name regexp '(美|设计|ui)';
delete from compony
where job_name regexp '(java|php|python)' and job_name not regexp 'script';
delete from compony
where job_name regexp '(ue4|unity|u3d|c#|游戏|专员|基础|产品经理|mcu|ic|gis|ios|andriod|帐|账|催|转行|应届|客)' and job_name not regexp 'node';
delete from compony
where `update` < '2018-9-1';
数据清洗完毕
将数据导出成数据文件
为配合我写的echarts图,直接将数据导出成.js文件
项目不大,所以直接将数据定义成全局变量
html直接用相对路径引用,这样直接就可以用浏览器打开,方便查看
编写createDataFile.js
const db = require('./work_module/db');
const file = require('./work_module/file');
// 这里写入数据表名
const tableName = 'compony';
run().catch(e => {
console.error(e);
});
async function run() {
let sql = `
select * from ${tableName}
order by salary_max,salary_min,working_exp;
`;
// 获取数据库数据
let results = await db.query(sql);
// 封装进一个全局变量
let zhilianData = {
name: '成都前端薪资分布图',// 图表的名称
dataTime: '2018-12-01',// 图表的数据时间
cityGeo: [104.07, 30.62],// 该城市的经纬度
initZoom: 12,// 地图的初始缩放值
datas: results
};
// 生成dist/zhilianData.js
await file.writeFile('dist/zhilianData.js', 'const zhilianData=' + JSON.stringify(zhilianData));
console.log('done!');
process.exit();
}
生成的数据文件在dist文件夹中
数据展示
将生成的文件放入/data_view/assets文件夹中
/data_view/index.html即是地图散点图
/data_view/statistic.html即是数据统计图
https://github.com/leaon4/recruit-data
觉得不错的话,请给个star!!!
文末彩蛋
我大帝都前端数据!
和大帝都前端比起来,成都真是十八线去了。。。。。。
再附一份成都UI数据,送给不会写代码的可爱的成都UI妹子