node爬取拉勾網數據並導出爲excel文件

前言

以前斷斷續續學習了node.js,今天就拿拉勾網練練手,順便經過數據瞭解瞭解最近的招聘行情哈!node方面算是萌新一個吧,但願能夠和你們共同窗習和進步。

1、概要

咱們首先須要明確具體的需求:php

  1. 能夠經過node index 城市 職位來爬取相關信息
  2. 也能夠輸入node index start直接爬取咱們預約義好的城市和職位數組,循環爬取不一樣城市的不一樣職位信息
  3. 將最終爬取的結果存儲在本地的./data目錄下
  4. 生成對應的excel文件,並存儲到本地

2、爬蟲用到的相關模塊

  • fs: 用於對系統文件及目錄進行讀寫操做
  • async:流程控制
  • superagent:客戶端請求代理模塊
  • node-xlsx:將必定格式的文件導出爲excel

3、爬蟲主要步驟:

初始化項目

新建項目目錄前端

在合適的磁盤目錄下建立項目目錄 node-crwl-lagou

初始化項目java

  1. 進入node-crwl-lagou文件夾下
  2. 執行npm init,初始化package.json文件

安裝依賴包node

  1. npm install async
  2. npm install superagent
  3. npm install node-xlsx

命令行輸入的處理

對於在命令行輸入的內容,能夠用process.argv來獲取,他會返回個數組,數組的每一項就是用戶輸入的內容。
區分node index 地域 職位node index start兩種輸入,最簡單的就是判斷process.argv的長度,長度爲四的話,就直接調用爬蟲主程序爬取數據,長度爲三的話,咱們就須要經過預約義的城市和職位數組來拼湊url了,而後利用async.mapSeries循環調用主程序。關於命令分析的主頁代碼以下:python

if (process.argv.length === 4) {
  let args = process.argv
  console.log('準備開始請求' + args[2] + '的' + args[3] + '職位數據');
  requsetCrwl.controlRequest(args[2], args[3])
} else if (process.argv.length === 3 && process.argv[2] === 'start') {
  let arr = []
  for (let i = 0; i < defaultArgv.city.length; i++) {
    for (let j = 0; j < defaultArgv.position.length; j++) {
      let obj = {}
      obj.city = defaultArgv.city[i]
      obj.position = defaultArgv.position[j]
      arr.push(obj)
    }
  }
  async.mapSeries(arr, function (item, callback) {
    console.log('準備開始請求' + item.city + '的' + item.position + '職位數據');
    requsetCrwl.controlRequest(item.city, item.position, callback)
  }, function (err) {
    if (err) throw err
  })
} else {
  console.log('請正確輸入要爬取的城市和職位,正確格式爲:"node index 城市 關鍵詞" 或 "node index start" 例如:"node index 北京 php" 或"node index start"')
}

預約義好的城市和職位數組以下:android

{
    "city": ["北京","上海","廣州","深圳","杭州","南京","成都","西安","武漢","重慶"],
    "position": ["前端","java","php","ios","android","c++","python",".NET"]
}

接下來就是爬蟲主程序部分的分析了。ios

分析頁面,找到請求地址

首先咱們打開拉勾網首頁,輸入查詢信息(好比node),而後查看控制檯,找到相關的請求,如圖:c++

圖片描述

這個post請求https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false就是咱們所須要的,經過三個請求參數來獲取不一樣的數據,簡單的分析就可得知:參數first是標註當前是不是第一頁,true爲是,false爲否;參數pn是當前的頁碼;參數kd是查詢輸入的內容。git

經過superagent請求數據

首先須要明確得是,整個程序是異步的,咱們須要用async.series來依次調用。
查看分析返回的response:github

圖片描述

能夠看到content.positionResult.totalCount就是咱們所須要的總頁數
咱們用superagent直接調用post請求,控制檯會提示以下信息:

{'success': False, 'msg': '您操做太頻繁,請稍後再訪問', 'clientIp': '122.xxx.xxx.xxx'}

這實際上是反爬蟲策略之一,咱們只須要給其添加一個請求頭便可,請求頭的獲取方式很簡單,以下:

圖片描述

而後在用superagent調用post請求,主要代碼以下:

// 先獲取總頁數
    (cb) => {
      superagent
        .post(`https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false&city=${city}&kd=${position}&pn=1`)
        .send({
          'pn': 1,
          'kd': position,
          'first': true
        })
        .set(options.options)
        .end((err, res) => {
          if (err) throw err
          // console.log(res.text)
          let resObj = JSON.parse(res.text)
          if (resObj.success === true) {
            totalPage = resObj.content.positionResult.totalCount;
            cb(null, totalPage);
          } else {
            console.log(`獲取數據失敗:${res.text}}`)
          }
        })
    },

拿到總頁數後,咱們就能夠經過總頁數/15獲取到pn參數,循環生成全部url並存入urls中:

(cb) => {
      for (let i=0;Math.ceil(i<totalPage/15);i++) {
        urls.push(`https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false&city=${city}&kd=${position}&pn=${i}`)
      }
      console.log(`${city}的${position}職位共${totalPage}條數據,${urls.length}頁`);
      cb(null, urls);
    },

有了全部的url,在想爬到全部的數據就不是難事了,繼續用superagent的post方法循環請求全部的url,每一次獲取到數據後,在data目錄下建立json文件,將返回的數據寫入。這裏看似簡單,可是有兩點須要注意:

  1. 爲了防止併發請求太多而致使被封IP:循環url時候須要使用async.mapLimit方法控制併發爲3, 每次請求完都要過兩秒在發送下一次的請求
  2. 在async.mapLimit的第四個參數中,須要經過判斷調用主函數的第三個參數是否存在來區分一下是那種命令輸入,由於對於node index start這個命令,咱們使用得是async.mapSeries,每次調用主函數都傳遞了(city, position, callback),因此若是是node index start的話,須要在每次獲取數據完後將null傳遞回去,不然沒法進行下一次循環

主要代碼以下:

// 控制併發爲3
    (cb) => {
      async.mapLimit(urls, 3, (url, callback) => {
        num++;
        let page = url.split('&')[3].split('=')[1];
        superagent
          .post(url)
          .send({
            'pn': totalPage,
            'kd': position,
            'first': false
          })
          .set(options.options)
          .end((err, res) => {
            if (err) throw err
            let resObj = JSON.parse(res.text)
            if (resObj.success === true) {
              console.log(`正在抓取第${page}頁,當前併發數量:${num}`);
              if (!fs.existsSync('./data')) {
                fs.mkdirSync('./data');
              }
              // 將數據以.json格式儲存在data文件夾下
              fs.writeFile(`./data/${city}_${position}_${page}.json`, res.text, (err) => {
                if (err) throw err;
                // 寫入數據完成後,兩秒後再發送下一次請求
                setTimeout(() => {
                  num--;
                  console.log(`第${page}頁寫入成功`);
                  callback(null, 'success');
                }, 2000);
              });
            }
          })
      }, (err, result) => {
        if (err) throw err;
        // 這個arguments是調用controlRequest函數的參數,能夠區分是那種爬取(循環仍是單個)
        if (arguments[2]) {
          ok = 1;
        }
        cb(null, ok)
      })
    },
    () => {
      if (ok) {
        setTimeout(function () {
          console.log(`${city}的${position}數據請求完成`);
          indexCallback(null);
        }, 5000);
      } else {
        console.log(`${city}的${position}數據請求完成`);
      }
      // exportExcel.exportExcel() // 導出爲excel
    }

導出的json文件以下:
圖片描述

json文件導出爲excel

將json文件導出爲excel有多種方式,我使用的是node-xlsx這個node包,這個包須要將數據按照固定的格式傳入,而後導出便可,因此咱們首先作的就是先拼出其所需的數據格式:

function exportExcel() {
  let list = fs.readdirSync('./data')
  let dataArr = []
  list.forEach((item, index) => {
    let path = `./data/${item}`
    let obj = fs.readFileSync(path, 'utf-8')
    let content = JSON.parse(obj).content.positionResult.result
    let arr = [['companyFullName', 'createTime', 'workYear', 'education', 'city', 'positionName', 'positionAdvantage', 'companyLabelList', 'salary']]
    content.forEach((contentItem) => {
      arr.push([contentItem.companyFullName, contentItem.phone, contentItem.workYear, contentItem.education, contentItem.city, contentItem.positionName, contentItem.positionAdvantage, contentItem.companyLabelList.join(','), contentItem.salary])
    })
    dataArr[index] = {
      data: arr,
      name: path.split('./data/')[1] // 名字不能包含 \ / ? * [ ]
    }
  })

// 數據格式
// var data = [
//   {
//     name : 'sheet1',
//     data : [
//       [
//         'ID',
//         'Name',
//         'Score'
//       ],
//       [
//         '1',
//         'Michael',
//         '99'
//
//       ],
//       [
//         '2',
//         'Jordan',
//         '98'
//       ]
//     ]
//   },
//   {
//     name : 'sheet2',
//     data : [
//       [
//         'AA',
//         'BB'
//       ],
//       [
//         '23',
//         '24'
//       ]
//     ]
//   }
// ]

// 寫xlsx
  var buffer = xlsx.build(dataArr)
  fs.writeFile('./result.xlsx', buffer, function (err)
    {
      if (err)
        throw err;
      console.log('Write to xls has finished');

// 讀xlsx
//     var obj = xlsx.parse("./" + "resut.xls");
//     console.log(JSON.stringify(obj));
    }
  );
}

導出的excel文件以下,每一頁的數據都是一個sheet,比較清晰明瞭:
圖片描述

咱們能夠很清楚的從中看出目前西安.net的招聘狀況,以後也能夠考慮用更形象的圖表方式展現爬到的數據,應該會更加直觀!

總結

其實整個爬蟲過程並不複雜,注意就是注意的小點不少,好比async的各個方法的使用以及導出設置header等,總之,也是收穫滿滿噠!

源碼

gitbug地址: https://github.com/fighting12...

相關文章
相關標籤/搜索