基於 Node.JS 爬取 博客園 1W+博文,對博文內容作關鍵詞提取,生成詞雲。html
克隆代碼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
整個項目重要目錄是public
和server
,public
目錄放置詞雲的前端代碼,server
目錄放置後端代碼。在項目中,server
目錄還放置了爬蟲、數據庫等相關代碼。另外,根目錄下的 word.txt
是 jieba
分詞結果。es6
咱們知道互聯網是經過每一份HTML經過某種方式互相關聯在一塊兒,從而造成一個巨大的 網
。咱們只要在其中一份頁面就能夠沿着 網
去到不一樣的頁面。而頁面和頁面之間是經過 超連接
方式聯繫在一塊兒,因此咱們只要找到這個 超連接
就能夠到達下一個頁面。而爬蟲就是這樣的工做方式,找到 超連接
,沿着超連接一直前進並記錄下所到之處,就能夠抵達互聯網的任何一個角落。github
在 spider.js
中咱們將使用 Google Chrome 的 puppeteer
,做爲演示mongodb
打開server
目錄下的spider
文件裏的spider.js
。spider.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
模塊而使用 cheerio
和 request
模塊。
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
函數返回一個 Promise
,Promise
是 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)) }