phantomjs使用
卢卡斯·怀特 ( Lukas White )对本文进行了同行评审。 感谢所有SitePoint的同行评审员使SitePoint内容达到最佳状态!
在项目过程中,经常发现自己需要编写自定义脚本来执行各种操作。 通常通过命令行( CLI )执行的这种一次性脚本实际上可以用于任何类型的任务。 多年来编写了许多此类脚本,我逐渐意识到,花少量时间预先放置自定义CLI 微框架以促进此过程的价值。 幸运的是, Node.js及其广泛的软件包生态系统npm使得做到这一点变得容易。 无论是解析文本文件还是运行ETL ,都有适当的约定可以轻松地以有效和结构化的方式添加新功能。
尽管不一定与命令行相关联,但Web爬网通常用于某些问题领域,例如自动功能测试和污损检测。 本教程演示了如何实现一个轻量级的CLI框架,其支持的操作围绕Web爬行。 希望这将使您的创意源源不断,无论您对爬网还是命令行感兴趣。 涵盖的技术包括Node.js, PhantomJS以及与爬网和CLI相关的各种npm软件包。
该教程的源代码可以在GitHub上找到 。 为了运行示例,您将需要同时安装Node.js和PhantomJS。 下载和安装它们的说明可以在这里找到: Node.js , PhantomJS 。
设置基本的命令行框架
任何CLI框架的核心都是将命令(通常包括一个或多个可选或必需参数)转换为具体操作的概念。 在这方面非常有用的两个npm软件包是commander和提示符 。
Commander允许您定义支持哪些参数,而提示符允许您(适当地)提示用户在运行时输入。 最终结果是一个语法优美的界面,用于基于某些用户提供的数据以动态行为执行各种动作。
比如说,我们希望我们的命令看起来像这样:
$ node run.js -x hello_world
我们的入口点( run.js )定义了如下可能的参数:
program
.version('1.0.0')
.option('-x --action-to-perform [string]', 'The type of action to perform.')
.option('-u --url [string]', 'Optional URL used by certain actions')
.parse(process.argv);
并定义各种用户输入案例,如下所示:
var performAction = require('./actions/' + program.actionToPerform)
switch (program.actionToPerform) {
case 'hello_world':
prompt.get([{
// What the property name should be in the result object
name: 'url',
// The prompt message shown to the user
description: 'Enter a URL',
// Whether or not the user is required to enter a value
required: true,
// Validates the user input
conform: function (value) {
// In this case, the user must enter a valid URL
return validUrl.isWebUri(value);
}
}], function (err, result) {
// Perform some action following successful input
performAction(phantomInstance, result.url);
});
break;
}
至此,我们已经定义了一条基本路径,通过该路径我们可以指定要执行的动作,并添加了接受URL的提示。 我们只需要添加一个模块来处理特定于此操作的逻辑即可。 我们可以通过在actions目录中添加一个名为hello_world.js的文件来做到这一点:
'use strict';
/**
* @param Horseman phantomInstance
* @param string url
*/
module.exports = function (phantomInstance, url) {
if (!url || typeof url !== 'string') {
throw 'You must specify a url to ping';
} else {
console.log('Pinging url: ', url);
}
phantomInstance
.open(url)
.status()
.then(function (statusCode) {
if (Number(statusCode) >= 400) {
throw 'Page failed with status: ' + statusCode;
} else {
console.log('Hello world. Status code returned: ', statusCode);
}
})
.catch(function (err) {
console.log('Error: ', err);
})
// Always close the Horseman instance
// Otherwise you might end up with orphaned phantom processes
.close();
};
如您所见,该模块期望提供一个PhantomJS对象的实例( phantomInstance
)和一个URL( url
)。 我们将暂时了解定义PhantomJS实例的细节,但是到目前为止,足以看到我们已经为触发特定操作奠定了基础。 现在我们已经约定好了,我们可以轻松地以定义和理智的方式添加新操作。
使用Horseman与PhantomJS爬行
Horseman是一个Node.js软件包,它提供了一个强大的接口来创建PhantomJS进程并与之交互。 对Horseman及其功能的全面解释将保证能拥有其自己的文章,但足以说,它使您可以轻松地模拟人类用户可能在其浏览器中表现出的任何行为。 Horseman提供了广泛的配置选项,包括自动注入jQuery和忽略SSL证书警告之类的东西。 它还提供了Cookie处理和截图的功能。
每次我们通过CLI框架触发动作时,输入脚本( run.js
)都会实例化Horseman的实例,并将其传递到指定的动作模块。 用伪代码看起来像这样:
var phantomInstance = new Horseman({
phantomPath: '/usr/local/bin/phantomjs',
loadImages: true,
injectJquery: true,
webSecurity: true,
ignoreSSLErrors: true
});
performAction(phantomInstance, ...);
现在,当我们运行命令时,Horseman实例和输入URL将传递到hello_world模块,从而使PhantomJS请求URL,捕获其状态代码并将状态打印到控制台。 我们刚刚使用Horseman运行了第一个真正的爬网。 Giddyup!
复杂相互作用的链骑士方法
到目前为止,我们已经看过Horseman的一种非常简单的用法,但是当我们将其方法链接在一起以在浏览器中执行一系列操作时,该程序包可以做更多的事情。 为了演示其中一些功能,让我们定义一个模拟用户浏览GitHub来创建新存储库的操作。
请注意:此示例仅用于演示目的,不应视为创建Github存储库的可行方法。 这仅是如何使用Horseman与Web应用程序进行交互的示例。 如果您对以自动化方式创建存储库感兴趣,则应使用官方的Github API 。
让我们假设新的爬网将像这样触发:
$ node run.js -x create_repo
遵循我们已经采用的CLI框架的约定,我们需要向action目录中添加一个名为create_repo.js的新模块。 与我们之前的“ hello world”示例一样, create_repo
模块导出一个包含该动作所有逻辑的函数。
module.exports = function (phantomInstance, username, password, repository) {
if (!username || !password || !repository) {
throw 'You must specify login credentials and a repository name';
}
...
}
![](https://i-blog.csdnimg.cn/blog_migrate/b31f4b75f780408f7b89f0fdde51d7ee.png)
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
请注意,通过此操作,我们将比以前传递更多的参数给导出的函数。 参数包括username
, password
和repository
。 用户成功完成提示质询后,我们将从run.js
传递这些值。
但是,在发生任何事情之前,我们必须向run.js
添加逻辑以触发提示并捕获数据。 为此,我们在主switch
语句中添加了一个case:
switch (program.actionToPerform) {
case 'create_repo':
prompt.get([{
name: 'repository',
description: 'Enter repository name',
required: true
}, {
name: 'username',
description: 'Enter GitHub username',
required: true
}, {
name: 'password',
description: 'Enter GitHub password',
hidden: true,
required: true
}], function (err, result) {
performAction(
phantomInstance,
result.username,
result.password,
result.repository
);
});
break;
...
现在,我们已经将该钩子添加到run.js
,当用户输入相关数据时,它将被传递到动作,从而使我们能够继续进行爬网。
至于create_repo
逻辑本身,我们使用Horseman的方法数组导航到Github登录页面,输入提供的username
和password
,然后提交以下表单:
phantomInstance
.open('https://github.com/login')
.type('input[name="login"]', username)
.type('input[name="password"]', password)
.click('input[name="commit"]')
我们通过等待表单提交页面加载来继续执行链:
.waitForNextPage()
之后,我们使用jQuery确定登录是否成功:
.evaluate(function () {
$ = window.$ || window.jQuery;
var fullHtml = $('body').html();
return !fullHtml.match(/Incorrect username or password/);
})
.then(function (isLoggedIn) {
if (!isLoggedIn) {
throw 'Login failed';
}
})
如果登录失败,将引发错误。 否则,我们将继续使用链接方法导航到我们的个人资料页面:
.click('a:contains("Your profile")')
.waitForNextPage()
进入个人资料页面后,我们导航至存储库选项卡:
.click('nav[role="navigation"] a:nth-child(2)')
.waitForSelector('a.new-repo')
在“存储库”选项卡上时,我们检查是否存在具有指定名称的存储库。 如果是这样,那么我们会抛出一个错误。 如果没有,那么我们继续我们的序列:
// Gather the names of the user's existing repositories
.evaluate(function () {
$ = window.$ || window.jQuery;
var possibleRepositories = [];
$('.repo-list-item h3 a').each(function (i, el) {
possibleRepositories.push($(el).text().replace(/^\s+/, ''));
});
return possibleRepositories;
})
// Determine if the specified repository already exists
.then(function (possibleRepositories) {
if (possibleRepositories.indexOf(repository) > -1) {
throw 'Repository already exists: ' + repository;
}
})
假设没有引发任何错误,我们以编程方式单击“新存储库”按钮,然后等待下一页:
.click('a:contains("New")')
.waitForNextPage()
之后,我们输入提供的repository
名称并提交表单:
.type('input#repository_name', repository)
.click('button:contains("Create repository")')
到达结果页面后,我们知道存储库已创建:
.waitForNextPage()
.then(function () {
console.log('Success! You should now have a new repository at: ', 'https://github.com/' + username + '/' + repository);
})
与任何Horseman爬网一样,至关重要的是,最后关闭Horseman实例:
.close();
无法关闭Horseman实例可能导致孤立的PhantomJS进程在计算机上持久存在。
爬行以收集数据
至此,我们已经组装了一系列静态动作,以编程方式在GitHub上创建了一个新的存储库。 为此,我们链接了一系列Horseman方法。
这种方法对于预先已知的特定结构和行为模式很有用,但是,您可能会发现您有时需要实现更灵活的脚本。 如果您的操作顺序有可能根据具体情况而有很大不同,或者产生多种不同的结果,则可能是这种情况。 如果您需要从DOM中提取数据,情况也会如此。
在这种情况下,您可以使用Horseman的validate()方法,该方法允许您通过注入内联或外部链接JavaScript在浏览器中执行自由形式的交互。
本节演示了从页面(锚链接,在这种情况下)提取基本数据的示例。 可能需要这样做的一种情况是构建一个污损检测爬网程序以攻击域中的每个URL。
与上一个示例一样,我们必须首先将新模块添加到actions目录:
module.exports = function (phantomInstance, url) {
if (!url || typeof url !== 'string') {
throw 'You must specify a url to gather links';
}
phantomInstance
.open(url)
// Interact with the page. This code is run in the browser.
.evaluate(function () {
$ = window.$ || window.jQuery;
// Return a single result object with properties for
// whatever intelligence you want to derive from the page
var result = {
links: []
};
if ($) {
$('a').each(function (i, el) {
var href = $(el).attr('href');
if (href) {
if (!href.match(/^(#|javascript|mailto)/) && result.links.indexOf(href) === -1) {
result.links.push(href);
}
}
});
}
// jQuery should be present, but if it's not, then collect the links using pure javascript
else {
var links = document.getElementsByTagName('a');
for (var i = 0; i < links.length; i++) {
var href = links[i].href;
if (href) {
if (!href.match(/^(#|javascript|mailto)/) && result.links.indexOf(href) === -1) {
result.links.push(href);
}
}
}
}
return result;
})
.then(function (result) {
console.log('Success! Here are the derived links: \n', result.links);
})
.catch(function (err) {
console.log('Error getting links: ', err);
})
// Always close the Horseman instance
// Otherwise you might end up with orphaned phantom processes
.close();
然后在run.js
为新动作添加一个钩子:
switch (program.actionToPerform) {
...
case 'get_links':
prompt.get([{
name: 'url',
description: 'Enter URL to gather links from',
required: true,
conform: function (value) {
return validUrl.isWebUri(value);
}
}], function (err, result) {
performAction(phantomInstance, result.url);
});
break;
现在此代码已经到位,我们可以运行爬网以通过运行以下命令从任何给定页面提取链接:
$ node run.js -x get_links
此操作演示了从页面中提取数据,并且没有利用Horseman内置的任何浏览器操作。 它可以直接执行您在evaluate()
方法中放置的任何JavaScript,并像在浏览器环境中本地运行一样执行此操作。
在本节中应该提到的最后一件事,在前面已经提到:您不仅可以使用evaluate()
方法在浏览器中执行自定义JavaScript,而且还可以在运行评估逻辑之前将外部脚本注入运行时环境中。 。 可以这样完成:
phantomInstance
.open(url)
.injectJs('scripts/CustomLogic.js')
.evaluate(function() {
var x = CustomLogic.getX(); // Assumes variable 'CustomLogic' was loaded by scripts/custom_logic.js
console.log('Retrieved x using CustomLogic: ', x);
})
通过扩展上述逻辑,您几乎可以在任何网站上执行任何操作。
使用御马者截图
我要演示的最终用例是如何使用Horseman截屏。 我们可以使用Horseman的screenshotBase64()方法来执行此操作,该方法返回表示屏幕截图的base64编码的字符串。
与前面的示例一样,我们必须首先将新模块添加到actions目录:
module.exports = function (phantomInstance, url) {
if (!url || typeof url !== 'string') {
throw 'You must specify a url to take a screenshot';
}
console.log('Taking screenshot of: ', url);
phantomInstance
.open(url)
// Optionally, determine the status of the response
.status()
.then(function (statusCode) {
console.log('HTTP status code: ', statusCode);
if (Number(statusCode) >= 400) {
throw 'Page failed with status: ' + statusCode;
}
})
// Take the screenshot
.screenshotBase64('PNG')
// Save the screenshot to a file
.then(function (screenshotBase64) {
// Name the file based on a sha1 hash of the url
var urlSha1 = crypto.createHash('sha1').update(url).digest('hex')
, filePath = 'screenshots/' + urlSha1 + '.base64.png.txt';
fs.writeFile(filePath, screenshotBase64, function (err) {
if (err) {
throw err;
}
console.log('Success! You should now have a new screenshot at: ', filePath);
});
})
.catch(function (err) {
console.log('Error taking screenshot: ', err);
})
// Always close the Horseman instance
// Otherwise you might end up with orphaned phantom processes
.close();
};
然后在run.js
为新动作添加一个钩子:
case 'take_screenshot':
prompt.get([{
name: 'url',
description: 'Enter URL to take screenshot of',
required: true,
conform: function (value) {
return validUrl.isWebUri(value);
}
}], function (err, result) {
performAction(phantomInstance, result.url);
});
break;
现在,您可以使用以下命令拍摄屏幕截图:
$ node run.js -x take_screenshot
使用base64编码的字符串(而不是例如保存实际图像)的原因是它们是表示原始图像数据的便捷方式。 这个StackOverflow的答案会更详细。
如果要保存实际图像,则可以使用screenshot()方法。
结论
本教程试图通过使用Horseman软件包来利用PhantomJS来演示自定义的CLI微框架以及在Node.js中进行爬网的一些基本逻辑。 尽管使用CLI框架可能会使许多项目受益,但爬网的使用通常仅限于非常具体的问题域。 质量保证(QA)是一个常见的领域,其中可以将爬网用于功能和用户界面测试。 另一个领域是安全性,例如,您可能希望定期爬网您的网站,以检测它是否遭到破坏或受到破坏。
无论您的项目是哪种情况,请确保明确定义您的目标,并尽量避免干扰。 在可能的情况下获取许可,尽最大可能礼貌,并且永远不要对站点进行DDoS防护 。 如果您怀疑自己在生成大量自动流量,则可能是,应该重新评估您的目标,实施或权限级别。
翻译自: https://www.sitepoint.com/web-crawling-node-phantomjs-horseman/
phantomjs使用