一、准备工作
- 首先当然是安装node,这一步忽略。
- 然后是各种模块,本实例用到了http、fs、url、cheerio、request、async、phantom,前三个是node自带的,无需install。
- 因为要服务器渲染,所以要用到phantomjs,这个需要自行安装一下,最后再配置一下全局环境。
二、模块解释
- cheerio模块用于解析DOM树,进行DOM操作, 具体用法跟JQuery类似,对熟悉JQ的人来说,学会使用也就是几分钟的事。
- request模块,http模块的高级封装版,便于操作。
- async模块,解决“恶魔金字塔”问题。
- phantom模块,在服务器端渲染整个界面,为的是能够爬取到页面上一些通过js等动态加载的内容。
三、具体实现
1. 公用接口
| 1 2 3 4
 | exports.Strategy = {  "SAVE_IN_ROOT": 1,  "SAVE_IN_SUB_DIR": 2 };
 | 
| 1 2 3 4 5 6 7 8 9 10 11 12 13
 | exports.uniqueArray = function (arr) {  var hash = {},      len = arr.length,      result = [];  for (var i = 0; i < len; i++){      if (!hash[arr[i]]){          hash[arr[i]] = true;          result.push(arr[i]);      }  }  return result; };
 | 
| 1 2 3
 | exports.timer = function (date,msg) {  console.log(msg + " : "+(new Date() - date) +"ms" ); };
 | 
2. 配置
| 1 2 3 4 5 6 7 8 9 10 11
 | var config = {  url: "http://localhost:8081/dhay/",  savePath: "J:/nodejs/open-source-spider",  containOutLink: false,   totalNum: 10,   endWith: "html",  saveStrategy: publicAPI.Strategy.SAVE_IN_ROOT,  getOuterJs: false,  getOuterCss: false,  getOuterImages: false };
 | 
为了简化操作,判断是否为外部资源的方式简化为判断URL是否以http或https为开头,虽然不够严谨,但是能保证爬取下来的网页能根据URL获取到资源。
 保存策略目前也只实现了SAVE_IN_ROOT而已,懒~
3. 全局变量
 | 1 2 3 4
 |  var list = [config.url];  var count = 0;  var date = null;  var urlInfo = url.parse(config.url); 
 | 
4. 获取服务器渲染之后的页面
| 1 2 3 4 5 6 7 8 9 10
 | phantom.create().then(function (ph) {      ph.createPage().then(function (page) {          page.open(url).then(function (status) {                  if (status == 'success') {                  page.property('content').then(function (html) {                      console.log(html);                  }              }          })      });
 | 
5. 解析DOM树
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 | var $ = cheerio.load(html); var js, css , images; var scripts = $("script"); js = getJs(scripts); console.log(js); var stylesheets = $("link[rel='stylesheet']"); css = getCss(stylesheets); console.log(css); var imgs = $("img"); images = getImages(imgs); console.log(images); if(!config.totalNum || count < config.totalNum){    var links = $("a");    getLink(links,url); }
 | 
获取页面中所有的js、css和img,分别进行调用函数,返回所要爬取的文件的url数组。
6. getJs()等函数
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14
 | function getJs(scripts) {  var res = [];  scripts.each(function (i, script) {      var src = script.attribs.src;      if (!src)          return;      if (!config.getOuterJs) {          if (/^https?/.test(src))              return;      }      res.push(src);  });  return publicAPI.uniqueArray(res); }
 | 
getJs()函数,遍历每一个元素,判断其src属性是否存在,不存在则跳过,再根据配置判断是否获取外部文件,满足各条件的加入到res数组中,最后去重后返回结果。
 getCss、getImages和getLink方法与上述类似,不同的是getLink多了一些判断和URL格式化操作。
7. 保存网页文本
 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
 |  var saveHtml = function (url, html, callback) {  url = url.match(/https?:\/\/((?:(?![\?])[\S])*)/)[1];  var endWith = /\/$/.test(url);   url = endWith ? url.match(/(\S*)\/$/)[1] : url;   var reg = new RegExp(/\.html|\.htm|\.asp|\.jsp$/);   var usePathAsName = reg.test(url);  var array = url.split("/");   array[0] = urlInfo.hostname;  var length = usePathAsName ? array.length-1: array.length;  var currentPath = config.savePath;  for (var i = 0; i < length; i++) {       (function (i) {          currentPath += "/" + array[i];          if (fs.existsSync(currentPath)) {              write(i, callback)          } else {              try {                  fs.mkdirSync(currentPath);                  write(i, callback)              }catch (err){                  console.log(err);              }          }      })(i)  }  function write(index, callback) {       if (index == length - 1) {          var fileName = endWith ? "index." + config.endWith : usePathAsName ? array[array.length-1] : array[array.length-1] + config.endWith;           fs.writeFile(currentPath + "/" + fileName, html, function (err) {              if (err) {                  console.log(err, "appendFile");              } else {                  callback();              }          });      }  } };
 | 
8. 保存js等资源
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
 | var saveJs = function (url, js, callback ,callback2) {  if(!js.length){      callback2(null);      return;  }  var length = js.length;  var count = 0;  var root = config.savePath + "/" + urlInfo.hostname;  if(config.saveStrategy == publicAPI.Strategy.SAVE_IN_ROOT){      if(fs.existsSync(root)){          write(callback,callback2)      }else{          fs.mkdirSync(root);          write(callback,callback2)      }  }  function write(callback,callback2) {      for(var i = 0;i<length;i++){          (function (i) {              var reg = new RegExp(/^\//);              var path = reg.test(js[i]) ? js[i].substring(1) : js[i];              var array = path.split("/");              var currentPath = root;              var len = array.length;              for(var j =0;j<len-1;j++){                   (function (j) {                      currentPath += "/" + array[j];                      if (fs.existsSync(currentPath)) {                          if(j == len-2){                              fetch(js[i],function () {                                  callback(count, js[i])                              },callback2)                          }                      } else {                          try {                              fs.mkdirSync(currentPath);console.log(j,4);                              if(j == len-2){                                  fetch(js[i],function () {                                      callback(count, js[i])                                  },callback2)                              }                          }catch (err){                              console.log("error!")                          }                      }                  })(j);              }          })(i);      }  }  function fetch(js,callback,callback2) {      request(url+js,function (err,res,body) {                     js = js.match(/((?:(?![\?])[\S])*)/)[1];          if(err){              count++;          }else{              fs.writeFile(root +"/"+ js,body,function (err) {                  count++;                  if(count == length)                      callback2(null);                  if(err){                      console.log(err);                  }else{                      callback();                  }              });          }      })  } };
 | 
保存js等资源的方法比保存网页的更为复杂,主要是因为一个页面可能存在多个js、css等资源,这里有两种保存的策略,一是直接将所有文件保存在根目录下,如根目录如localhost,现有js文件链接为“localhost:8080//abc/js/main/js”,将该js文件保存在localhost/js/下,另一种是保存在对应目录下,即localhost/abc/js/下,但是第二种方法会导致出现很多重复的文件,就没有实现出来。
 获取css、images的方法类似,不做赘述。
9. 并行执行写入操作
 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
 |  async.parallel([  function (callback) {      saveHtml(url, html, function () {          console.log("Page:"+(count+1)+"    Url:"+url+"    success!\n");          callback(null);      });  },  function (callback) {      saveJs(url,js, function (x, js) {          console.log("Page:"+(count+1)+"    Js"+(x+1)+"     Src:"+js+"    Success!\n");      },callback);  },  function (callback) {      saveCss(url, css, function (x, css) {          console.log("Page:"+(count+1)+"    Css"+(x+1)+"     Src:"+css+"    Success!\n");      },callback);  },  function (callback) {      saveImage(url, images, function (x,img) {          console.log("Page:"+(count+1)+"    Images"+(x+1)+"     Src:"+img+"    Success!\n");      },callback);  } ],function (err) {  page.close();  if(err){      console.log(err,"ERROR IN PARALLEL PAGE "+(count+1));  }else {      count++;      console.log("Page:"+(count)+" finished!");      var cur = list.shift(),next = list[0];      next = /^https?:/.test(next) ? next : cur+'/'+next;      if( count < config.totalNum)          requirePage(next);  } })
 | 
用async模块,并行处理写入操作,当所有写入操作结束后,从list队列获取下一跳地址,循环操作。