nodejs爬取某联数据,为年末准备找工作的成都前端ers送上一份小礼(附成都UI,北京前端)

成都前端数据
成都前端数据

年末准备换工作投简历,却发现招聘网上的搜索功能不是那么好用。心血来潮,爬取某联的数据做一个分析。

需要的小伙伴可直接上
https://github.com/leaon4/recruit-data
for_download文件夹下载所需资源(附成都UI,北京前端)。

如果觉得还不错,请star!!!


nodejs爬取过程

某联的数据真是良心,非常好爬取,用的是GET请求,也未作任何加密,甚至不用登陆

分析请求

分析请求

只需输入“前端"二字,其他条件都不用填,便可获取数据

data.numFound是条目总数,data.results是具体数据

分析HEADER

点击一下”下一页“,分析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妹子

成都UI数据

成都UI数据

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值