为了账号安全,请及时绑定邮箱和手机立即绑定

从零开始写一个node爬虫(上)—— 数据采集篇

标签:
Node.js 爬虫

  爬虫相信大家都知道,这里我们从一个空的文件夹开始,也来写一写我们自己的爬虫程序吧。

  github入口
  下一篇——数据分析篇入口
  爬虫毕竟涉及到数据的爬取,所以其实有一个道德的约束,那就是Robots协议,也就是爬虫协议,爬虫程序在爬取网站数据之前,会先看看是否存在robots.txt文件,假如有,会在这个文件允许的范围内进行爬取。像著名的百度,谷歌等搜索引擎,都是遵循这一道德规约的。
  好了,闲话少说,开始我们的编程之旅吧。

  准备工作:

  ①、node环境
  ②、一个空文件夹
  准备工作是不是很简单?接下来,你需要能被爬取到数据的数据源。也就是一串URL,这里我选择对前端开发这个岗位做一次数据采集与分析。选择的对象呢——是猎聘网,希望别打我 = =
  我们创建好文件夹之后,先去猎聘网,搜索一下前端开发,看看它们页面的数据是怎样的。
图片描述
  大概每页40条,很多很多页。。。除了前端开发这个关键字以外其他一律选择不限。

  这时候打开NetWork面板,在它的分页上随便点一页,看看此时新出来这40条数据是怎么来的。

  此时你可以发现有一个数据包,将整个页面html返回给你。
图片描述
  而这样的一个页面就是我们的数据源。找到这个数据包的同时,别忘了把它的URL拷贝出来。
  接下来第一步,将这一页的40条招聘信息数据爬出来。
  先在空文件夹下创一个app.js。里面写上一句‘hello world’,并在终端(vscode或webstorm的terminal或者直接打开cmd,git bash啥的都可以),运行最熟悉的一句命令——node app.js。如下:
图片描述  这样我们的node程序可以正常运行了。
  接下来,将我们的这串URL拷贝进app.js,再引入一下https这个模块(因为刚好我们要爬取的网页是带数字证书的)。
图片描述
  代码如下:

// console.log('你好啊!my name is dorsey');

const https = require('https');

let url = 'https://www.liepin.com/zhaopin/?init=-1&headckid=0417b67c8d823dcb&fromSearchBtn=2&sfrom=click-pc_homepage-centre_searchbox-search_new&ckid=0417b67c8d823dcb&degradeFlag=0&key=%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91&siTag=D_7XS8J-xxxQY6y2bMqEWQ%7EfA9rXquZc5IkJpXC-Ycixw&d_sfrom=search_fp&d_ckId=466b672969a37b2deaf20975f4b05e7c&d_curPage=0&d_pageSize=40&d_headId=466b672969a37b2deaf20975f4b05e7c&curPage=1';

https.get(url, function (res) {

    res.on('data', function (chunk) {
        console.log(chunk);
    });

    res.on('end', function () {
        console.log('数据包传输完毕');
    });
})

  代码很简单,通过https模块去get我们这个URL的链接,从这个链接不断下载一个个的字节流数据过来,等到数据流传输完了,监听end,就可以把这些字节码打包成一个完整的数据包,将字节码转成字符,就是我们所需要的数据。
  我们还是通过node app.js去运行我们的程序。
图片描述
  可以看到一个个的包。我们将这些流缓冲数据合并成一个包,并转化成我们看得懂的字符串。此时代码需要做一些小改造。变成这样:

// console.log('你好啊!my name is dorsey');

const https = require('https');

let url = 'https://www.liepin.com/zhaopin/?init=-1&headckid=0417b67c8d823dcb&fromSearchBtn=2&sfrom=click-pc_homepage-centre_searchbox-search_new&ckid=0417b67c8d823dcb&degradeFlag=0&key=%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91&siTag=D_7XS8J-xxxQY6y2bMqEWQ%7EfA9rXquZc5IkJpXC-Ycixw&d_sfrom=search_fp&d_ckId=466b672969a37b2deaf20975f4b05e7c&d_curPage=0&d_pageSize=40&d_headId=466b672969a37b2deaf20975f4b05e7c&curPage=1';

https.get(url, function (res) {
    let chunks = [],
        size = 0;
    res.on('data', function (chunk) {
        chunks.push(chunk);
        size += chunk.length;
    });

    res.on('end', function () {
        console.log('数据包传输完毕');
        let data = Buffer.concat(chunks, size);
        console.log(data);
        let html = data.toString();
        console.log(html);
    });
})

  再node app.js运行一下,此时可以看到控制台打印的就是我们之前在NetWork那里看到的那个页面,里面有我们需要的40条数据。
图片描述
  这时候得回到页面,看看存放了招聘信息的那些关键数据放在哪个标签下面。可以看到每一条的关键信息都存在一个类名为job-info的div里,里面包含职位信息condition,招聘公司信息company-info。此时需要将这里的数据提取出来。
图片描述
  那如何提取出来?此时看到这个你可能想,要是有个JQuery就好了,通过选择器来选到对应的信息元素,但注意,这不是页面,这是node,那怎么办?
  要相信,假如我们还是处于小白及进阶阶段,你遇到过的问题一定也是很多人也会遇到的问题,也往往能搜到相应的解决方案。这里就有一个很好很实用的package包,那就是cheerio。它相当于node环境中的JQuery,用来爬虫,爬取网页特定信息,最适合不过了。
  首先当然是安装一下。
图片描述   接下来,边对照着猎聘网的对应位置,通过写JQuery代码的方式,来一步步选择到目标数据,JQuery选择器如何选到对应DOM元素的,相信你们都会了,代码的改造如下。

// console.log('你好啊!my name is dorsey');

const https = require('https');
const cheerio = require('cheerio');

let url = 'https://www.liepin.com/zhaopin/?init=-1&headckid=0417b67c8d823dcb&fromSearchBtn=2&sfrom=click-pc_homepage-centre_searchbox-search_new&ckid=0417b67c8d823dcb&degradeFlag=0&key=%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91&siTag=D_7XS8J-xxxQY6y2bMqEWQ%7EfA9rXquZc5IkJpXC-Ycixw&d_sfrom=search_fp&d_ckId=466b672969a37b2deaf20975f4b05e7c&d_curPage=0&d_pageSize=40&d_headId=466b672969a37b2deaf20975f4b05e7c&curPage=1';

https.get(url, function (res) {
    let chunks = [],
        size = 0;
    res.on('data', function (chunk) {
        chunks.push(chunk);
        size += chunk.length;
    });

    res.on('end', function () {
        console.log('数据包传输完毕');
        let data = Buffer.concat(chunks, size);
        let html = data.toString();


        let $ = cheerio.load(html);

        let result = [];
        
        $('.sojob-list').find('.job-info').each(i => {
            let map = {};
            //  个人基本信息
            map.name = $('.job-info').eq(i).find('h3').attr('title');

            let baseOthersInfo = $('.job-info').eq(i).find('.condition').attr('title');
            baseOthersInfo = baseOthersInfo.split("_");

            map.reward = baseOthersInfo[0];
            map.area = baseOthersInfo[1];
            map.experience = baseOthersInfo[2];

            //  公司信息
            let companyTagDom = $('.company-info').eq(i).find('.temptation').find('span');
            let companyTag = [];
            companyTagDom.each(i => {
                companyTag.push(companyTagDom.eq(i).text());
            });
            let companyInfo = {
                name: $('.company-info').eq(i).find('.company-name a').attr('title'),
                href: $('.company-info').eq(i).find('.company-name a').attr('href'),
                type: $('.company-info').eq(i).find('.industry-link a').text(),
                tag: companyTag.join(',')
            }
            map.company = companyInfo;
            result.push(map);
            map = {};
        });
        console.log(result);
    });
});

  运行下app.js,(node app.js)再看下控制台:
图片描述
  可以看到我们需要的目标数据已经被load出来了。
  此时你可能有疑惑了,写了这么一大段代码,总是去console.log打印日志来排错,费时又费力,能不能跟写前端页面代码一样,在浏览器上直接打断点。
  答案是肯定的,此时,我们还是运行app.js,只不过命令稍加变动,变成:

node --inspect-brk app.js

  终端输入命令之后运行,并打开浏览器的:

chrome://inspect/#devices

  此时,你可以看到Target目标调试对象:
图片描述
  这样就跟在浏览器打断没有区别了。是不是很cool?
  好了扯远了,继续我们的node编程。
  我们刚刚拿到的是一个数据包,40条数据,也就是一个分页的数据,可是要拿多个分页怎么办?能否再次合并成一个数据包或者数据文件?答案是肯定的。别忘了,我们现在的舞台不是浏览器,而在于更为广阔的系统平台本身或者更准确的一点是一台具备完整系统功能的JVM上,所以读写文件,甚至读写数据库这个利器才是我们的根本。
简单就不去搞一个mongoDB了,直接写进一个txt文件。这时候代码需要补充一下文件模块依赖 fs 以及文件写入的一部分代码,如下:。

const fs = require('fs');
// ...
fs.writeFile('./cache/jobs.txt', JSON.stringify(result), { 'flag': 'a' }, function(err) {
    if(err) throw err;
    console.log('写入成功');
});

  这样写虽然可以,但有一个问题,因为我们写入txt文件的时候,每个数据包,也就是result都是一个数组,这样直接转成字符串增量写入txt会出现一个问题。看看哈。
图片描述
  也就是说,这时候虽然写进去没问题,但是最终读出来却不是JSON格式的数据,所以在每个数据包写入的时候,我们得做一点点字符串替换,就比如将:

][   替换成    ,

  所以代码需要改造成这样,比如说我们这里暂且设置是10个数据包,后期改造参数传递就好了:

let dataStr = JSON.stringify(result).trim().replace(/^\[/, curPage == 1 ? '[' : '').replace(/\]$/, curPage == 10 ? ']' : ',');
fs.writeFile('./cache/jobs.txt', dataStr, { 'flag': 'a' }, function(err) {
    if(err) throw err;
    console.log('写入成功');
});

  但这时候URL没变,你拿到的数据包虽然有10个,但都是一样的数据包,这显然不是我们想要的,所以又得回到猎聘网,看看他们分页改变时是哪些关键参数改变了。仔细观察可以发现URL参数变动的是这两个。
图片描述   所以我们将这两个参数抽出来,模拟一下URL入参传参,再次进行爬虫,代码需要做改造。由于这部分数据采集尽管有内部分工,但完成的目标是一致的,所以我们可以定义一个class,来将整个的过程封装起来。如下:

const 
    https = require('https'),
    fs = require('fs'),
    cheerio = require('cheerio');

class crawlData {

    constructor ( page ) {

        this.currentPage = 1;
        this.page = page;

        this.baseUrl = 'https://www.liepin.com/zhaopin/?isAnalysis=&dqs=&pubTime=&salary=&subIndustry=&industryType=&compscale=&key=%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91&init=-1&searchType=1&headckid=b41b3a1f788e456c&compkind=&fromSearchBtn=2&sortFlag=15&ckid=e0769e995864e9e1&degradeFlag=0&jobKind=&industries=&clean_condition=&siTag=D_7XS8J-xxxQY6y2bMqEWQ%7EfA9rXquZc5IkJpXC-Ycixw&d_sfrom=search_prime&d_ckId=ec6119ede4a8421d04cde68240799352&d_curPage=';

        this.init();
    }
    init () {
        let _self = this;

        let time = setInterval(function () {

            if(_self.currentPage > _self.page) {
                clearInterval(time);
            }
            else{
                console.log('第 ' + _self.currentPage + ' 个爬虫请求发出');
                _self.getDataPackage(_self.baseUrl + (_self.currentPage + 1) + '&d_pageSize=40&d_headId=ad878683a46e56bca93e6f921e59a95&curPage=' + _self.currentPage, _self.currentPage);
                _self.currentPage ++;
            }

        }, 1000 * 5);
    }
    getDataPackage (url, curPage) {
        console.log(url);
        let _self = this;
        https.get(url, function(response){
            var chunks = [];
            var size = 0;
            response.on('data',function(chunk){
                chunks.push(chunk);
                size += chunk.length;
            });
            response.on('end',function(){
                let data = Buffer.concat(chunks, size);
                let html = data.toString();
                
                let $ = cheerio.load(html);
                let result = [];
        
                $('.sojob-list').find('.job-info').each(i => {
                    let map = {};
                    //  个人基本信息
                    map.name = $('.job-info').eq(i).find('h3').attr('title');
        
                    let baseOthersInfo = $('.job-info').eq(i).find('.condition').attr('title');
                    baseOthersInfo = baseOthersInfo.split("_");
        
                    map.reward = baseOthersInfo[0];
                    map.area = baseOthersInfo[1];
                    map.experience = baseOthersInfo[2];
        
                    //  公司信息
                    let companyTagDom = $('.company-info').eq(i).find('.temptation').find('span');
                    let companyTag = [];
                    companyTagDom.each(i => {
                        companyTag.push(companyTagDom.eq(i).text());
                    });
                    let companyInfo = {
                        name: $('.company-info').eq(i).find('.company-name a').attr('title'),
                        href: $('.company-info').eq(i).find('.company-name a').attr('href'),
                        type: $('.company-info').eq(i).find('.industry-link a').text(),
                        tag: companyTag.join(',')
                    }
                    map.company = companyInfo;
                    result.push(map);
                    map = {};
                });
                let dataStr = JSON.stringify(result).trim().replace(/^\[/, curPage == 1 ? '[' : '').replace(/\]$/, curPage == _self.page ? ']' : ',');
                fs.writeFile('./cache/jobs.txt', dataStr, { 'flag': 'a' }, function(err) {
                    if(err) throw err;
                    console.log('写入成功');
                });
            });
        });
    }
}
//  一个数据包40条,这里是99 * 40 = 3960条
new crawlData(99);

  接下来,开始爬取数据了。
图片描述
  这里设置99条的原因是因为猎聘网最大的分页是100,也就是只保留了100页的分页数据。再看看此时jobs.txt文件的大小,有1M多的数据。
图片描述
  看看打印在页面上的数据哈。
图片描述
  可以看到有3960条数据,虽然还不是很多,但已从零开始,完成了一个基本的数据采集流程,不是吗?
  第一篇的数据采集就暂且到这,等待后续的数据分析吧。

点击查看更多内容
5人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
1.3万
获赞与收藏
1519

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消