基於Nodejs的爬蟲

簡介

基於 Node.JS 爬取 博客園 1W+博文,對博文內容作關鍵詞提取,生成詞雲。html

演示

安裝

安裝 gitNode.JSMongoDBYarn前端

克隆代碼node

git clone git@github.com:ZhihaoJian/bokeyuan_spider.git

若是以爲安裝速度慢,可將源切換到淘寶,cmd 或者 powershell 下執行python

yarn config set registry 'https://registry.npm.taobao.org'

進入bokeyuan_spider文件夾安裝依賴git

yarn install

目錄結構

整個項目重要目錄是publicserverpublic目錄放置詞雲的前端代碼,server目錄放置後端代碼。在項目中,server目錄還放置了爬蟲、數據庫等相關代碼。另外,根目錄下的 word.txtjieba 分詞結果。es6

基本工做原理

咱們知道互聯網是經過每一份HTML經過某種方式互相關聯在一塊兒,從而造成一個巨大的 。咱們只要在其中一份頁面就能夠沿着 去到不一樣的頁面。而頁面和頁面之間是經過 超連接 方式聯繫在一塊兒,因此咱們只要找到這個 超連接 就能夠到達下一個頁面。而爬蟲就是這樣的工做方式,找到 超連接,沿着超連接一直前進並記錄下所到之處,就能夠抵達互聯網的任何一個角落。github

核心功能

  • 抓取博文連接

spider.js 中咱們將使用 Google Chrome 的 puppeteer,做爲演示mongodb

打開server目錄下的spider文件裏的spider.jsspider.js的主要功能是使用 puppeteer 對博客園的 班級列表博文 連接進行爬取。shell

如下是spider.js的核心代碼數據庫

/**
 *  spider.js
 */
toPage(page, URL).then(async (url) => {
    console.log('PAGE LOG'.blue + ' Page has been loaded');

    //分頁數量
    totalPages = await page.$eval('.last', el => Number.parseInt(el.textContent));
    console.log(`PAGE LOG`.blue + ` site:${URL} has ${totalPages} pages`);

    //抓取post文超連接
    for (let i = 1; i <= totalPages; i++) {
        url = getNextUrl(i);
        await toPage(page, url, 1500);
        let links = await parseElementHandle(page, url);
        let result = await getPostUrls(links);
        postUrls.push(result);
    }

    //保存到數據庫
    saveToDB(postUrls);

    console.log('PAGE LOG : All tasks have been finished.'.green);
    writeToFileSys();
    await broswer.close();
});

toPage方法是根據指定的URL跳轉的相應頁面,方法接收兩個參數,page是通過 puppeteer 實例化的對象,URL 是咱們指定爬蟲的入口。待頁面加載成功之後,響應回調函數,獲取當前頁面的最大分頁數量,for 循環每隔 1500ms 跳轉到下一頁並抓取頁面中全部博文連接。最後保存到數據庫中。

  • 抓取博文內容

打開 content.js,在這裏咱們不用前面演示的 puppeteer 模塊而使用 cheeriorequest模塊。

安裝 cheeriorequest 模塊

yarn add cheerio request

cheerio能夠簡單看做是服務器端的jQuery,而request是一個高度封裝好了的 nodejs http模塊

如下是 content.js 的核心代碼示例

/* content.js
     * 根據post文連接抓取post文內容
     */
    getIPs().then(async ipTable => {
        for (let i = 0; i < postLen; i++) {
            let postUrl = docs[i];
            proxyIndex < ipTable.length ? proxyIndex : proxyIndex = 0;
            rq(postUrl, ipTable[proxyIndex++], (body) => parseBody(body, postUrl))
                .catch(async e => {
                    console.log('LOG'.red + ': Request ' + postUrl + ' failed. Retrying...');
                    ipTable.splice(proxyIndex, 1);
                    await delay(3000);
                    getIPs().then(ips => ipTable = ipTable.concat(ips));
                    await rq(postUrl, ipTable[++proxyIndex], (body) => parseBody(body, postUrl));
                })
        }
    })

函數 getIps 用於獲取三方代理IP,而後使用 request 模塊對指定的博文連接發起http請求。函數 parseBody 使用 cheerio 模塊解析博文內容,而後保存到數據庫中。在 catch 塊中咱們將處理請求失敗的狀況,這裏咱們更換新的代理IP,針對請求失敗的博文連接從新發起請求。

  • 分詞

關於分詞,咱們選擇 node-jieba,它是python jieba庫的一個nodejs版本

安裝 node-jieba,詳細 API

yarn add node-jieba

核心代碼以下

/* jieba.js
 * 分詞,以txt形式保存到文件系統
 */
(() => {
    const jiebaResult = [];
    POST.find({}, async (err, docs) => {
        if (err) {
            throw new Error(err)
        }
        docs.forEach((v) => {
            jiebaResult.push(jieba(v.post));
        });
        await Promise.all(jiebaResult).then(() => {
            writeToFileSys();
        })
        console.log('end');
    })
})()

咱們從數據庫中取出全部的博文,循環依次對博文作一個關鍵詞提取。由於文本量巨大,因此這裏的重點是 異步分詞。待全部 異步分詞 結束之後,將分詞結果寫入文件系統。

下面給出異步分詞的實現

/**
 * jieba異步分詞
 */
function jieba(post) {
    return new Promise(resolve => {
        analyzer.tags(post, {
            top: 20,
            withWeight: false,
            textRank: false,
            allowPOS: ['ns', 'n', 'vn', 'v']
        }, (err, results) => {
            if (err) {
                console.log(err);
            }
            if (results) {
                results.forEach(word => {
                    if (wordMap.has(word)) {
                        let count = wordMap.get(word);
                        wordMap.set(word, ++count);
                    } else {
                        wordMap.set(word, 0);
                    }
                })
            }
            resolve(wordMap);
        })
    })
}

jieba 函數返回一個 PromisePromise 是 es6 新增的一種異步解決方案,比傳統的解決方案,例如回調函數和事件更強大和合理。由於要對詞頻作統計,使用 Map 對象保存分詞結果,這從查找性能或是可讀性上解釋都更加合理。

踩坑之路

  • 使用 cheerio 解析HTML,中文亂碼

在使用 cheerio.html() 方法時候,發現多數博文內容都變成了 x56ED等 Unicode編碼。經查閱,能夠關閉這個轉換實體編碼的功能

const $ = cheerio.load(html)

改爲

const $ = cheerio.load(html,{decodeEntities:false})
  • 代理問題

單IP爬取1W數據量,明顯要被封號的。最佳的解決方式是買一堆的代理IP,配合 request 庫,輪詢使用代理IP進行請求,進行爬取。親測使用得當的狀況下,1W+博文能夠在5min內爬取完畢。

示例代碼以下

/**
* 
* @param {string} REQUEST_URL 待爬取的URL
* @param {string} proxy 代理IP
* @param {fn} success 成功回調函數
* @param {fn} fail 失敗回調函數
*/
function rq(REQUEST_URL, proxy, callback) {
    return rp({ 'url': url.parse(REQUEST_URL), 'proxy': `http://${proxy}` })
        .then(res => callback(res))
}

詞頻前200

相關文章
相關標籤/搜索