本文介绍在Node.js环境中,针对包含Ajax、JavaScript重定向等复杂动态内容的现代网站,如何构建高效的网页抓取方案。通过结合PhantomJS无头浏览器处理页面渲染与JavaScript执行,并利用Request库进行资源下载,实现了从登录验证到PDF文件下载的完整流程。文章详细解析了会话Cookie同步、并发控制及DOM交互等关键技术难点与代码实现。
动态网页抓取的挑战与背景
当前,网页抓取技术已广为人知,但面对现代化网站中广泛使用的Ajax轮询、XMLHttpRequest、WebSockets以及Flash Sockets等复杂技术,传统的简单爬虫往往难以胜任。
以Hubdoc项目为例,我们需要从银行、公用事业及信用卡公司网站抓取账单金额、到期日、账户号及近期账单PDF文件。起初尝试了类似MessageLab/Symantec时期的Perl简单爬虫方案,但发现垃圾邮件网站的结构比正规金融机构网站更为简单,导致该方案在实际应用中进展不顺。
基于Request与Cheerio的基础方案
为解决这一问题,我们首先采用Mikea开发的request库。通过浏览器Network窗口观察实际发出的请求头,并将其复制到代码中,模拟从登录到下载PDF的完整请求过程。为了简化HTML数据提取,我们引入了轻量级的cheerio库,使其能像jQuery一样通过CSS选择器选取页面元素。整个流程被封装在一个框架中,该框架还负责从数据库获取证书、加载独立机器人实例,并通过socket.io与UI进行通信。
上述方法对部分网站有效,但这仅是JS脚本,并非目标网站后台的Node.js代码。许多网站通过分层复杂性来应对遗留问题,使得获取登录信息变得极其困难。尽管尝试结合request库进行多次抓取,但仍未完全解决所有问题。
引入PhantomJS解决动态渲染问题
在尝试多次后,我们发现了node-phantomjs库。该库允许Node.js控制PhantomJS无头WebKit浏览器(即后台渲染页面,无需显示设备)。虽然看似简单,但仍需解决PhantomJS无法回避的几个问题:
PhantomJS实施中的关键难点与对策
1. PhantomJS仅能告知页面是否加载完成,无法确定是否存在由JavaScript或meta标签触发的重定向(redirect),尤其是当JavaScript使用setTimeout()延迟调用时。
2. PhantomJS提供了页面加载开始(pageLoadStarted)的钩子,可用于处理上述重定向问题。但这要求开发者预先确定要加载的页面数量,在每页加载完成后递减该数字,并为可能的超时提供处理机制。当数字减为0时调用回调函数。这种方式虽能工作,但感觉更像是一种黑客手段。
3. PhantomJS为每个抓取的页面需要一个完整独立的进程,否则无法隔离各页面间的Cookies。若使用同一个PhantomJS进程,已登录页面的会话Cookie会被发送到另一个页面,导致状态混乱。
4. PhantomJS无法直接下载资源,仅能将页面保存为PNG或PDF格式。因此,我们仍需借助request库来下载PDF文件。
5. 鉴于上述原因,必须找到一种将PhantomJS会话中的Cookie分发到request库会话中的方法。具体做法是将document.cookie字符串提取出来,解析后注入到request的cookie jar中。
6. 将变量注入浏览器会话并非易事。为此,我们需要创建一个字符串来构建一个JavaScript函数。
数据注入与DOM交互实现
代码如下:
Robot.prototype.add_page_data = function (page, name, data) {
page.evaluate(
"function () { var " + name + " = window." + name + " = " + JSON.stringify(data) + "}"
);
}7. 部分网站充斥着console.log()等调试代码,需要将其重新定义并输出到指定位置。实现方式如下:
控制台日志重定向处理
代码如下:
if (!console.log) {
var iframe = document.createElement("iframe");
document.body.appendChild(iframe);
console = window.frames[0].console;
}
8. 部分网站充斥着console.log()等调试代码,需要将其重新定义并输出到指定位置。实现方式如下:
代码如下:
if (!console.log) {
var iframe = document.createElement("iframe");
document.body.appendChild(iframe);
console = window.frames[0].console;
}9. 告知浏览器点击a标签同样不易,为此我们加入了以下代码:
模拟鼠标点击事件
代码如下:
var clickElement = window.clickElement = function (id){
var a = document.getElementById(id);
var e = document.createEvent("MouseEvents");
e.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
a.dispatchEvent(e);
};
会话管理与最终方案总结
10. 我们还需要限制浏览器会话的最大并发量,以防止服务器过载。尽管有此限制,该方案的并发处理能力仍远高于昂贵的商业解决方案。
完成所有工作后,我们得到了一个较为完善的PhantomJS + Request爬虫解决方案。必须使用PhantomJS进行登录,然后返回使用Request库请求,后者将使用在PhantomJS中设置的Cookie来验证登录会话。这是一个重大突破,因为我们现在可以利用Request库的流功能来下载PDF文件。
整体计划旨在让Web开发者相对容易地理解如何使用jQuery和CSS选择器创建针对不同网站的爬虫。虽然尚未完全证明该思路在所有场景下的可行性,但相信很快能够实现。