使用 LWP
大多数时候,本专栏讨论的是我们如何改进或自定义 Web 服务器所做的工作。 无论是使用 CGI 程序还是 mod_perl 模块,我们通常都是从服务器的角度来看待问题。
本月,我们将研究 LWP,即 Perl 的“Web 编程库”,以及几个相关的模块。 我们将编写的程序将是 Web 客户端,而不是 Web 服务器。 服务器端程序接收 HTTP 请求并生成 HTTP 响应; 本月我们的程序将生成请求并等待服务器生成的响应。
当我们研究这些模块时,我们将更好地理解 HTTP 的工作原理,以及如何使用 LWP 的各种模块来构建各种程序,以检索和整理存储在 Web 上的信息。
HTTP,即“超文本传输协议”,使 Web 成为可能。 HTTP 是互联网上使用的众多协议之一,被认为是与 SMTP(简单邮件传输协议)和 FTP(文件传输协议)并列的高级协议。 这些之所以被认为是高级协议,是因为它们建立在处理更平凡的网络方面的低级协议的基础之上。 HTTP 消息不必担心处理丢包和路由,因为 TCP 和 IP 会处理这些事情。 如果出现问题,将在较低级别处理。
以这种方式划分问题可以让您专注于重要问题,而不会被细枝末节分散注意力。 如果您每次想开车去某个地方都必须考虑汽车的内部结构,您很快就会发现自己一次专注于太多事情而无法完成手头的任务。 同样,HTTP 和其他高级协议可以忽略网络如何运行的低级细节,而只需假设两台计算机之间的连接将按预期工作。
HTTP 基于客户端-服务器模型运行,其中发出请求的计算机称为客户端,而接收请求并发出响应的计算机称为服务器。 在 HTTP 的世界中,服务器在被告知之前永远不会说话——而且它们总是拥有最终决定权。 这意味着客户端的请求永远不能依赖于服务器的响应; 有兴趣使用之前的响应来形成新请求的客户端必须打开新连接。
考虑到所有这些理论,HTTP 在实践中是如何工作的呢? 您可以使用简单的 telnet 命令自行进行实验。 telnet 通常用于远程访问另一台计算机,方法是输入
telnet remotehost
这演示了默认行为,其中 telnet 打开到端口 23 的连接,这是此类访问的标准端口。 您也可以使用 telnet 连接到其他端口,如果那里有服务器在运行,您甚至可以与其通信。
由于 HTTP 服务器通常在端口 80 上运行,因此我可以使用以下命令连接到一个服务器
telnet www.lerner.co.il 80
我在我的 Linux 机器上得到以下响应
Trying 209.239.47.145... Connected to www.lerner.co.il. Escape character is '^]'.一旦我们建立了这种连接,就轮到我说话了。 在这种情况下,我是客户端,这意味着我必须先发出请求,服务器才会发出任何响应。 HTTP 请求至少包含一个方法、一个应用该方法的对象和一个 HTTP 版本号。 例如,我们可以通过键入以下内容来检索 / 处文件的内容
GET / HTTP/1.0这表明我们希望将 / 处的文件返回给我们,并且我们可以处理的最高编号的 HTTP 版本是 HTTP/1.0。 如果我们表明我们支持 HTTP/1.1,高级服务器会以同样的方式响应,从而使我们能够执行各种巧妙的技巧。
如果您在发出上述命令后按了 return 键,您可能仍在等待接收响应。 这是因为 HTTP/1.0 引入了“请求头”的概念,即客户端可以作为请求的一部分传递给服务器的额外信息。 这些客户端标头可以包括 Cookie、语言首选项、此客户端上次访问的 URL(“referer”)以及许多其他信息。
因为我们将坚持使用简单的 GET 请求,所以在我们的单行命令后按两次 return 键:一次结束请求的第一行,另一次指示我们没有更多内容要发送。 与电子邮件消息一样,空行将标头(有关消息的信息)与消息本身分隔开。
第二次键入 return 后,您应该会看到 http://www.lerner.co.il/ 的内容返回给您。 一旦文档传输到您的终端,连接就会终止。 如果您想再次连接到同一服务器,您可以这样做。 但是,您必须建立新连接并发出新请求。
正如客户端可以在请求本身之前发送请求标头一样,服务器也可以在响应之前发送响应标头。 与请求标头的情况一样,响应标头和响应正文之间必须有一个空行。
以下是我在发出上述 GET 请求后收到的标头
HTTP/1.1 200 OK Date: Thu, 12 Aug 1999 19:36:44 GMT Server: Apache/1.3.6 (UNIX) PHP/3.0.11 FrontPage/3.0.4.2 Rewrit/1.0a Connection: close Content-Type: text/html
以上几行是响应的典型内容。
第一行产生有关响应的一般信息,包括即将发生的事情的指示。 首先,服务器告诉我们它可以处理任何高达 HTTP/1.1 的内容。 如果我们想使用 HTTP/1.1 发送请求,此服务器将允许这样做。 在 HTTP 版本号之后是响应代码。 此代码可以指示各种可能性,包括一切是否正常(200)、文件是否已永久移动(301)、文件是否未找到(404)或服务器端是否发生错误(501)。
数字代码通常后跟文本消息,该消息指示数字背后的含义。 Apache 和其他服务器可能允许我们自定义发生错误时显示的页面,但该自定义不扩展到此错误代码,该代码是标准且固定的。
错误代码之后是生成响应的日期。 此标头对于代理和缓存非常有用,它们可以存储文档的日期及其内容。 下次您的浏览器尝试检索文件时,它将比较先前响应中的 Date: 标头,仅当服务器的版本较新时才检索新版本。
服务器在 Server: 标头中标识自身。 在这种特殊情况下,服务器不仅告诉我们它是 Apache 1.3.6,运行在某种形式的 UNIX(在本例中为 Linux)下,还告诉我们一些已安装的模块。 我的 Web 空间提供商选择安装 PHP、FrontPage 和 Rewrit; 正如我们在前几个月看到的那样,mod_perl 是另一个流行的服务器端编程模块,它在此标头中宣传自己。
正如我们所见,HTTP 连接在服务器完成发送其响应后终止。 这可能非常低效; 考虑一个包含五个 IMG 标签的 HTML 页面,指示应加载图像的位置。 为了完整下载此页面,Web 浏览器必须创建六个单独的 HTTP 连接——HTML 一个,每个图像各一个。 为了克服这种低效率,HTTP/1.1 允许“持久连接”,这意味着可以在单个 HTTP 事务中检索多个文档。 这通过 Connection 标头发出信号,该标头指示它已准备好在单个事务后关闭连接(在上面的示例中)。
上面输出中的最后一个标头是 Content-type,CGI 程序员非常熟悉。 此标头使用 MIME 样式描述来告诉浏览器期望的内容类型。 它应该期望 HTML 格式的文本 (text/html) 吗? 还是 JPEG 图像 (image/jpeg)? 或者某些无法识别的东西,应将其视为二进制数据 (application/octet-stream)? 如果没有这样的标头,您的浏览器将不知道如何处理它接收到的数据,这就是为什么服务器在 Content-type 丢失时经常产生错误消息的原因。
HTTP/1.0 支持许多 GET 以外的方法,但主要方法是 GET、HEAD 和 POST。 GET,顾名思义,允许我们检索链接的内容。 这是最常用的方法,是您的 Web 浏览器执行的大多数简单检索的幕后功臣。 HEAD 与 GET 相同,但在打印响应标头后退出。 发送以下请求
HEAD / HTTP/1.0
是测试您的 Web 服务器并查看其是否正常运行的好方法。
POST 不仅命名服务器计算机上的路径,还以名称-值对的形式发送输入。 (GET 也可以以名称-值对的形式提交信息,但在大多数情况下,它被认为不太理想。)POST 通常在用户单击 HTML 表单中的“提交”按钮时调用。
现在我们已经了解了 HTTP 背后的基础知识,让我们看看如何使用 Perl 处理请求和响应。 幸运的是,LWP 包含几乎所有我们可能想要执行的操作的对象,代码经过了许多人的测试。
如果我们只是想使用 HTTP 检索文档,我们可以使用 LWP::Simple 模块。 例如,这是一个简单的 Perl 程序,用于从我的网站检索根文档
#!/usr/bin/perl --w use strict; use diagnostics; use LWP::Simple; # Get the contents my $content = get "http://www.lerner.co.il/"; # Print the contents print $content, "\n";
在这种特殊情况下,启动和诊断代码比程序本身更长。 将 LWP::Simple 导入我们的程序会自动引入 get 函数,该函数接受 URL,使用 GET 检索其内容,并返回响应的正文。 在此示例中,我们将该输出打印到屏幕上。
一旦文档的内容存储在 $content 中,我们就可以将其视为普通的 Perl 标量,尽管它包含相当多的文本。 此时,我们可以搜索感兴趣的文本,对 $content 执行搜索和替换操作,删除我们认为冒犯性的任何部分,甚至将部分内容翻译成 Pig Latin。 例如,这个简单程序的以下变体将内容颠倒过来,反转每一行,使最后一行变成第一行,反之亦然; 以及每行上的每个字符,使最后一个字符变成第一个字符,反之亦然
#!/usr/bin/perl -w use strict; use diagnostics; use LWP::Simple; # Get the contents my $content = get "http://www.lerner.co.il/"; # Print the contents print scalar reverse $content, "\n";
请注意,我们必须将 reverse 放在标量上下文中才能使其正常工作。 由于 print 接受参数列表,因此我们使用 scalar 关键字强制标量上下文。
但是,有时我们希望创建更复杂的应用程序,这反过来又需要更复杂的方式来联系服务器。 这样做将需要许多不同的对象,每个对象都有自己的任务。
首先,我们将必须创建一个 HTTP::Request 对象。 正如您可以从其名称中猜到的那样,此对象处理与 HTTP 请求有关的所有事情。 我们可以通过以下方式最容易地创建它
use HTTP::Request; my $request = new HTTP::Request("GET", "http://www.lerner.co.il");
其中第一个参数指示我们希望使用的请求方法,第二个参数指示目标 URL。
创建 HTTP 请求后,我们需要将其发送到服务器。 我们通过一个“useragent”对象来完成此操作,该对象充当此交换中的中间人。 我们已经在上面的示例程序中查看了 LWP::Simple useragent。
通常,useragent 接受一个 HTTP::Request 对象作为参数,并返回一个 HTTP::Response 对象。 换句话说,给定上面定义的 $request,我们的下一步将是以下两个步骤
my $ua = new LWP::UserAgent; my $response = $ua->request($request);
在我们创建 HTTP::Response 并将其分配给 $response 之后,我们可以执行各种测试和操作。
对于初学者,我们可能想知道作为响应一部分收到的响应代码,以确保我们的请求成功。 我们可以使用 code 和 message 方法获取响应代码和随附的文本消息
my $response_code = $response->code; my $response_message = $response->message;
如果我们然后说
print qq{Code: "$code"\n}; print qq{Message: "$message"\n};我们将得到输出
Code: "200" Message: "OK"这很好,但这带来了一个问题:我们如何知道如何对不同的响应代码做出反应? 我们知道 200 表示一切正常,但我们必须建立一个值表才能知道哪些响应代码表示我们可以继续,哪些表示程序应该退出并发出错误信号。
HTTP::Response 的 is_success 方法为我们处理了这个问题。 有了它,我们可以轻松检查我们的请求是否已通过以及我们是否收到了响应
if (!$response->is_success) {print "Success. \n";} else {print "Error: " . $response->status_line . "\n"; }
status_line 方法结合了代码和消息的输出,以生成数字响应代码及其打印描述。
我们可以使用 headers 方法检查响应标头。 这将返回 HTTP::Headers 的实例,该实例提供了许多方便的方法,使我们能够检索单个标头值
my $headers = $response->headers; print "Content-type:", $headers->content_type, "\n"; print "Content—length:", $headers->content_length, "\n"; print "Date:", $headers->date, "\n print "Server:", $headers->server, "\n";
当然,如果没有我们检索到的文档的内容,Web 就不是很有用。 HTTP::Response 只有一个方法用于检索响应的内容,不出所料地命名为 content。 因此我们可以说
my $content = $response->content;
此时,我们回到了我们之前使用 LWP::Simple 示例时的状态:我们有文档的内容在 $content 中,它将文本存储为普通字符串。
如果我们有兴趣使用 HTTP::Request 和 HTTP::Response 来反转文档,我们可以按照列表 1 中所示的方式进行操作。 如果您真的有兴趣制作像这样的程序,您可能会坚持使用 LWP::Simple 并使用那里描述的 get 方法。 如果目的只是从 URL 检索内容,则没有必要加重程序的负担,以及添加各种方法调用。
使用更复杂的 user agent 的优势在于它提供的额外灵活性。 这种灵活性是否值得在复杂性方面做出权衡将取决于您的需求。
例如,许多站点在其根目录中都有一个 robots.txt。 这样的文件告诉“机器人”或软件控制的 Web 客户端,站点的哪些部分应被视为超出范围。 这些文件是惯例问题,但却是应该遵循的长期传统。 幸运的是,LWP 包含对象 LWP::RobotUA,它是一个 user agent,可在从网站检索文件之前自动检查 robots.txt 文件。 如果文件被 robots.txt 排除,LWP::RobotUA 将不会检索它。
LWP::RobotUA 与 LWP::UserAgent 的不同之处还在于它试图对服务器保持友好,即每分钟仅发送一个请求。 我们可以使用 delay 方法更改此设置,但仅当您熟悉站点及其处理大量自动生成请求的能力时,才建议这样做。
一旦我们从网站检索到内容,我们该如何处理它呢? 如上所示,我们可以将其打印出来或处理文本。 但是很多时候,我们想分析文档中的标签,挑选出图像、超链接甚至标题。
为了做到这一点,我们可以使用正则表达式和 m//,Perl 的匹配运算符。 但是更简单的方法是使用 HTML::LinkExtor,这是另一个为此目的设计的对象。 一旦我们创建了 HTML::Extor 的实例,我们就可以使用 parse 方法来检索其中的每个标签。
HTML::LinkExtor 的工作方式与您之前可能使用过的许多模块不同,因为它使用了“回调”。 在这种情况下,回调是一个子例程,定义为接受两个参数——一个标量,其中包含标签的名称,以及一个哈希,其中包含与该标签关联的名称-值对。 每次 HTML::LinkExtor 找到标签时,都会调用该子例程。
例如,给定 HTML
<input type="text" value="Reuven" name="first_name" size="5">
我们的回调必须准备好处理值为 input 的标量,以及看起来像这样的哈希
(type => "text", value => "Reuven", name => "first_name", size => "5")列表 2
如果我们有兴趣将各种 HTML 标签打印到屏幕上,我们可以编写一个简单的回调,如列表 2 所示。 我们如何告诉 HTML::LinkExtor 每次找到匹配项时都调用我们的回调子例程? 最简单的方法是将回调作为参数传递给 parse 方法。
Perl 允许我们像传递数据一样传递子例程和其他代码块,方法是创建对它们的引用。 引用看起来和行为都像标量,除了它可以变成其他东西。 Perl 具有标量、数组和哈希引用; 子例程引用也很自然地融入了这张图中。 HTML::LinkExtor 将解引用并使用我们定义的子例程。
我们通过在子例程名称前加上 \& 将子例程转换为子例程引用。 Perl 5 不再要求您在子例程名称前放置 &,但是当您传递子例程引用时,这是必需的。 反斜杠告诉 Perl 我们想将所讨论的对象转换为引用。 如果 &callback 定义如上,那么我们可以使用以下命令打印文档中的所有链接
my $parser = HTML::LinkExtor->new(\&callback); $parser->parse($response->content);
请注意,$content 可能具有 HTTP 响应返回的所有 HTML 链接。 但是,该响应无疑包含一些相对 URL,这些 URL 在上下文之外将无法正确解释。 我们如何准确地查看链接?
HTML::LinkExtor 考虑到了这一点,并允许我们向其构造函数 (new) 传递两个参数,而不仅仅是一个参数。 第二个参数(可选)是我们从中收到此内容的 URL。 传递此 URL 可确保我们提取的所有 URL 都是完整的。 如果我们要使用此功能,则必须在我们的应用程序中包含以下行
use URI::URL;
然后我们可以说
my $parser = HTML::LinkExtor->new(\&callback, "http://www.lerner.co.il/"); $parser->parse($response->content);并且我们的回调将为每个标签调用,即使文档包含相对 URL,也会使用完整、绝对的 URL。
我们上面的 &callback 版本打印出所有链接,而不仅仅是超链接。 我们可以忽略除“锚点”标签之外的所有标签,这些标签允许我们通过稍微修改 &callback 来创建超链接,如列表 3 所示。
有了以上所有知识,我们将编写一个应用程序(列表 4),该应用程序递归地跟踪链接,直到程序停止。 这种程序对于检查您站点上的链接或从文档中收集信息非常有用。
我们的程序 download-recursively.pl 从名为 $origin 的 URL 开始,并收集其中包含的 URL,将它们放入哈希 %to_be_retrieved 中。 然后它逐个遍历这些 URL,收集其中可能包含的任何超链接。 每次它检索 URL 时,download-recursively.pl 都会将其放入 %already_retrieved 中。 这确保了我们不会下载相同的 URL 两次。
我们在“while”循环外部创建 $ua,即 LWP::RobotUA 的实例。 毕竟,我们的 HTTP 请求和响应会随着每次循环迭代而变化,但我们的 user agent 在程序的整个生命周期中可以保持不变。
我们以看似随机的顺序遍历 %to_be_retrieved 中的每个 URL,并获取 keys 返回的第一个项目。 显然,可以在从结果列表中获取第一个元素之前对 keys 进行排序,或者对 URL 列表进行深度优先或广度优先搜索。
在循环内部,代码正如我们可能期望的那样:我们创建一个新的 HTTP:Request 实例并将其传递给 $ua,从而在返回中接收一个新的 HTTP:Response 实例。 然后,我们使用 HTML::LinkExtor 解析响应内容,将每个新 URL 放入 %to_be_retrieved 中,但前提是它还不是 %already_retrieved 中的键。
您可能会发现让此程序运行一段时间很有趣,它可以跟踪您最喜欢的网站之一的链接。 Web 的全部意义在于链接; 看看谁链接到谁。 您可能会对自己的发现感到惊讶。
我们对 HTTP 和 LWP 的旋风之旅到此结束。 正如您所看到的,关于它们并没有太多需要学习的; 诀窍是使用它们来创建有趣的应用程序,并避免在使用它们时遇到的陷阱。 LWP 是一个我并不经常需要的包,但当我需要它时,我发现它不可或缺。
