蜘蛛程序可以从网站上自动提取资料,从而极大地节省人力,所以现在蜘蛛程序现在用得非常多。不过一般的蜘蛛程序由于计算量比较大,通常用C#加上HTML分析库来做。而对于简单的任务,例如从成百上千页的列表中提取数据,那么用PHP来实现更简单易行。
用PHP编写的蜘蛛程序还有一个好处,就是服务器能起到优质代理服务器的作用,因为是通过服务器抓取目标网站的。一般托管主机都拥有优秀的网络接入质量,这对抓取速度有很大帮助。如果你拥有一个主机,特别是国外主机的话,这个好处嘛……你懂的。
以一个从数百页的列表中提取出需要的数据的任务为例,这像是一个反过程,将网页上的列表还原成数据库表。程序主要有以下工作要做:
- 获得或生成URL
- 下载目标页面内容
- 分析页面、提取数据:
- 将提取到的数据写入数据库或文件
我的思路是将这个蜘蛛程序的控制功能放在客户端,而具体执行任务的功能放在服务器端。由客户端的AJAX来监视和控制服务器端的运行。
客户端的工作
1. 生成URL
通过分析页面,或从提取到的页面中取得链接,可以确定下一页的URL。例如”www.ex1.com/list.php?page=123”或”www.ex2.com/list-123.html”。由JavaScript生成要提取的URL,并发送给服务器端。服务器端一次只负责处理一个页面。
2. 存放和展示程序运行状态
用户可以知道现在正在抓取哪一页、抓取到了多少条数据、失败重试的次数。如果用户暂停程序,下次还可以继续抓取。
3. 控制程序运行
通过AJAX命令服务器端执行提取、分析和储存工作。
在本例中,为了提高程序运行的稳定性,没有采用多线程设计,一次只请求一个页面。所以定义了两个函数:fnRun()
和 fnGet()
。
fnRun()
是”启动/暂停”按钮的事件处理函数。它负责切换程序的运行和暂停状态,这是通过设置一个标志位 bRunning
来实现的。
var bRunning = 0, nRetry = 0, page = 500;
function fnRun() {
// 当前处于停止状态
if (bRunning == 0) {
bRunning = 1;
$('#btn').val(' 暂停 '); // 更改按钮文字
fnGet();
}
// 当前处于运行状态
else {
bRunning = 0;
$('#btn').val(' 继续 ');
}
}
fnGet()
则负责生成URL并创建AJAX请求,当AJAX请求完成(服务器端完成了工作)后,fnGet()
递归调用,继续处理下一页。如果 fnGet()
遇到 bRunning==0
也就是用户要求暂停时,则停止递归调用。
function fnGet() {
if (bRunning == 0) return 0;
if (page < 1) {
$('#btn').val(' 完成 ').attr('disabled', true);
alert('祝贺您!抓取成功完成!');
return 0;
}
$('#cur_page').html(page);
$.ajax({
type: "GET",
url: "count.php",
data: "page=" + page–,
beforeSend: function(jqXHR, settings) {
$('#busy').show(); // 告诉用户正在进行AJAX请求
},
success: function(data, textStatus, jqXHR) {
$('#busy').hide(); // 告诉用户AJAX请求已完成
try {
var json = eval('('+ data +')');
} catch (oException) {
// 网络繁忙,页面抓取有问题的处理
var json = new Object();
json.msg = [];
json.msg[0] = 'ERROR';
json.msg[1] = data;
}
if (json.msg[0] == 'S') {
// 成功获取,更新抓取到的数据条数
$('#cur_num').html(json.num);
fnGet();
}
else {
page++; // 要重试这一页,所以页码先加1
nRetry++; // 重试数加1
$('#cur_retry').html(nRetry);
fnGet();
}
},
error: function(jqXHR, textStatus, errorThrown) {
// 与服务器端的通信出错
// TODO: 错误处理
}
});
}
4. 多线程控制
如果感到服务器仍有余力,可以在客户端同时创建多个AJAX请求,创建一个类来管理它们。
服务器端的工作:
1. 下载目标页面
通过 file_get_contents() 下载页面。以GET方式发送的参数只要加在URL后面就行了,但有时需要以POST方式发送,就要用 stream_context_create() 做一个上下文:
$url = 'http://www.ex3.com/list.php';
$page = (int)$_GET['page'];
$query = array('page' => $page);
$context['http'] = array(
'method' => 'POST',
'content' => http_build_query($query),
);
$text = file_get_contents($url, false, stream_context_create($context));
如果遇到目标网站用Cookie识别身份,或者检查 Referer 和 User Agent。可以进一步修改上下文,例如:
$context['http'] = array(
'method' => 'POST',
'header' => 'Referer: http://www.ex3.com/index.htmlrn' .
'Cookie: foo=barrn',
'content' => http_build_query($query),
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
);
2. 提取所需数据
简单的采集任务不需要分析HTML,只要用正则表达式找到需要的数据即可。因为动态网页是将数据填入一个模板来生成的,可以通过分析页面的结构找出合适的正则表达式。也可以通过正则表达式搜索 <a>
、<img>
等标签。
编码问题需要注意。当你的正则表达式中含有中文等双字节字符时,注意正则表达式的编码要和抓取到的内容编码相符,否则永远也无法匹配。例如:
$ntd = iconv('UTF-8′, 'BIG5′, '新台幣'); // 有些字不能正确转换造成出错
$ntd = mb_convert_encoding('新台幣', 'BIG5′); // 需要 mbstring 库
preg_match_all("/(d+){$ntd}", $text, $matches);
如果需要分析HTML页面,还有 PHP Simple HTML DOM Parser、phpQuery 等可供使用。
3. 将数据写入数据库
4. 返回运行情况
用JSON返回运行情况,主要是下载是否成功、现在已提取到多少条数据等。供客户端显示。
剩下的工作便是测试和改正任何一个可能出错的地方。因为客户端和服务器端之间通过网络进行通信、服务器端也通过网络下载目标页面,所以会有一些不确定性。例如,服务器无法下载到目标页面时应如何处理、服务器端没有相应客户端的请求时应如何处理等。需要把可能出现的问题考虑周全,才能做到程序稳定运行直到抓取完成。