從零開始寫一個node爬蟲(上)—— 資料採集篇

johnchou發表於2021-09-09

  爬蟲相信大家都知道,這裡我們從一個空的資料夾開始,也來寫一寫我們自己的爬蟲程式吧。

  
  
  爬蟲畢竟涉及到資料的爬取,所以其實有一個道德的約束,那就是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 = '%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 = '%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 = '%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 = '%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條資料,雖然還不是很多,但已從零開始,完成了一個基本的資料採集流程,不是嗎?
  第一篇的資料採集就暫且到這,等待後續的資料分析吧。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4422/viewspace-2823075/,如需轉載,請註明出處,否則將追究法律責任。

相關文章