蜘蛛程序可以从网站上自动提取资料,从而极大地节省人力,所以现在蜘蛛程序现在用得非常多。不过一般的蜘蛛程序由于计算量比较大,通常用C#加上HTML分析库来做。而对于简单的任务,例如从成百上千页的列表中提取数据,那么用PHP来实现更简单易行。

用PHP编写的蜘蛛程序还有一个好处,就是服务器能起到优质代理服务器的作用,因为是通过服务器抓取目标网站的。一般托管主机都拥有优秀的网络接入质量,这对抓取速度有很大帮助。如果你拥有一个主机,特别是国外主机的话,这个好处嘛……你懂的。

以一个从数百页的列表中提取出需要的数据的任务为例,这像是一个反过程,将网页上的列表还原成数据库表。程序主要有以下工作要做:

  1. 获得或生成URL
  2. 下载目标页面内容
  3. 分析页面、提取数据:
  4. 将提取到的数据写入数据库或文件

我的思路是将这个蜘蛛程序的控制功能放在客户端,而具体执行任务的功能放在服务器端。由客户端的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 ParserphpQuery 等可供使用。

3. 将数据写入数据库
4. 返回运行情况

用JSON返回运行情况,主要是下载是否成功、现在已提取到多少条数据等。供客户端显示。

剩下的工作便是测试和改正任何一个可能出错的地方。因为客户端和服务器端之间通过网络进行通信、服务器端也通过网络下载目标页面,所以会有一些不确定性。例如,服务器无法下载到目标页面时应如何处理、服务器端没有相应客户端的请求时应如何处理等。需要把可能出现的问题考虑周全,才能做到程序稳定运行直到抓取完成。