如何管理 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
启动。
master.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爬取数据