利用NodeJS和PhantomJS抓取网站页面信息以及网站截图


Posted in NodeJs onNovember 18, 2013

利用PhantomJS做网页截图经济适用,但其API较少,做其他功能就比较吃力了。例如,其自带的Web Server Mongoose最高只能同时支持10个请求,指望他能独立成为一个服务是不怎么实际的。所以这里需要另一个语言来支撑服务,这里选用NodeJS来完成。

安装PhantomJS

首先,去PhantomJS官网下载对应平台的版本,或者下载源代码自行编译。然后将PhantomJS配置进环境变量,输入

$ phantomjs

如果有反应,那么就可以进行下一步了。

利用PhantomJS进行简单截图

var webpage = require('webpage') , page = webpage.create(); page.viewportSize = { width: 1024, height: 800 }; page.clipRect = { top: 0, left: 0, width: 1024, height: 800 }; page.settings = { javascriptEnabled: false, loadImages: true, userAgent: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.31 (KHTML, like Gecko) PhantomJS/19.0' }; page.open('http://www.baidu.com', function (status) { var data; if (status === 'fail') { console.log('open page fail!'); } else { page.render('./snapshot/test.png'); } // release the memory page.close(); });

这里我们设置了窗口大小为1024 * 800:

page.viewportSize = { width: 1024, height: 800 };

截取从(0, 0)为起点的1024 * 800大小的图像:

page.clipRect = { top: 0, left: 0, width: 1024, height: 800 };

禁止Javascript,允许图片载入,并将userAgent改为"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.31 (KHTML, like Gecko) PhantomJS/19.0":

page.settings = { javascriptEnabled: false, loadImages: true, userAgent: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.31 (KHTML, like Gecko) PhantomJS/19.0'};

然后利用page.open打开页面,最后截图输出到./snapshot/test.png中:

page.render('./snapshot/test.png') ;

 

NodeJS与PhantomJS通讯

我们先来看看PhantomJS能做什么通讯。

命令行传参
例如:
phantomjs snapshot.js http://www.baidu.com
命令行传参只能在PhantomJS开启时进行传参,在运行过程中就无能为力了。
标准输出
标准输出能从PhantomJS向NodeJS输出数据,但却没法从NodeJS传数据给PhantomJS。
不过测试中,标准输出是这几种方式传输最快的,在大量数据传输中应当考虑。
 HTTP
PhantomJS向NodeJS服务发出HTTP请求,然后NodeJS返回相应的数据。
这种方式很简单,但是请求只能由PhantomJS发出。
Websocket
值得注意的是PhantomJS 1.9.0支持Websocket了,不过可惜是hixie-76 Websocket,不过毕竟还是提供了一种NodeJS主动向PhantomJS通讯的方案了。
测试中,我们发现PhantomJS连上本地的Websocket服务居然需要1秒左右,暂时不考虑这种方法吧。
phantomjs-node
phantomjs-node成功将PhantomJS作为NodeJS的一个模块来使用,但我们看看作者的原理解释:
I will answer that question with a question. How do you communicate with a process that doesn't support shared memory, sockets, FIFOs, or standard input?
Well, there's one thing PhantomJS does support, and that's opening webpages. In fact, it's really good at opening web pages. So we communicate with PhantomJS by spinning up an instance of ExpressJS, opening Phantom in a subprocess, and pointing it at a special webpage that turns socket.io messages into alert()calls. Those alert() calls are picked up by Phantom and there you go!
The communication itself happens via James Halliday's fantastic dnode library, which fortunately works well enough when combined with browserify to run straight out of PhantomJS's pidgin Javascript environment.
实际上phantomjs-node使用的也是HTTP或者Websocket来进行通讯,不过其依赖庞大,我们只想做一个简单的东西,暂时还是不考虑这个东东吧。

 

设计图

利用NodeJS和PhantomJS抓取网站页面信息以及网站截图

 

让我们开始吧
我们在第一版中选用HTTP进行实现。

首先利用cluster进行简单的进程守护(index.js):

module.exports = (function () {
  "use strict"
  var cluster = require('cluster')
    , fs = require('fs');
  if(!fs.existsSync('./snapshot')) {
    fs.mkdirSync('./snapshot');
  }
  if (cluster.isMaster) {
    cluster.fork();
    cluster.on('exit', function (worker) {
      console.log('Worker' + worker.id + ' died :(');
      process.nextTick(function () {
        cluster.fork();
      });
    })
  } else {
    require('./extract.js');
  }
})();

然后利用connect做我们的对外API(extract.js):

module.exports = (function () {
  "use strict"
  var connect = require('connect')
    , fs = require('fs')
    , spawn = require('child_process').spawn
    , jobMan = require('./lib/jobMan.js')
    , bridge = require('./lib/bridge.js')
    , pkg = JSON.parse(fs.readFileSync('./package.json'));
  var app = connect()
    .use(connect.logger('dev'))
    .use('/snapshot', connect.static(__dirname + '/snapshot', { maxAge: pkg.maxAge }))
    .use(connect.bodyParser())
    .use('/bridge', bridge)
    .use('/api', function (req, res, next) {
      if (req.method !== "POST" || !req.body.campaignId) return next();
      if (!req.body.urls || !req.body.urls.length) return jobMan.watch(req.body.campaignId, req, res, next);
      var campaignId = req.body.campaignId
        , imagesPath = './snapshot/' + campaignId + '/'
        , urls = []
        , url
        , imagePath;
      function _deal(id, url, imagePath) {
        // just push into urls list
        urls.push({
          id: id,
          url: url,
          imagePath: imagePath
        });
      }
      for (var i = req.body.urls.length; i--;) {
        url = req.body.urls[i];
        imagePath = imagesPath + i + '.png';
        _deal(i, url, imagePath);
      }
      jobMan.register(campaignId, urls, req, res, next);
      var snapshot = spawn('phantomjs', ['snapshot.js', campaignId]);
      snapshot.stdout.on('data', function (data) {
        console.log('stdout: ' + data);
      });
      snapshot.stderr.on('data', function (data) {
        console.log('stderr: ' + data);
      });
      snapshot.on('close', function (code) {
        console.log('snapshot exited with code ' + code);
      });
    })
    .use(connect.static(__dirname + '/html', { maxAge: pkg.maxAge }))
    .listen(pkg.port, function () { console.log('listen: ' + 'http://localhost:' + pkg.port); });
})();

这里我们引用了两个模块bridge和jobMan。

其中bridge是HTTP通讯桥梁,jobMan是工作管理器。我们通过campaignId来对应一个job,然后将job和response委托给jobMan管理。然后启动PhantomJS进行处理。

通讯桥梁负责接受或者返回job的相关信息,并交给jobMan(bridge.js):

module.exports = (function () {
  "use strict"
  var jobMan = require('./jobMan.js')
    , fs = require('fs')
    , pkg = JSON.parse(fs.readFileSync('./package.json'));
  return function (req, res, next) {
      if (req.headers.secret !== pkg.secret) return next();
      // Snapshot APP can post url information
      if (req.method === "POST") {
        var body = JSON.parse(JSON.stringify(req.body));
        jobMan.fire(body);
        res.end('');
      // Snapshot APP can get the urls should extract
      } else {
        var urls = jobMan.getUrls(req.url.match(/campaignId=([^&]*)(\s|&|$)/)[1]);
        res.writeHead(200, {'Content-Type': 'application/json'});
        res.statuCode = 200;
        res.end(JSON.stringify({ urls: urls }));
      }
  };
})();

如果request method为POST,则我们认为PhantomJS正在给我们推送job的相关信息。而为GET时,则认为其要获取job的信息。

jobMan负责管理job,并发送目前得到的job信息通过response返回给client(jobMan.js):

module.exports = (function () {
  "use strict"
  var fs = require('fs')
    , fetch = require('./fetch.js')
    , _jobs = {};
  function _send(campaignId){
    var job = _jobs[campaignId];
    if (!job) return;
    if (job.waiting) {
      job.waiting = false;
      clearTimeout(job.timeout);
      var finished = (job.urlsNum === job.finishNum)
        , data = {
        campaignId: campaignId,
        urls: job.urls,
        finished: finished
      };
      job.urls = [];
      var res = job.res;
      if (finished) {
        _jobs[campaignId] = null;
        delete _jobs[campaignId]
      }
      res.writeHead(200, {'Content-Type': 'application/json'});
      res.statuCode = 200;
      res.end(JSON.stringify(data));
    }
  }
  function register(campaignId, urls, req, res, next) {
    _jobs[campaignId] = {
      urlsNum: urls.length,
      finishNum: 0,
      urls: [],
      cacheUrls: urls,
      res: null,
      waiting: false,
      timeout: null
    };
    watch(campaignId, req, res, next);
  }
  function watch(campaignId, req, res, next) {
    _jobs[campaignId].res = res;
    // 20s timeout
    _jobs[campaignId].timeout = setTimeout(function () {
      _send(campaignId);
    }, 20000);
  }
  function fire(opts) {
    var campaignId = opts.campaignId
      , job = _jobs[campaignId]
      , fetchObj = fetch(opts.html);
    if (job) {
      if (+opts.status && fetchObj.title) {
        job.urls.push({
          id: opts.id,
          url: opts.url,
          image: opts.image,
          title: fetchObj.title,
          description: fetchObj.description,
          status: +opts.status
        });
      } else {
        job.urls.push({
          id: opts.id,
          url: opts.url,
          status: +opts.status
        });
      }
      if (!job.waiting) {
        job.waiting = true;
        setTimeout(function () {
          _send(campaignId);
        }, 500);
      }
      job.finishNum ++;
    } else {
      console.log('job can not found!');
    }
  }
  function getUrls(campaignId) {
    var job = _jobs[campaignId];
    if (job) return job.cacheUrls;
  }
  return {
    register: register,
    watch: watch,
    fire: fire,
    getUrls: getUrls
  };
})();

这里我们用到fetch对html进行抓取其title和description,fetch实现比较简单(fetch.js):

module.exports = (function () {
  "use strict"
  return function (html) {
    if (!html) return { title: false, description: false };
    var title = html.match(/\<title\>(.*?)\<\/title\>/)
      , meta = html.match(/\<meta\s(.*?)\/?\>/g)
      , description;
    if (meta) {
      for (var i = meta.length; i--;) {
        if(meta[i].indexOf('name="description"') > -1 || meta[i].indexOf('name="Description"') > -1){
          description = meta[i].match(/content\=\"(.*?)\"/)[1];
        }
      }
    }
    (title && title[1] !== '') ? (title = title[1]) : (title = 'No Title');
    description || (description = 'No Description');
    return {
      title: title,
      description: description
    };
  };
})();

最后是PhantomJS运行的源代码,其启动后通过HTTP向bridge获取job信息,然后每完成job的其中一个url就通过HTTP返回给bridge(snapshot.js):

var webpage = require('webpage')
  , args = require('system').args
  , fs = require('fs')
  , campaignId = args[1]
  , pkg = JSON.parse(fs.read('./package.json'));
function snapshot(id, url, imagePath) {
  var page = webpage.create()
    , send
    , begin
    , save
    , end;
  page.viewportSize = { width: 1024, height: 800 };
  page.clipRect = { top: 0, left: 0, width: 1024, height: 800 };
  page.settings = {
    javascriptEnabled: false,
    loadImages: true,
    userAgent: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.31 (KHTML, like Gecko) PhantomJS/1.9.0'
  };
  page.open(url, function (status) {
    var data;
    if (status === 'fail') {
      data = [
        'campaignId=',
        campaignId,
        '&url=',
        encodeURIComponent(url),
        '&id=',
        id,
        '&status=',
      ].join('');
      postPage.open('http://localhost:' + pkg.port + '/bridge', 'POST', data, function () {});
    } else { 
      page.render(imagePath);
      var html = page.content;
      // callback NodeJS
      data = [
        'campaignId=',
        campaignId,
        '&html=',
        encodeURIComponent(html),
        '&url=',
        encodeURIComponent(url),
        '&image=',
        encodeURIComponent(imagePath),
        '&id=',
        id,
        '&status=',
      ].join('');
      postMan.post(data);
    }
    // release the memory
    page.close();
  });
}
var postMan = {
  postPage: null,
  posting: false,
  datas: [],
  len: 0,
  currentNum: 0,
  init: function (snapshot) {
    var postPage = webpage.create();
    postPage.customHeaders = {
      'secret': pkg.secret
    };
    postPage.open('http://localhost:' + pkg.port + '/bridge?campaignId=' + campaignId, function () {
      var urls = JSON.parse(postPage.plainText).urls
        , url;
      this.len = urls.length;
      if (this.len) {
        for (var i = this.len; i--;) {
          url = urls[i];
          snapshot(url.id, url.url, url.imagePath);
        }
      }
    });
    this.postPage = postPage;
  },
  post: function (data) {
    this.datas.push(data);
    if (!this.posting) {
      this.posting = true;
      this.fire();
    }
  },
  fire: function () {
    if (this.datas.length) {
      var data = this.datas.shift()
        , that = this;
      this.postPage.open('http://localhost:' + pkg.port + '/bridge', 'POST', data, function () {
        that.fire();
        // kill child process
        setTimeout(function () {
          if (++this.currentNum === this.len) {
            that.postPage.close();
            phantom.exit();
          }
        }, 500);
      });
    } else {
      this.posting = false;
    }
  }
};
postMan.init(snapshot);
效果

利用NodeJS和PhantomJS抓取网站页面信息以及网站截图

 

NodeJs 相关文章推荐
我的NodeJs学习小结(一)
Jul 06 NodeJs
nodejs中实现阻塞实例
Mar 24 NodeJs
NodeJs——入门必看攻略
Jun 27 NodeJs
详解nodejs操作mongodb数据库封装DB类
Apr 10 NodeJs
详解nodejs微信公众号开发——6.自定义菜单
Apr 13 NodeJs
nodejs+websocket实时聊天系统改进版
May 18 NodeJs
深入学习nodejs中的async模块的使用方法
Jul 12 NodeJs
nodejs基于express实现文件上传的方法
Mar 19 NodeJs
NodeJs搭建本地服务器之使用手机访问的实例讲解
May 12 NodeJs
NodeJS实现自定义流的方法
Aug 01 NodeJs
深入理解NodeJS 多进程和集群
Oct 17 NodeJs
Nodejs实现多文件夹文件同步
Oct 17 NodeJs
NodeJS的url截取模块url-extract的使用实例
Nov 18 #NodeJs
NodeJS url验证(url-valid)的使用方法
Nov 18 #NodeJs
NodeJS与Mysql的交互示例代码
Aug 18 #NodeJs
利用NodeJS的子进程(child_process)调用系统命令的方法分享
Jun 05 #NodeJs
将nodejs打包工具整合到鼠标右键的方法
May 11 #NodeJs
用nodejs写的一个简单项目打包工具
May 11 #NodeJs
nodejs教程 安装express及配置app.js文件的详细步骤
May 11 #NodeJs
You might like
社区(php&amp;&amp;mysql)一
2006/10/09 PHP
php数组去重的函数代码
2013/02/03 PHP
一个严格的PHP Session会话超时时间设置方法
2014/06/10 PHP
php采用curl模仿登录人人网发布动态的方法
2014/11/07 PHP
php指定长度分割字符串str_split函数用法示例
2017/01/30 PHP
jQuery源码中的chunker 正则过滤符分析
2012/07/31 Javascript
使用js+jquery实现无限极联动
2013/05/23 Javascript
JQuery获取样式中的background-color颜色值的问题
2013/08/20 Javascript
JQuery控制div外点击隐藏而div内点击不会隐藏的方法
2015/01/13 Javascript
js实现跟随鼠标移动且带关闭功能的图片广告实例
2015/02/26 Javascript
javascript中使用正则表达式清理table样式的代码
2020/04/01 Javascript
js实现Select列表各项上移和下移的方法
2015/08/14 Javascript
jQuery实现宽屏图片轮播实例教程
2015/11/24 Javascript
AngularJS入门教程之数据绑定用法示例
2016/11/01 Javascript
JS创建对象的写法示例
2016/11/04 Javascript
EasyUI Combobox设置默认值 获取text的方法
2016/11/28 Javascript
Ajax高级笔记 JavaScript高级程序设计笔记
2017/06/22 Javascript
详解JavaScript中的六种错误类型
2017/09/21 Javascript
js中对象与对象创建方法的各种方法
2019/02/27 Javascript
Vuex的actions属性的具体使用
2019/04/14 Javascript
Vue源码之关于vm.$delete()/Vue.use()内部原理详解
2019/05/01 Javascript
解决ant-design-vue中menu菜单无法默认展开的问题
2020/10/31 Javascript
jquery实现图片放大镜效果
2020/12/23 jQuery
[02:28]DOTA2亚洲邀请赛附加赛 RECAP赛事回顾
2015/01/29 DOTA
微信 用脚本查看是否被微信好友删除
2016/10/28 Python
Python 使用os.remove删除文件夹时报错的解决方法
2017/01/13 Python
详解Python字符串切片
2019/05/20 Python
深入浅析Python 函数注解与匿名函数
2020/02/24 Python
python爬虫实现获取下一页代码
2020/03/13 Python
Python响应对象text属性乱码解决方案
2020/03/31 Python
网络方面基础面试题
2012/11/16 面试题
工程项目经理岗位职责
2013/12/15 职场文书
草船借箭教学反思
2014/02/03 职场文书
竞选部门副经理的自荐书范文
2014/02/11 职场文书
2015年推普周活动方案
2015/05/06 职场文书
php实例化对象的实例方法
2021/11/17 PHP