写这篇 blog 其实一开始我是拒绝的,因为爬虫爬的就是cnblog博客园。搞不好编辑看到了就把我的账号给封了:)。
言归正传,前端同学可能向来对爬虫不是很感冒,觉得爬虫需要用偏后端的语言,诸如 php , python 等。当然这是在 nodejs 前了,nodejs 的出现,使得 Javascript 也可以用来写爬虫了。由于 nodejs 强大的异步特性,让我们可以轻松以异步高并发去爬取网站,当然这里的轻松指的是 cpu 的开销。
要读懂本文,其实只需要有
本文较长且图多,但如果能耐下心读完本文,你会发现,简单的一个爬虫实现并不难,并且能从中学到很多东西。
本文中的完整的爬虫代码,在我的github上可以下载。主要的逻辑代码在 server.js 中,建议边对照代码边往下看。
在详细说爬虫前,先来简单看看要达成的最终目标,入口为 ,博客园文章列表页每页有20篇文章,最多可以翻到200页。我这个爬虫要做的就是异步并发去爬取这4000篇文章的具体内容,拿到一些我们想要的关键数据。
爬虫流程
看到了最终结果,那么我们接下来看看该如何一步一步通过一个简单的 nodejs 爬虫拿到我们想要的数据,首先简单科普一下爬虫的流程,要完成一个爬虫,主要的步骤分为:
抓取
爬虫爬虫,最重要的步骤就是如何把想要的页面抓取回来。并且能兼顾时间效率,能够并发的同时爬取多个页面。
同时,要获取目标内容,需要我们分析页面结构,因为 ajax 的盛行,许多页面内容并非是一个url就能请求的的回来的,通常一个页面的内容是经过多次请求异步生成的。所以这就要求我们能够利用抓包工具分析页面结构。
如果深入做下去,你会发现要面对不同的网页要求,比如有认证的,不同文件格式、编码处理,各种奇怪的url合规化处理、重复抓取问题、cookies 跟随问题、多线程多进程抓取、多节点抓取、抓取调度、资源压缩等一系列问题。
所以第一步就是拉网页回来,慢慢你会发现各种问题待你优化。
存储
当把页面内容抓回来后,一般不会直接分析,而是用一定策略存下来,个人觉得更好的架构应该是把分析和抓取分离,更加松散,每个环节出了问题能够隔离另外一个环节可能出现的问题,好排查也好更新发布。
那么存文件系统、SQL or NOSQL 数据库、内存数据库,如何去存就是这个环节的重点。
分析
对网页进行文本分析,提取链接也好,提取正文也好,总之看你的需求,但是一定要做的就是分析链接了。通常分析与存储会交替进行。可以用你认为最快最优的办法,比如正则表达式。然后将分析后的结果应用与其他环节。
展示
要是你做了一堆事情,一点展示输出都没有,如何展现价值?
所以找到好的展示组件,去show出肌肉也是关键。
如果你为了做个站去写爬虫,抑或你要分析某个东西的数据,都不要忘了这个环节,更好地把结果展示出来给别人感受。
编写爬虫代码
Step.1 页面分析
现在我们一步一步来完成我们的爬虫,目标是爬取博客园第1页至第200页内的4000篇文章,获取其中的作者信息,并保存分析。
共4000篇文章,所以首先我们要获得这个4000篇文章的入口,然后再异步并发的去请求4000篇文章的内容。但是这个4000篇文章的入口 URL 分布在200个页面中。所以我们要做的第一步是 从这个200个页面当中,提取出4000个 URL 。并且是通过异步并发的方式,当收集完4000个 URL 再进行下一步。那么现在我们的目标就很明确了:
Step2.获取4000个文章入口URL
要获取这么多 URL ,首先还是得从分析单页面开始,F12 打开 devtools 。很容易发现文章入口链接保存在 class 为 titlelnk 的 <a> 标签中,所以4000个 URL 就需要我们轮询 200个列表页 ,将每页的20个 链接保存起来。那么该如何异步并发的从200个页面去收集这4000个 URL 呢,继续寻找规律,看看每一页的列表页的 URL 结构:
那么,1~200页的列表页 URL 应该是这个样子的:
for(var i=1 ; i<= 200 ; i++){ pageUrls.push('http://www.cnblogs.com/#p'+i); }
有了存放200个文章列表页的 URL ,再要获取4000个文章入口就不难了,下面贴出关键代码,一些最基本的nodejs语法(譬如如何搭建一个http服务器)默认大家都已经会了:
// 一些依赖库 var http = require("http"), url = require("url"), superagent = require("superagent"), cheerio = require("cheerio"), async = require("async"), eventproxy = require('eventproxy'); var ep = new eventproxy(), urlsArray = [], //存放爬取网址 pageUrls = [], //存放收集文章页面网站 pageNum = 200; //要爬取文章的页数 for(var i=1 ; i<= 200 ; i++){ pageUrls.push('http://www.cnblogs.com/#p'+i); } // 主start程序 function start(){ function onRequest(req, res){ // 轮询 所有文章列表页 pageUrls.forEach(function(pageUrl){ superagent.get(pageUrl) .end(function(err,pres){ // pres.text 里面存储着请求返回的 html 内容,将它传给 cheerio.load 之后 // 就可以得到一个实现了 jquery 接口的变量,我们习惯性地将它命名为 `$` // 剩下就都是利用$ 使用 jquery 的语法了 var $ = cheerio.load(pres.text); var curPageUrls = $('.titlelnk'); for(var i = 0 ; i < curPageUrls.length ; i++){ var articleUrl = curPageUrls.eq(i).attr('href'); urlsArray.push(articleUrl); // 相当于一个计数器 ep.emit('BlogArticleHtml', articleUrl); } }); }); ep.after('BlogArticleHtml', pageUrls.length*20 ,function(articleUrls){ // 当所有 'BlogArticleHtml' 事件完成后的回调触发下面事件 // ... }); } http.createServer(onRequest).listen(3000); } exports.start= start;
这里我们用到了三个库,superagent 、 cheerio 、 eventproxy。
分别简单介绍一下:
superagent
superagent( ) 是个轻量的的 http 方面的库,是nodejs里一个非常方便的客户端请求代理模块,当我们需要进行 get 、 post 、 head 等网络请求时,尝试下它吧。
cheerio
cheerio(https://github.com/cheeriojs/cheerio ) 大家可以理解成一个 Node.js 版的 jquery,用来从网页中以 css selector 取数据,使用方式跟 jquery 一样一样的。
eventproxy
eventproxy(https://github.com/JacksonTian/eventproxy ) 非常轻量的工具,但是能够带来一种事件式编程的思维变化。
用 js 写过异步的同学应该都知道,如果你要并发异步获取两三个地址的数据,并且要在获取到数据之后,对这些数据一起进行利用的话,常规的写法是自己维护一个计数器。