Perl 语言的网络爬虫
网络爬虫机器人,或称蜘蛛程序,在互联网用户中具有一定的神秘感。我们都使用像 Lycos 和 Infoseek 这样的搜索引擎来查找互联网上的资源,而这些引擎使用蜘蛛程序来收集它们呈现给我们的信息。然而,我们中很少有人直接使用蜘蛛程序。
蜘蛛程序是网络应用程序,它们遍历 Web,积累关于所发现内容的统计信息。那么网络蜘蛛是如何工作的呢?其算法很简单:
创建一个待搜索 URL 的队列,从一个或多个已知的 URL 开始。
从队列中取出一个 URL,并获取可以在该位置找到的超文本标记语言 (HTML) 页面。
扫描 HTML 页面,查找新发现的超链接。将找到的任何超链接的 URL 添加到 URL 队列中。
如果队列中还有 URL,则转到步骤 2。
列表 1 是一个名为 spider.pl 的程序,它用 Perl 实现了上述算法。该程序应在任何安装了 Perl 版本 4 或更高版本的 Linux 系统上运行。请注意,本文中提到的所有代码都假定 Perl 安装在 /usr/bin/Perl 中。这些脚本可以在我的网页 http://www.javanet.com/~thomas/ 上下载。
要在 shell 提示符下运行蜘蛛程序,请使用命令
spider.pl <starting-URL<search-phrase>
蜘蛛程序将开始搜索。起始 URL 必须完全指定,否则可能无法正确解析。蜘蛛程序搜索初始页面及其所有后代页面以查找给定的搜索短语。任何包含匹配项的页面的 URL 都会被打印出来。要打印出来自 SSC 站点且包含短语 “Linux Journal” 的 URL 列表,请输入
spider.pl http://www.ssc.com/ "Linux Journal"Perl 变量 $DEBUG 在 spider.pl 的前几行中定义,用于控制蜘蛛程序产生的输出量。$DEBUG 的范围可以从 0(打印匹配的 URL)到 2(输出程序状态和内部数据结构的转储)。
关于蜘蛛程序最有趣的事情是它是一个网络程序。子程序 get_http() 封装了实现蜘蛛程序所需的所有网络编程;它执行上述算法步骤 2 中提到的 “fetch”(获取)操作。此子程序打开一个到服务器的套接字,并使用 HTTP 协议来检索页面。如果服务器附加了端口号,则使用此端口建立连接;否则,使用众所周知的端口 80。
一旦与远程机器建立连接,get_http() 将发送一个字符串,例如
GET /index.html HTTP/1.0
此字符串后跟两个换行符。这是超文本传输协议 (HTTP) 的一个片段,Web 就是基于该协议的。此请求要求我们连接到的 Web 服务器将文件 /index.html 的内容发送给我们。然后 get_http() 读取套接字,直到遇到文件结尾。由于 HTTP 是无连接协议,因此对话仅限于此。我们提交一个请求,Web 服务器发送一个响应,连接终止。
来自 Web 服务器的响应由标头(如 HTTP 标准所指定)和构成页面的 HTML 标记文本组成。响应的这两个部分用一个空行分隔。在调试级别 2 运行蜘蛛程序将显示页面被获取时的 HTTP 标头。以下是来自 Web 服务器的典型响应。
HTTP/1.0 200 OK Date: Tue, 11 Feb 1997 21:54:05 GMT Server: Apache/1.0.5 Content-type: text/html Content-length: 79 Last-modified: Fri, 22 Nov 1996 10:11:48 GMT <HTML><TITLE>My Web Page</TITLE> <BODY> This is my web page. </BODY> </HTML>
蜘蛛程序在其到达时检查 HTTP 标头中的 Content-type 字段。如果内容是除 text/html 或 text/plain 之外的任何 MIME 类型,则下载将被中止。这避免了耗时的下载,例如 .Z 和 .tar.gz 文件,我们不希望搜索这些文件。虽然大多数站点使用 FTP 协议来传输此类文件,但越来越多的站点正在使用 HTTP。
如果您在 SPARC 或 Alpha 上运行 Linux,则应该注意 get_http() 中的硬件依赖性。在构建套接字的网络地址时,Perl pack() 例程用于编码整数数据。行
$sockaddr="S n a4 x8";
仅适用于 32 位 CPU。为了解决这个问题,请参阅 Mike Mull 在 LJ 第 35 期中的文章 “Perl and Sockets”。
一旦蜘蛛程序下载了网页的 HTML 源代码,我们就可以扫描它以查找与搜索短语匹配的文本,并在找到匹配项时通知用户。
我们还可以找到嵌入在页面中的任何超文本链接,并将它们用作进一步搜索的起点。这正是蜘蛛程序所做的;它扫描 HTML 内容以查找 <A HREF="url"> 形式的锚标记,并将找到的任何链接添加到其 URL 队列中。
HTML 页面中的超链接可以有多种形式。其中一些必须与嵌入它们的页面的 URL 组合才能获得完整的 URL。这由 fqURL() 函数完成。它组合当前页面的 URL 和在该页面中找到的超链接的 URL,以生成超链接的完整 URL。
例如,以下是一些可能在 http://www.ddd.com/clients/index.html 的虚构网页中找到的链接,以及 fqURL() 生成的最终 URL。
锚标记中的 URL | 最终 URL |
http://www.eee.org/index.html | http://www.eee.org/index.html |
att.html | http://www.ddd.com/clients/att.html |
/att.html | http://www.ddd.com/att.html |
正如这些示例所示,蜘蛛程序可以处理完全指定的 URL 和仅包含文档名称的 URL。当只给出文档名称时,它可以是完全限定的路径或相对路径。此外,蜘蛛程序可以处理嵌入端口号的 URL,例如 http://www.ddd.com:1234/index.html。
fqURL() 中未实现的一个功能是从 URL 中剥离后向引用 (../)。理想情况下,URL /test/.../index.html 会被转换为 /index.html,我们知道两者都指向相同的文档。
一旦我们获得了超链接的完全指定的 URL,我们就可以将其添加到我们的待扫描 URL 队列中。出现的一个问题是如何将我们的搜索限制在互联网的给定子集内。不受限制的搜索最终会下载很大一部分全球互联网内容——这不是我们希望对与我们共享网络带宽的伙伴做的事情。 spider.pl 采取的方法是丢弃任何与起始 URL 具有不同主机名的 URL;因此,蜘蛛程序被限制在单个主机内。我们还可以扩展程序以指定一组合法主机,从而允许搜索一小群服务器的内容。
处理我们找到的链接时出现的另一个问题是如何防止蜘蛛程序陷入循环。循环超链接在 Web 上非常常见。例如,页面 A 有一个指向页面 B 的链接,页面 B 有一个返回页面 A 的链接。如果我们将蜘蛛程序指向页面 A,它会找到指向 B 的链接并检查它。在 B 上,它找到一个指向 A 的链接并检查它。此循环无限期地继续下去。避免陷入循环的最简单方法是跟踪蜘蛛程序去过的地方,并确保它不会返回。本文开头所示算法中的步骤 2 建议我们 “从队列中取出一个 URL” 并访问它。蜘蛛程序不会从队列中删除 URL。相反,它将该 URL 标记为已扫描。如果蜘蛛程序稍后找到指向此 URL 的超链接,它可以忽略它,因为它已经访问过该页面。我们的 URL 队列同时包含已访问和未访问的 URL。
蜘蛛程序已访问的页面集将稳步增长,而尚未访问的页面集可能会快速增长和缩小,具体取决于每个页面中找到的超链接数量。如果要遍历大型站点,您可能需要将 URL 队列存储在数据库中,而不是像我们在这里所做的那样存储在内存中。保存 URL 队列的关联数组 %URLqueue 可以很容易地与 GDBM 数据库链接,使用 Perl 4 函数 dbmopen() 和 dbmclose() 或 Perl 5 函数 tie() 和 untie()。
请注意,您不应在整个互联网上释放这个野兽,不仅因为带宽消耗,还因为互联网惯例。蜘蛛程序发送的文档请求是一行 GET 请求。为了严格遵循 HTTP 协议,它还应包括 User-Agent 和 From 字段,让远程服务器有机会拒绝我们的请求和/或收集统计信息。
此程序还忽略了 “robots.txt” 约定,该约定由管理员用于拒绝机器人访问。在进一步扫描主机之前,应检查文件 /robots.txt。此文件指示是否欢迎来自机器人的扫描,并声明任何禁止访问的子目录。一个 robots.txt 文件,仅排除扫描 2 个目录,如下所示:
Useuagent: * Disallow: /tmp/ Disallow: /cgi-bin/
一个禁止在特定 Web 服务器上进行所有扫描的文件如下所示:
User-agent: * Disallow: /像我们的蜘蛛程序这样的机器人可能会给 Web 服务器带来沉重的负载,我们不希望在已被其管理员声明为禁止机器人访问的服务器上使用它。
除了作为一种好奇心之外,我们如何使用蜘蛛程序呢?该程序的一种用途是替代网站索引和查询程序,如 Harvest (http://harvest.cs.colorado.edu/Harvest/) 或 Excite for Web Servers (http://www.excite.com/navigate/prodinfo.html)。这些程序很大且复杂。它们通常提供 Perl 蜘蛛程序的功能、存档检索文本的方法以及针对结果数据库运行的 CGI 查询引擎。需要持续维护,因为查询引擎针对数据库而不是实际站点内容运行;因此,每当站点内容发生更改时,都必须重新生成数据库。
一些搜索引擎,例如 Excite for Web Servers,无法索引远程站点的内容。这些引擎从构成网站的文件而不是从跨网络检索的数据构建其数据库。如果您有两个网站的内容要出现在单个搜索应用程序中,则这些工具将不适用。此外,Linux 版本的 Excite for Web Servers 仍处于 “即将推出” 阶段。
列表 2 和 列表 3 展示了一个使用 spider.pl 程序实现的简单 CGI 搜索引擎。列表 2 是一个 HTML 表单,它调用 spiderfind.cgi 来处理其输入。列表 3 是 spiderfind.cgi。它首先使用 Brigitte Jellinek 的库将表单中输入的数据移动到关联数组中。然后,它使用 Perl system() 函数调用 spider.pl 程序,并将表单数据作为参数传递。最后,它将 spider.pl 的输出转换为一系列 HTML 链接。用户的浏览器将显示一个超链接 URL 列表,其中找到了搜索文本。请注意,要搜索的主机名由 HTML 文档中的隐藏字段指定。Perl 程序之间进行交互的方式比通过 Perl system() 调用更好且更安全,但我希望在此演示中使用 spider.pl 的未修改副本。
此脚本不提供上述软件包的完整功能,并且性能也不如它们。由于我们正在针对网络上的 Web 服务器文档进行搜索,因此我们没有索引文件的优势;因此,搜索速度会更慢,并且处理器密集程度更高。但是,此脚本易于安装,并且比那些引擎更易于维护。
可以使用 spider.pl 程序构建的另一个应用程序是 Web 的断开链接扫描器。我们之前展示的 HTTP 响应以 “HTTP/1.0 200 OK” 行开始,表明请求可以被满足。如果我们尝试访问一个不存在文档的 URL,我们将得到 “HTTP/1.0 404 Not found” 行作为替代。我们可以将其用作文档不存在的指示,并打印引用此页面的 URL。
完成此操作所需的对蜘蛛程序的修改很小。每次将超链接的 URL 添加到 URL 队列时,我们还会记录在其中找到超链接的文档的 URL。然后,当蜘蛛程序检查超链接并收到 “404 Not found” 响应时,它会输出引用页面的 URL。
