如何管理 PhantomJS 实例的“池”

Posted

技术标签:

【中文标题】如何管理 PhantomJS 实例的“池”【英文标题】:How to manage a 'pool' of PhantomJS instances 【发布时间】:2012-04-15 05:13:23 【问题描述】:

我正在计划一个供我自己在内部使用的网络服务,它接受一个参数,一个 URL,并从该 URL 返回表示 已解析 DOM 的 html。通过已解决,我的意思是 web 服务将首先获取该 URL 处的页面,然后使用 PhantomJS 来“渲染”页面,然后在执行所有 DHTML、AJAX 调用等之后返回结果源。然而,基于每个请求(我现在正在这样做)启动幻象是方式太慢了。我宁愿拥有一个 PhantomJS 实例池,其中一个始终可以为我的 web 服务的最新调用提供服务。

以前有没有在这种事情上做过任何工作?我宁愿这个 web 服务建立在别人的工作之上,也不愿从头开始为自己编写一个池管理器/http 代理服务器。

更多上下文:我在下面列出了到目前为止我看到的 2 个类似项目以及为什么我避开了每个项目,从而导致了这个关于管理 PhantomJS 实例池的问题.

jsdom - 据我所见,它具有在页面上执行脚本的强大功能,但它不会尝试复制浏览器行为,所以如果我将它用作通用“DOM 解析器”最终需要大量额外的代码来处理各种边缘情况、事件调用等。我看到的第一个示例是必须为我使用 node.js 设置的测试应用程序手动调用 body 标签的 onload() 函数。这似乎是一个深兔子洞的开始。

Selenium - 它只是有更多的移动部件,因此设置一个池来管理长期存在的浏览器实例将比使用 PhantomJS 更复杂。我不需要它的任何宏录制/脚本优势。我只想要一个在获取网页和解析它的 DOM 方面同样高效的网络服务,就好像我正在使用浏览器浏览该 URL 一样(或者如果我可以让它忽略图像等甚至更快)

【问题讨论】:

【参考方案1】:

我设置了一个 PhantomJs 云服务,它几乎可以满足您的要求。我花了大约 5 周的时间完成工作。

您将遇到的最大问题是memory leaks in PhantomJs 的已知问题。我解决这个问题的方法是每 50 次调用循环我的实例。

您将遇到的第二大问题是每页处理非常消耗 CPU 和内存,因此每个 CPU 只能运行 4 个左右的实例。

您将遇到的第三大问题是 PhantomJs 对页面完成事件和重定向非常古怪。您将被告知您的页面在实际渲染之前已完成渲染。 There are a number of ways to deal with this,但遗憾的是没有什么“标准”。

您必须处理的第四大问题是 nodejs 和 phantomjs 之间的互操作,幸好有 a lot of npm packages that deal with this issue 可供选择。

所以我知道我有偏见(因为我写了我要建议的解决方案),但我建议你查看PhantomJsCloud.com,它是免费的。

2015 年 1 月更新: 我遇到的另一个(第 5 次?)大问题是如何从管理器/负载平衡器发送请求/响应。最初我使用的是 PhantomJS 的内置 HTTP 服务器,但一直遇到它的限制,尤其是在最大响应大小方面。我最终将请求/响应写入本地文件系统作为通信线路。 * 实施服务所花费的总时间可能代表 20 人周的问题,可能是 1000 小时的工作。 * 仅供参考,我正在为下一个版本进行完全重写....(进行中)

【讨论】:

很好的答案杰森。如果您能继续告诉我们更多有关实施细节的信息,那就太好了。例如,您如何管理所有实例?另外,如何从 Node 本身启动 de Phantom 实例?有什么模块建议这样做吗?或者你产生进程? 我从服务器上的 nodejs“路由器”应用程序进行所有管理。它通过正常的 nodejs spawn process 命令启动多个 phantomjs.exe 实例。实际上在这方面没有什么特别的。我尝试了在 NPM 上找到的所有各种 phantomjs 包装器,但坦率地说,它们大多都很糟糕。最终只使用 phantomjs 的内置 http 服务器与 nodejs 路由器应用程序进行通信。 在一个 phantomJS 实例中创建多个网页对象怎么样?这有什么问题吗?【参考方案2】:

async javascript library 在 Node 中工作,并且有一个 queue 函数,对于这类事情非常方便:

queue(worker, concurrency)

创建具有指定并发性的队列对象。添加到队列的任务将被并行处理(直到并发限制)。如果所有工作人员都在进行中,则任务将排队,直到有一个可用。一旦工作人员完成了一项任务,就会调用该任务的回调。

一些伪代码:

function getSourceViaPhantomJs(url, callback) 
  var resultingHtml = someMagicPhantomJsStuff(url);
  callback(null, resultingHtml);


var q = async.queue(function (task, callback) 
  // delegate to a function that should call callback when it's done
  // with (err, resultingHtml) as parameters
  getSourceViaPhantomJs(task.url, callback);
, 5); // up to 5 PhantomJS calls at a time

app.get('/some/url', function(req, res) 
  q.push(url: params['url_to_scrape'], function (err, results) 
    res.end(results);
  );
);

查看entire documentation for queue at the project's readme。

【讨论】:

你知道排队的详细工作原理吗?我在想它是在队列中调用多个 XHR 请求吗?我正在寻找一种解决方案,它实际上让 phantomjs 进程作为守护进程运行,而不是每次有任务进入时都启动一个。 @CMCDragonkai 问题提到“一个 PhantomJS 实例池,其中一个始终可用于服务对我的 web 服务的最新调用”,这意味着不断运行 PhantomJS 守护程序,但这个答案适用于任何一种情况。 async.queue 函数所做的只是确保在任何给定时间不超过一定数量的函数调用;您在该函数中执行的操作取决于您。 你我的朋友,差不多 4 年后,让我免于头痛。【参考方案3】:

对于我的硕士论文,我开发了库phantomjs-pool,它正是这样做的。它允许提供工作,然后映射到 PhantomJS 工作人员。该库处理作业分配、通信、错误处理、日志记录、重新启动和其他一些事情。该库已成功用于爬取超过一百万页。

示例:

以下代码对数字 0 到 9 执行 Google 搜索,并将页面截图保存为 googleX.png。四个网站并行爬取(由于创建了四个worker)。该脚本通过node master.js启动。

ma​​ster.js(在 Node.js 环境中运行)

var Pool = require('phantomjs-pool').Pool;

var pool = new Pool( // create a pool
    numWorkers : 4,   // with 4 workers
    jobCallback : jobCallback,
    workerFile : __dirname + '/worker.js', // location of the worker file
    phantomjsBinary : __dirname + '/path/to/phantomjs_binary' // either provide the location of the binary or install phantomjs or phantomjs2 (via npm)
);
pool.start();

function jobCallback(job, worker, index)  // called to create a single job
    if (index < 10)  // index is count up for each job automatically
        job(index, function(err)  // create the job with index as data
            console.log('DONE: ' + index); // log that the job was done
        );
     else 
        job(null); // no more jobs
    

worker.js(在 PhantomJS 环境中运行)

var webpage = require('webpage');

module.exports = function(data, done, worker)  // data provided by the master
    var page = webpage.create();

    // search for the given data (which contains the index number) and save a screenshot
    page.open('https://www.google.com/search?q=' + data, function() 
        page.render('google' + data + '.png');
        done(); // signal that the job was executed
    );

;

【讨论】:

这是一个很棒的图书馆。我想知道,有没有办法检测何时不再有进程产生?例如,通过异步或承诺,在pool.start() 之后等待一系列进程完成后做某事? 谢谢。目前没有办法像使用异步一样简单。但是,您可以为每个单独的作业使用回调(当一个作业完成时触发)并以这种方式增加一个计数器。因此,您仍然能够检测到所有作业何时完成。【参考方案4】:

作为@JasonS 出色答案的替代方案,您可以尝试我构建的PhearJS。 PhearJS 是用 NodeJS 为 PhantomJS 实例编写的主管,并通过 HTTP 提供 API。它可以从Github 开源。

【讨论】:

【参考方案5】:

如果你使用 nodejs 为什么不使用 selenium-webdriver

    运行一些 phantomjs 实例作为 webdriver phantomjs --webdriver=port_number

    为每个 phantomjs 实例创建 PhantomInstance

    function PhantomInstance(port) 
        this.port = port;
    
    
    PhantomInstance.prototype.getDriver = function() 
        var self = this;
        var driver = new webdriver.Builder()
            .forBrowser('phantomjs')
            .usingServer('http://localhost:'+self.port)
            .build();
        return driver;
    
    

    并将它们全部放入一个数组 [phantomInstance1,phantomInstance2]

    创建 dispather.js 从数组中获取免费的 phantomInstance 和

    var driver = phantomInstance.getDriver();
    

【讨论】:

这不是一个好办法。相信我……在我的程序中,我使用了 selenium-webdriver,但最后我放弃了!【参考方案6】:

如果您使用的是 nodejs,您可以使用https://github.com/sgentle/phantomjs-node,这将允许您将任意数量的 phantomjs 进程连接到您的主要 NodeJS 进程,因此,可以使用 async.js 和许多节点好东西。

【讨论】:

这不是真的。如果您创建多个 phantom JS 实例并同时运行它们,您会得到“错误:监听 EADDRINUSE”。我目前正在寻找一种将幻像实例放在不同端口或导致 EADDRINUSE 的任何东西的方法。 当然你有责任启动幻象实例,以便它们在不同的端口上侦听。

以上是关于如何管理 PhantomJS 实例的“池”的主要内容,如果未能解决你的问题,请参考以下文章

scrapy使用PhantomJS和selenium爬取数据

Selenium中通过修改User-Agent标识将PhantomJS伪装成Chrome浏览器

多个 PhantomJS 实例挂起

Node.js如何使用MySQL的连接池实例

Phan - PHP静态分析器

Phan 给出内置 JetBrains PhpStorm 注释的问题