使用 nodejs 寫爬蟲(一): 經常使用模塊和 js 語法

經常使用模塊

經常使用模塊有如下幾個:javascript

  1. fs-extra
  2. superagent
  3. cheerio
  4. log4js
  5. sequelize
  6. chalk
  7. puppeteer

fs-extra

使用 async/await 的前提是必須將接口封裝成 promise, 看一個簡單的例子:html

const sleep = (milliseconds) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(), milliseconds)
    })
}

const main = async  () => {
    await sleep(5000);
    console.log('5秒後...');
}

main();
複製代碼

經過在 async 函數中使用 await + promise 的方式來組織異步代碼就像是同步代碼通常,很是的天然和有助於咱們分析代碼的執行流程。java

在 node 中, fs 模塊是一個很經常使用的操做文件的 native 模塊,fs (file system) 模塊提供了和文件系統相關的一些同步和異步的 api, 有時候使用同步 api 很是有必要,好比你要在一個本身寫的模塊中的在訪問文件後導出一些接口時,這個時候用同步的 api 就很實用。看個例子:node

const path = require('path');
const fs = require('fs-extra');
const { log4js } = require('../../config/log4jsConfig');
const log = log4js.getLogger('qupingce');

const createModels = () => {
    const models = {};
    const fileNames = fs.readdirSync(path.resolve(__dirname, '.'));

    fileNames
        .filter(fileName => fileName !== 'index.js')
        .map(fileName => fileName.slice(0, -3))
        .forEach(modelName => {
            log.info(`Sequelize define model ${modelName}!`);
            models[modelName] = require(path.resolve(__dirname, `./${modelName}.js`));
        })
    return models;
}

module.exports = createModels();
複製代碼

這個模塊訪問了當前目錄下全部的 model 模塊並導出 models, 若是是使用異步接口也就是 fs.readdir, 這樣的話你在別的模塊導入這個模塊是獲取不到 models 的, 緣由是 require 是同步操做,而接口是異步的,同步代碼沒法當即得到異步操做的結果。python

爲了充分發揮 node 異步的優點,咱們仍是應該儘可能使用異步接口。mysql

咱們徹底可使用 fs-extra 模塊來代替 fs 模塊, 相似的模塊還有 mz。fs-extra 包含了全部的 fs 模塊的接口,還對每一個異步接口提供了promise 支持,更棒的是 fs-extra 還提供了一些其它的實用文件操做函數, 好比刪除移動文件的操做。更詳細的介紹請查看官方倉庫 fs-extrajquery

superagent

superagent 是一個 node 的 http client, 能夠類比 java 中的 httpclient 和 okhttp, python 中的 requests。可讓咱們模擬 http 請求。superagent 這個庫有不少實用的特色。git

  1. superagent 會根據 response 的 content-type 自動序列化,經過 response.body 就能夠獲取到序列化後的返回內容
  2. 這個庫會自動緩存和發送 cookies, 不須要咱們手動管理 cookies
  3. 再來就是它的 api 是鏈式調用風格的,調用起來很爽,不過使用的時候要注意調用順序
  4. 它的異步 api 都是返回 promise的。

很是方便有木有😋。官方文檔就是很長的一頁,目錄清晰,很容易就搜索到你須要的內容。最後,superagent 還支持插件集成,好比你須要在超時後自動重發,可使用 superagent-retry。更多插件能夠去 npm 官網上搜索關鍵字 superagent-。更多詳情查看官方文檔superagentgithub

// 官方文檔的一個調用示例 
request
   .post('/api/pet')
   .send({ name: 'Manny', species: 'cat' })
   .set('X-API-Key', 'foobar')
   .set('Accept', 'application/json')
   .then(res => {
      alert('yay got ' + JSON.stringify(res.body));
   });
複製代碼

cheerio

寫過爬蟲的人都知道, 咱們常常會有解析 html 的需求, 從網頁源代碼中爬取信息應該是最基礎的爬蟲手段了。python 中有 beautifulsoup, java 中有 jsoup, node 中有 cheerio。web

cheerio 是爲服務器端設計的,給你近乎完整的 jquery 體驗。使用 cheerio 來解析 html 獲取元素,調用方式和 jquery 操做 dom 元素用法徹底一致。並且還提供了一些方便的接口, 好比獲取 html, 看一個例子:

const cheerio = require('cheerio')
const $ = cheerio.load('<h2 class="title">Hello world</h2>')

$('h2.title').text('Hello there!')
$('h2').addClass('welcome')

$.html()
//=> <h2 class="title welcome">Hello there!</h2>
複製代碼

官方倉庫: cheerio

log4js

log4j 是一個爲 node 設計的日誌模塊。 場景簡單狀況下能夠考慮使用 debug 模塊。 log4js 比較符合我對日誌庫的需求,其實他倆定位也不同,debug 模塊是爲調試而設計的,log4js 則是一個日誌庫,確定得提供文件輸出和分級等常規功能。

log4js 模塊看名字有點向 java 中頗有名的日誌庫 log4j 看齊的節奏。log4j 有如下特色:

  1. 能夠自定義 appender(輸出目標),lo4js 甚至提供了輸出到郵件等目標的 appender
  2. 經過組合不一樣的 appender, 能夠實現不一樣目的的 logger(日誌器)
  3. 提供了日誌分級功能,官方的 FAQ 中提到了若是要對 appender 實現級別過濾,可使用 logLevelFilter
  4. 提供了滾動日誌和自定義輸出格式

下面經過我最近一個爬蟲項目的配置文件來感覺如下這個庫的特色:

const log4js = require('log4js');
const path = require('path');
const fs = require('fs-extra');

const infoFilePath = path.resolve(__dirname, '../out/log/info.log');
const errorFilePath = path.resolve(__dirname, '../out/log/error.log');
log4js.configure({
    appenders: {
        dateFile: {
            type: 'dateFile',
            filename: infoFilePath,
            pattern: 'yyyy-MM-dd',
            compress: false
        },
        errorDateFile: {
            type: 'dateFile',
            filename: errorFilePath,
            pattern: 'yyyy-MM-dd',
            compress: false,
        },
        justErrorsToFile: {
            type: 'logLevelFilter',
            appender: 'errorDateFile',
            level: 'error'
        },
        out: {
            type: 'console'
        }
    },
    categories: {
        default: {
            appenders: ['out'],
            level: 'trace'
        },
        qupingce: {
            appenders: ['out', 'dateFile', 'justErrorsToFile'],
            level: 'trace'
        }
    }
});


const clear = async () => {
    const files = await fs.readdir(path.resolve(__dirname, '../out/log'));
    for (const fileName of files) {
        fs.remove(path.resolve(__dirname, `../out/log/${fileName}`));
    }
}


module.exports = {
    log4js,
    clear
}
複製代碼

sequelize

寫項目咱們每每會有持久化的需求,簡單的場景可使用 JSON 保存數據,若是數據量比較大還要便於管理,那麼咱們就要考慮用數據庫了。若是是操做 mysql 和 sqllite 建議使用 sequelize, 若是是 mongodb, 我更推薦用專門爲 mongodb 設計的 mongoose

sequelize 有幾點我以爲仍是有點不太好,好比默認生成 id (primary key), createdAtupdatedAt

拋開一些自做主張的小毛病,sequelize 設計的仍是很好的。內置的操做符,hooks, 還有 validators 頗有意思。sequelize 還提供了 promise 和 typescript 支持。若是是使用 typescript 開發項目,還有另一個很好的 orm 選擇 : typeorm。更多內容能夠查看官方文檔: sequelize

chalk

chalk 中文意思是粉筆的意思,這個模塊是 node 頗有特點和實用的一個模塊,它能夠爲你輸出的內容添加顏色, 下劃線, 背景色等裝飾。當咱們寫項目的時候每每須要記錄一些步驟和事件,好比打開數據庫連接,發出 http 請求等。咱們能夠適當使用 chalk 來突出某些內容,例如請求的 url 加上下劃線。

const logRequest = (response, isDetailed = false) => {
    const URL = chalk.underline.yellow(response.request.url);
    const basicInfo = `${response.request.method} Status: ${response.status} Content-Type: ${response.type} URL=${URL}`;
    if (!isDetailed) {
        logger.info(basicInfo);
    } else {
        const detailInfo = `${basicInfo}\ntext: ${response.text}`;
        logger.info(detailInfo);
    }
};
複製代碼

調用上面的 logRequest效果:

chalk

更多內容查看官方倉庫chalk

puppeteer

若是這個庫沒據說過,你可能據說過 selenium。puppeteer 是 Google Chrome 團隊開源的一個經過 devtools 協議操縱 chrome 或者Chromium 的 node 模塊。Google 出品,質量有所保證。這個模塊提供了一些高級的 api, 默認狀況下,這個庫操縱的瀏覽器用戶是看不到界面的,也就是所謂的無頭(headless)瀏覽器。固然能夠經過配置一些參數來啓動有界面的模式。在 chrome 中還有一些專門錄製 puppeteer 操做的擴展, 好比Puppeteer Recorder。使用這個庫咱們能夠用來抓取一些經過 js 渲染而不是直接存在於頁面源代碼中的信息。好比 spa 頁面,頁面內容都是 js 渲染出來的。這個時候 puppeteer 就爲咱們解決了這個問題,咱們能夠調用 puppeteer 在頁面某個標籤出現時獲取到頁面當時的渲染出來的 html。事實上,每每不少比較困難的爬蟲解決的最終法寶就是操縱瀏覽器。

前置的 js 語法

async/await

首先要提的就是 async/await, 由於 node 在很早的時候(node 8 LTS)就已經支持 async/await, 如今寫後端項目沒理由不用 async/await了。使用 async/await 可讓咱們從回調煉獄的困境中解脫出來。這裏主要提一下關於使用async/await 時可能會碰到的問題

使用 async/await 怎樣併發?

來看一段測試代碼:

const sleep = (milliseconds) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(), milliseconds)
    })
}

const test1 = async () => {
    for (let i = 0, max = 3; i < max; i++) {
        await sleep(1000);
    }
}

const test2 = async () => {
    Array.from({length: 3}).forEach(async () => {
        await sleep(1000);
    });
}

const main = async  () => {
    console.time('測試 for 循環使用 await');
    await test1();
    console.timeEnd('測試 for 循環使用 await');

    console.time('測試 forEach 調用 async 函數')
    await test2();
    console.timeEnd('測試 forEach 調用 async 函數')
}

main();
複製代碼

運行結果是:

測試 for 循環使用 await: 3003.905ms
測試 forEach 調用 async 函數: 0.372ms
複製代碼

我想可能會有些人會認爲測試 forEach 的結果會是 1 秒左右,事實上測試2等同於如下代碼:

const test2 = async () => {
    // Array.from({length: 3}).forEach(async () => {
    // await sleep(1000);
    // });
    
    Array.from({length: 3}).forEach(() => {
       sleep(1000);
    });
}

複製代碼

從上面的運行結果也能夠看出直接在 for 循環中使用 await + promise, 這樣等同於同步調用, 因此耗時是 3 秒左右。若是要併發則應該直接調用 promise, 由於 forEach 是不會幫你 await 的,因此等價於上面的代碼,三個任務直接異步併發了。

處理多個異步任務

上面的代碼還有一個問題,那就是測試2中並無等待三個任務都執行完就直接結束了,有時候咱們須要等待多個併發任務結束以後再執行後續任務。其實很簡單,利用下 Promise 提供的幾個工具函數就能夠了。

const sleep = (milliseconds, id='') => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`任務${id}執行結束`)
            resolve(id);
        }, milliseconds)
    })
}


const test2 = async () => {
    const tasks = Array.from({length: 3}).map((ele, index) => sleep(1000, index));
    const resultArray = await Promise.all(tasks);
    console.log({ resultArray} )
    console.log('全部任務執行結束');
}

const main = async  () => {
    console.time('使用 Promise.all 處理多個併發任務')
    await test2();
    console.timeEnd('使用 Promise.all 處理多個併發任務')
}

main()

複製代碼

運行結果:

任務0執行結束
任務1執行結束
任務2執行結束
{ resultArray: [ 0, 1, 2 ] }
全部任務執行結束
使用 Promise.all 處理多個併發任務: 1018.628ms

複製代碼

除了 Promise.all, Promise 還有 race 等接口,不過最經常使用應該就是 all 和 race 了。

正則表達式

正則表達式是處理字符串強有力的工具。核心是匹配,由此衍生出提取,查找, 替換的等操做。

有時候咱們經過 cheerio 中獲取到某個標籤內的文本時,咱們須要提取其中的部分信息,這個時候正則表達式就該上場了。正則表達式的相關語法這裏就不詳細說明了, 入門推薦看廖雪峯的正則表達式教程。來看個實例:

// 服務器返回的 img url 是: /GetFile/getUploadImg?fileName=9b1cc22c74bc44c8af78b46e0ca4c352.png
// 如今我只想提取文件名,後綴名也不要
const imgUrl = '/GetFile/getUploadImg?fileName=9b1cc22c74bc44c8af78b46e0ca4c352.png';
const imgReg = /\/GetFile\/getUploadImg\?fileName=(.+)\..+/;
const imgName = imgUrl.match(imgReg)[1];
console.log(imgName); // => 9b1cc22c74bc44c8af78b46e0ca4c352

複製代碼

暫時先介紹到這裏了,後續有更多內容會繼續補充。

本文爲原創內容,首發於我的博客, 轉載請註明出處。若是有問題歡迎郵件騷擾 ytj2713151713@gmail.com。

相關文章
相關標籤/搜索