基于 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)) }