现在我们已经看到了如何使用LWP模块来执行HTTP请求和响应,现在让我们用它来处理URL。一个URL告诉我们如何获取一些信息:“使用HTTP来进行请求”,“通过FTP连接服务器并且获取文件”,或者“给某个地址发送一个邮件”。

多样性的URL既是有好处又有坏处。一方面,我们可以任意的扩展URL语法来指向任何类型的网络资源地址。另一方面,非常灵活意味着在某些特殊情况下使用正则表达式解析URL会非常困难。

LWP模块提供了URI类来管理URLs。这一章主要讲解如何创建对象来显示URLs,从这些对象中提取信息,相对URL与绝对URL之间的转换。最后一个任务是创建一个对网页连接检查程序和爬虫来说非常有用的工具用来抓取HTML中的URL转换成可以访问的绝对URL。

4.1. URLs 解析 (Parsing URLs)

与其把URL的每一个部分使用正则表达式来分析,倒不如使用URI class。当创建一个用来代表URL的对象后,针对URL的每个部分有自己的属性(scheme, username, hostname , port, etc.)。使用方法来获取或者属性。 例子4-1,创建一个代表一个复杂的URL的URI对象,然后通过方法来获得URL中各个组成部分。 例子4-1,分解一个URL

  1. use URI;
  2. my $url = URI->new('http://user:pass@example.int:4345/hello.php?user=12');
  3. print "Scheme: ", $url->scheme( ), "\n";
  4. print "Userinfo: ", $url->userinfo( ), "\n";
  5. print "Hostname: ", $url->host( ), "\n";
  6. print "Port: ", $url->port( ), "\n";
  7. print "Path: ", $url->path( ), "\n";
  8. print "Query: ", $url->query( ), "\n";

例子4-1 打印:

  1. Scheme: http
  2. Userinfo: user:pass
  3. Hostname: example.int
  4. Port: 4345
  5. Path: /hello.php
  6. Query: user=12

除了读URL各个部分之外,像host()这种方法也可以改变URL, 使用我们非常熟悉的 $object->method来读取属性值和$object->method(newvalue)修改属性:

  1. use URI;
  2. my $uri = URI->new("http://www.perl.com/I/like/pie.html");
  3. $uri->host('testing.perl.com');
  4. print $uri,"\n";
  5. http://testing.perl.com/I/like/pie.html

现在让我们研究得更深入一些。

4.1.1. 构造(Constructors)

一个URI对象代表一个URL。() 通过一个URL字符串来创建一个URI对象,使用new()构造: An object of the URI class represents a URL. (Actually, a URI object can also represent a kind of URL-like string called a URN, but you’re unlikely to run into one of those any time soon.) To create a URI object from a string containing a URL, use the new( ) constructor:

  1. $url = URI->new(url [, scheme ]);

如果url是相对URL(),scheme决定你的URL将使用什么协议(http,ftp,etc)。但是在大部分情况下,当你知道是绝对URL的情况下可以使用URI->new来创建;对于相对URL或者可能是相对URL来说,使用URI->new_abs方法,如下展示。

URI模块剔除了引用,尖括号,和空格。所以这些声明创建的URL完全相同:

  1. $url = URI->new('<http://www.oreilly.com/>');
  2. $url = URI->new('"http://www.oreilly.com/"');
  3. $url = URI->new(' http://www.oreilly.com/');
  4. $url = URI->new('http://www.oreilly.com/ ');

URI类动态的转义在URL标准中没有提到的字符。所以如下定义是等效的:

  1. $url = URI->new('http://www.oreilly.com/bad page');
  2. $url = URI->new('http://www.oreilly.com/bad%20page');

如果你已经有了一个URI对象,clone()方法将会创建一个相同的对象出来:

  1. $copy = $url->clone( );

例子4-2 克隆一URI对象并且改变属性

  1. use URI;
  2. my $url = URI->new('http://www.oreilly.com/catalog/');
  3. $dup = $url->clone( );
  4. $url->path('/weblogs');
  5. print "Changed path: ", $url->path( ), "\n";
  6. print "Original path: ", $dup->path( ), "\n";

执行,打印如下:

  1. Changed path: /weblogs
  2. Original path: /catalog/

4.1.2. 输出(Output)

把URI对象当成一个字符串:

  1. $url = URI->new('http://www.example.int');
  2. $url->path('/search.cgi');
  3. print "The URL is now: $url\n";
  4. The URL is now: http://www.example.int/search.cgi

在输出之前标准化URL有时非常有用:

  1. $url->canonical( );

它究竟做了什么取决于url的类型,但是它通常会将域名转换成小写形式,并且如果是默认端口会移除端口(比如 http://www.eXample.int:80 会变成http://www.example.int),同时将转义字符变成大写。(比如, %2e 变成 %2E),并将不需要转义的转义字符翻译过来(比如, %41 变成 A)。在第12章中,“狙击手”,我们讨论了一种抓取数据但是避免抓取重复的url的程序。 它通过一个被称作%seen_url_before的哈希表来追踪那些被重复访问过的程序。 如果某个url存在于其中,那它就已经被抓取过了。 这种机制是在决定进入某url之前对所有的url调用canonical函数。如果没有调用canonical,那么你可能会在已经访问过 http://www.example.int:80 的情况下,再去访问http://www.EXample.int,并且你看不出这有什么问题。但是如果你调用了canonical,他们都会变成http://www.example.int,所以你就会发现你访问了同一个url两次。如果你觉得这种重复性的问题会在你的程序中出现,如果有此方面的疑惑,当构建url时调用canonical函数,像这样:

  1. $url = URI->new('http://www.example.int')->canonical;

4.1.3. 比较(Comparison)

比较两个URLs,使用eq()方法:

  1. if ($url_one->eq(url_two)) { ... }

例如:

  1. use URI;
  2. my $url_one = URI->new('http://www.example.int');
  3. my $url_two = URI->new('http://www.example.int/search.cgi');
  4. $url_one->path('/search.cgi');
  5. if ($url_one->eq($url_two)) {
  6. print "The two URLs are equal.\n";
  7. }
  8. The two URLs are equal.

如果标准化过的两个URL有相同的字符串则相等。eq()比eq操作符号更块一些:

  1. if ($url_one eq $url_two) { ... } # 没效率!

用来判断如果两个URL不相同但是是同一URI对象, 使用==操作符:

  1. if ($url_one == $url_two) { ... }

例如:

  1. use URI;
  2. my $url = URI->new('http://www.example.int');
  3. $that_one = $url;
  4. if ($that_one == $url) {
  5. print "Same object.\n";
  6. }
  7. Same object.

4.1.4. URL组成 (Components of a URL)

一个普通的URL看来起来像 Figure 4-1。 4.1. URLs 解析 (Parsing URLs) - 图1

URI模块提供访问每个组成部分的方法。有些组件只在一些sheme中有效(例如,邮件:URL不支持userinfo, host, 或者port)

除了比较直观的的sheme(), userinfo(), host(), port(), path(), query(), fragment()方法之外,还有一些不那么直观的也非常有用。

  1. $url->path_query([newval]);

把路径和查询作为一个字符串,e.g, /hello.php?user=21。

  1. $url->path_segments([segment, ...]);

在标量上下文中,与path()接口相同,但是在列表上下文还总,返回一个路径部分的列表(路径或者文件名等)。例如:

  1. $url = URI->new('http://www.example.int/eye/sea/ewe.cgi');
  2. @bits = $url->path_segments( );
  3. for ($i=0; $i < @bits; $i++) {
  4. print "$i {$bits[$i]}\n";
  5. }
  6. print "\n\n";
  7. 0 {}
  8. 1 {eye}
  9. 2 {sea}
  10. 3 {ewe.cgi}
  1. $url->host_port([newval])
  1. hostname与端口作为一个整体,e.g. www.example.int:8080
  1. $url->default_port( );

scheme的默认端口号(e.g.80是http的, 21是ftp)

如果URL中缺少一些组件,相应的访问函数访问时候通常返回undef:

  1. use URI;
  2. my $uri = URI->new("http://stuff.int/things.html");
  3. my $query = $uri->query;
  4. print defined($query) ? "Query: <$query>\n" : "No query\n";
  5. No query

可是,有些类型的URL并不包含一些必需的组件。比如, 邮件发送:URL不存在host组成,所以调用host()的代码将会挂掉。例如:

  1. use URI;
  2. my $uri = URI->new('mailto:hey-you@mail.int');
  3. print $uri->host;
  4. Can't locate object method "host" via package "URI::mailto"

考虑提取文档中所有的URL并检查:

  1. foreach my $url (@urls) {
  2. $url = URI->new($url);
  3. my $hostname = $url->host;
  4. next unless $Hosts_to_ignore{$hostname};
  5. ...otherwise ...
  6. }

因为邮件URL没有host(),所以访问的话会挂掉。可以使用can()来选择是否忽略调用host

  1. foreach my $url (@urls) {
  2. $url = URI->new($url);
  3. next unless $url->can('host');
  4. my $hostname = $url->host;
  5. ...

或者更直接一些:

  1. foreach my $url (@urls) {
  2. $url = URI->new($url);
  3. unless('http' eq $url->scheme) {
  4. print "Odd, $url is not an http url! Skipping.\n";
  5. next;
  6. }
  7. my $hostname = $url->host;
  8. ...and so forth...

因为所有的URLs都提供scheme方法,所有的http: URLs都提供host()方法,所以可以保证是安全的。 [1]对于那些好奇的人来说,URI class 的文档中对于什么样的URI允许什么样的URI scheme的解释,和一些特殊的子类功能比如URI::ldap的解释是一样的。 Because all URIs offer a scheme method, and all http: URIs provide a host( ) method, this is assuredly safe.[1] For the curious, what URI schemes allow for what is explained in the documentation for the URI class, as well as the documentation for some specific subclasses like URI::ldap.

4.1.5. 查询(Queries)

URI模块有两个方法用来查询数据, 就是如上所说的query()和path_query(),我们已经讨论过了。 在web初期, 查询是非常简单的文本字符串。空格被编码为加号(+):

  1. http://www.example.int/search?i+like+pie

query_keywords()方法用来处理这种类型的查询,返回关键字的列表:

  1. @words = $url->query_keywords([keywords, ...]);

例如:

  1. use URI;
  2. my $url = URI->new('http://www.example.int/search?i+like+pie');
  3. @words = $url->query_keywords( );
  4. print $words[-1], "\n";
  5. pie

更现代的查询接受一个值列表。名字和他的值被一个等号分开,每一对被&符号分开:

  1. http://www.example.int/search?food=pie&action=like

query_form()方法把没一个查询都当作一个键和值的列表:

  1. @params = $url->query_form([key,value,...);

例如:

  1. use URI;
  2. my $url = URI->new('http://www.example.int/search?food=pie&action=like');
  3. @params = $url->query_form( );
  4. for ($i=0; $i < @params; $i++) {
  5. print "$i {$params[$i]}\n";
  6. }
  7. 0 {food}
  8. 1 {pie}
  9. 2 {action}
  10. 3 {like}

4.2. 相对URLs(Relative URLs)

URL 路径既有绝对的又有相对的。一个绝对URL开头是scheme,然后是这个sheme要求的一些数据。对于一个HTTP URL,会有一个hostname和一个path:

  1. http://phee.phye.phoe.fm/thingamajig/stuff.html

所有不以scheme开头的都是相对URL。为了解释一个相对的URL,需要一个绝对的基本URL(就好像你不知道”这里向西800米”的GPS坐标,除非你知道”这里”的坐标)。

一个相对的URL会有一些隐式信息,通过查看它的基URL可以知道。例如,如果你的基URL是http://phee.phye.phoe.fm/thingamajig/stuff.html ,将会看到一个相对URL /also.html,那么隐式信息就是”相同的scheme(http)”和”相同的host(phee.phye.phoe.fm)”,显式信息是”路径为/also.html.”。所以它和以下的绝对URL等效:

  1. http://phee.phye.phoe.fm/also.html

某些种类的相对URL请求信息的方式与Unix文件系统相似,”..”意思是”上一层路径”,”.”代表当前所在路径,其他的代表“在这个路径中”。所以对于对于zing.xml相对与http://phee.phye.phoe.fm/thingamajig/stuff.html 被解释为如下URL:

  1. http://phee.phye.phoe.fm/thingamajig/zing.xml

也就是说,我们使用除最后一位的所有路径,然后添加在后面。 同样,一个 ../hi_there.jpg这样的相对URL在绝对URL http://phee.phye.phoe.fm/thingamajig/stuff.html 被解释成这样:

  1. http://phee.phye.phoe.fm/hi_there.jpg

搞清楚这一点, 对于http://phee.phye.phoe.fm/thingamajig/ 来说 “..”告诉我们回到上一层目录, 变成http://phee.phye.phoe.fm/ . 添加hi_there.jpg在后面就成了如上所说的形式了。 还有第三种相对URL,包含一个段,像#endnotes。这在html文档中很常见,代码如下:

  1. <a href="#endnotes">See the endnotes for the full citation</a>

我们需要基于它的基URL来解释一个fragment-only相对URL,把所有的段去掉,然后添加新的。所以如果基URL是这个:

  1. http://phee.phye.phoe.fm/thingamajig/stuff.html

相对URL是#endnotes, 那么新生成的绝对URL就是:

  1. http://phee.phye.phoe.fm/thingamajig/stuff.html#endnotes

We’ve looked at relative URLs from the perspective of starting with a relative URL and an absolute base, and getting the equivalent absolute URL. But you can also look at it the other way: starting with an absolute URL and asking “what is the relative URL that gets me there, relative to an absolute base URL?”. This is best explained by putting the URLs one on top of the other:

我们一直以一种 始于相对URL和一个绝对基础,最终获得一个等价的绝对URL的观点来看待相对URL。但是你也可以用另一种方式:从一个绝对URL开始,然后质问:“是一个什么样的相对URL让我得到了它,它又相对于什么样的绝对基础?”。 这是对于为什么某些URL会放在别的URL之上的最好的解释:

  1. Base: http://phee.phye.phoe.fm/thingamajig/stuff.xml
  2. Goal: http://phee.phye.phoe.fm/thingamajig/zing.html

To get from the base to the goal, the shortest relative URL is simply zing.xml. However, if the goal is a directory higher:

从基础到结果,最短的相对URL就是zing.xml。但是,如果这个结果是高一层的目录:

Base: http://phee.phye.phoe.fm/thingamajig/stuff.xml Goal: http://phee.phye.phoe.fm/hi_there.jpg

这样的话相对路径就是../hi_there.jpg。 在这种情况下,只需从文档目录的根节点得到一个相对路径/hi_there.jpg也能够让你得到这个结果。

then a relative path is ../hi_there.jpg. And in this case, simply starting from the document root and having a relative path of /hi_there.jpg would also get you there.

处理相对URL并且与绝对URL相互转化,其背后的逻辑并不简单并且很容易搞错。 实际上URL类库已经给我们提供了一些函数让我们以最佳方式来实现这些。 你可能会对URL进行两种处理:从相对URL转到绝对URL或者从绝对URL转到相对URL(说了半天就说这个,不是废话么)。

The logic behind parsing relative URLs and converting between them and absolute URLs is not simple and is very easy to get wrong. The fact that the URI class provides functions for doing it all for us is one of its greatest benefits. You are likely to have two kinds of dealings with relative URLs: wanting to turn an absolute URL into a relative URL and wanting to turn a relative URL into an absolute URL.