首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

HTML相关范例

HTML-Related Examples

在第2章,我们曾讨论过把纯文本转换为HTML的例子(F67),其中要使用正则表达式从文本中提取E-mail地址和http URL。本节来看一些与HTML相关的其他处理。

匹配HTML Tag

Matching an HTML Tag

最常见的办法就是用<[^>]+>来匹配HTML标签。它通常都能工作,例如下面这段用来去除标签的Perl语句:

$html = ~ s/<[^>]+>//g;

如果tag中含有‘>’,它就不能正常匹配了,而这样的tag明明是合乎HTML规范的:<input name=dir value=">">。虽然这种情况很少见,也不为大家推荐,但HTML语言确实容许在引号内的tag属性中出现非转义的‘<’和‘>’。这样,简单的<[^>]+>就无法匹配了,得想个聪明点的办法。

‘<…>’中能够出现引用文本和非引用形式的“其他文本(other stuff)”,其中包括除了‘>’和引号之外的任意字符。HTML的引文可以用单引号,也可以用双引号。但不容许转义嵌套的引号,所以我们可以直接用"[^"]*"'[^']*'来匹配。

把这些和“其他文本”表达式[^'">]合起来,我们得到:

<("[^"]*"|'[^']*'|[^'">])*>

这个表达式可能有点难看懂,那么加上注释,按宽松格式来看:

<                      #  开始尖括号 "<"

      (                 #      任意数量的 ...

         "[^"]*"      #          双引号字符串

         |              #          或者是...

         '[^']*'      #          单引号字符串

         |              #          或者是...

         [^'">]       #          "其他文本"

      )*               #

 >                     #  结束尖括号 ">"

这个表达式相当漂亮,它会把每个引用部分单作为一个单元,而且清楚地说明了在匹配的什么位置容许出现什么字符。这个表达式的各部分不会匹配重复的字符,因此不存在模糊性,也就不需要担心发生前面例子中出现的,“不小心冒出来(sneaking in)”非期望匹配。

不知你是否注意到了,最开始的两个多选分支的引号中使用了*,而不是+。引用字符串可能为空(例如‘alt=""’),所以要用*来处理这种情况。但不要在第三个多选分支中用*取代+,因为[^'">]只接受括号外的*的限定。给它添加一个加号得到([^'">]+)*,可能导致非常奇怪的结果,我不期望读者现在就能理解,下一章会详细讲解它。

在使用NFA引擎时,我们还需要考虑关于效率的问题:既然没有用到括号匹配的文本,我们可以把它们改为非捕获型括号(F137)。因为多选分支之间不存在重复,如果最后的>无法匹配,那么回头来尝试其他的多选分支也是徒劳的。如果一个多选分支能够在某个位置匹配,那么其他多选分支肯定无法在这里匹配。所以,不保存状态也无所谓,这样做还可以更快地导致失败,如果找不到匹配结果的话。我们可以用固化分组(?>…)而不是非捕获型括号(或者用占有优先的星号限定)。

匹配HTML Link

Matching an HTML Link

假设我们需要从一份文档中提取URL和链接文本,例如下面的文本中标记的内容:

…<a href="http://www.oreilly.com">O'Reilly Media</a>…

因为<A> tag的内容可能相当复杂,我会分两步实现这个任务。第一个是提取<A> tag内部的内容,也就是链接文本,然后从<A> tag中提取URL地址。

实现第一步有个简单办法,就是在点号通配模式下应用不区分大小写的<a\b([^>]+)>(.*?) </a>,这里使用了忽略优先量词。它会把<A>的内容放入$1,把链接文本放入$2。当然,像之前一样,我不应该用[^>]+,而应该使用前几节中的表达式。不过在本节,我会继续使用这个简单的形式,因为这样正则表达式更短,也更容易讲解。

<A>的内容存入字符串之后,就可以用独立的正则表达式来检查它们。其中,URL是href=value属性的值。之前已经说过,HTML容许等号的任意一侧出现空白字符,值可以

以引用形式出现,也可以以非引用形式出现。下面的Perl代码用来输出变量$Html中的链接。

#  请注意: while(...)中的正则表达式是简化的形式,请参见正文

while ($Html =~ m{a\b([^>]+)>(.*?)</a>}ig)

{

  my $Guts = $1; #  把匹配结果存入 ...

  my $Link = $2; #  ....对应变量

  if ($Guts =~ m{

                 \b HREF           #  "href" 属性

                 \s* = \s*        #  "=" 两端可能出现空白字符

                 (?:                #  其值为...

                   "([^"]*)"      #      双引号字符串

                   |                 #      或者是...

                   '([^']*)'      #      单引号字符串

                   |                 #      或者是...

                   ([^'">\s]+)    #      "其他文本"

                 )                   #

                }xi)

{

    my $Url = $+;     #  获得$1、$2等中实际参与匹配的编号最大的捕获型括号的内容

    print "$Url with link text: $Link\n";

}

}

有几点需要注意:

l   我们为匹配值的每个多选结构都添加了括号,来捕获确切的文本。

l   因为我使用了某些括号来捕获文本,在不需要捕获的地方我使用非捕获型括号,这样做既清楚又高效。

l   “其他字符”部分排除了空白字符,也排除了引号和‘>’。

l   因为需要捕获整个href的值,这里使用了+来限制“其他文本”多选分支。这是否会和第200页对其他字符应用+一样导致“非常奇怪的结果”呢?不会,因为这外面没有直接作用于整个多选结构的量词。其中的细节同样会在下一章讨论。

根据具体文本的不同,最后,URL可能保存在$1、$2或者$3中。此时其他捕获型括号就为空或是未定义。Perl提供了特殊变量$+,代表$1、$2之类中编号最靠后的捕获文本。在本例中,这就是我们真正需要的URL。

Perl中的$+很方便,其他语言也提供了其他办法来选择捕获的URL。常用的程序语言结构就可以检查捕获型括号,找到需要的内容。如果能够支持,命名捕获(F138)最适用于干这个,就像204页的VB.NET的例子那样(幸亏.NET提供了命名捕获,因为它的$+有问题,F424)。

检查HTTP URL

Examining an HTTP URL

现在我们得到了URL地址,来看看它是否是HTTP URL,如果是,就把它分解为主机名(hostname)和路径(path)两部分。因为已经有了URL,任务就比从随机文本中识别URL要简单许多,识别的程序要难许多,这将在后文介绍。

所以,如果拿到一个URL,我们需要能够将它拆分为各个部分。主机名是^http://之后和第一个反斜线(如果有的话)之前的内容,而路径就是除此之外的内容:^http://([^/]+)(/.*)?$

URL中有可能包含端口号,它位于主机名和路径之间,以一个冒号开头:^http:// ([^/:]+)(:(\d+))?(/.*)?$

下面是一个分解URL的Perl代码:

if ($url =~ m{^http://([^/:]+)(:(\d+))?(/.*)?$}i)

{

  my $host = $1;

  my $port = $3 || 80;    #  如果存在,就使用$3; 否则默认为80

  my $path = $4 || "/"; #  如果存在,就使用$4; 否则默认为"/"

  print "Host: $host\n";

  print "Port: $port\n";

  print "Path: $path\n";

} else {

  print "Not an HTTP URL\n";

}

验证主机名

Validating a Hostname

在上面的例子中,我们用[^/:]+来匹配主机名。不过,在第2章中(F76)我们使用的是更复杂的[-a-z]+(\.[-a-z]+)*\.(com|edu|…|info)。做同样的事情,复杂程度为什么会有这么大的差别?

而且,虽然二者都用来“匹配主机名”,方法却大不相同。从已知文本(例如,从现成的URL中)中提取一些信息是一回事,从随机文本中准确提取同样信息是另一回事。

而且,在上例中我们假设,‘http://’之后就是主机名,所以用[^/:]+来匹配就是理所当然的。但是在第2章的例子中,我们使用正则表达式从随机文本中寻找主机名,所以它必须更加复杂。

现在从另外一个角度来看主机名的匹配,我们可以用正则表达式来验证主机名。也就是说,我们需要知道,一串字符是否是形式规范、语意正确的主机名。按规定,主机名由点号分

VB.NET中的link检查程序

下面的程序会列出Html变量中的链接:

Imports System.Text.RegularExpressions

  

' 设置循环中将会遇到的正则表达式

Dim A_RRegex as Regex = New Regex(                        _

                "<a\b(?<guts>[^>]+)>(?<Link>.*?)</a>",  _

                RegexOptions.IgnoreCase)

Dim GutsRegex as Regex = New Regex( _

   "\b HREF                     (?#   'href' 属性        )" & _

   "\s* = \s*                   (?#   '=' 可能存在空白字符  )" & _

   "(?:                     (?#   其值为...           )" & _

   " ""(?<url>[^""]*)""     (?#   双引号字符串         )" & _

   " |                       (?#   或者是...            )" & _

   " '(?<url>[^']*)'        (?#   单引号字符串      )" & _

   " |                           (?#   或者是...             )" & _

   " (?<url>[^'"">\s]+)     (?#   '其他文本'             )" & _

   ")                              (?#                            )" , _

   RegexOptions.IgnoreCase Or RegexOptions.IgnorePatternWhitespace)

' 现在检查 'Html' 变量 ...

Dim CheckA as Match = A_Regex.Match(Html)

' For each match within ...

While CheckA.Success

   ' 已匹配 <a> tag,现在检查 URL

   Dim UrlCheck as Match = _

      GutsRegex.Match(CheckA.Groups("guts").Value)

   If UrlCheck.Success

      ' 已经匹配完毕,得到URL/link

      Console.WriteLine("Url " & UrlCheck.Groups("url").Value & _

                        " WITH LINK " & CheckA.Groups("Link").Value)

   End If

   CheckA = CheckA.NextMatch

End While

需要注意的几点:

    在VB.NET中使用正则表达式,需要首先执行对应的Imports语句,告诉编译器应当导入的库文件。

    程序中使用了(?#…)风格的注释,因为VB.NET中加入换行符很不方便,所以普通的‘#’注释会延伸到下一个换行符或者字符串的结尾(第一种情况即意味着正则表达式剩下的所有内容都作为注释)。为了使用正常的#…注释,请在每一行的结尾添加&chr(10)(F420)。

    表达式中的每个双引号都需要以‘" "’表示(F103)。

    两个表达式都用到了命名捕获,Groups("url")比Groups(1)和Groups(2)之类更为清晰。

隔的部分组成,每个部分可以包括ASCII字符、数字和连字符,但是不能以连字符作为开头和结尾。所以,我们可以在不区分大小写的模式下使用这个正则表达式:[a-z0-9]| [a-z0-9][-a-z0-9]*[a-z0-9]。结尾的后缀部分(‘com’、‘edu’、‘uk’等)只有有限多个可能,这在第2章的例子中提到过。结合起来,下面的正则表达式就能够匹配一个语意正确的主机名:

^

 (?i)        #  进行不区分大小写的匹配

#  零个或多个据点分隔的部分

 (?: [a-z0-9]\. | [a-z0-9][-a-z0-9]*[a-z0-9]\. )+

#  然后是结尾的后缀部分...

 (?: com|edu|gov|int|mil|net|org|biz|info|name|museum|coop|aero|[a-z][a-z] )

$

因为存在长度的限制,能够由这个正则表达式匹配的可能并不是合法的主机名:每个部分不能超过63个字符。也就是说,[-a-z0-9]*应该改为[-a-z0-9]{0,61}

还需要做最后的改动。按规定,只包括后缀的主机名同样是语意正确的。但实践证明,这些“主机名”不存在,但是对于两个字母的后缀来说情况可不是如此。例如,安哥拉的域名‘ai’就有一个Web服务器http://ai/。我见过其他这样的链接:cc、co、dk、mm、ph、tj、tv和tw。

如果希望匹配这些特殊情况,应该把中间的(?:…)+改为(?:…)*

^

 (?i)        #  进行不区分大小写的匹配

#  零个或多个据点分隔的部分

 (?: [a-z0-9]\. | [a-z0-9][-a-z0-9]{0,61}[a-z0-9]\. )*

#  然后是结尾的后缀部分...

 (?: com|edu|gov|int|mil|net|org|biz|info|name|museum|coop|aero|[a-z][a-z] )

$

现在它可以用来验证包含主机名的字符串了。因为这是我们想出的与主机名相关的三个正则表达式中最复杂的,你也许会想,不要这些锚点,可能比之前那个从随机文本中提取主机名的表达式更好。但情况并非如此。这个正则表达式能匹配任意双字母单词,正因为如此,第2章中不那么精妙的正则表达式的实际效果更好。但是在下一节我们会看到,某些情况下它仍然不够完善。

在真实世界中提取URL

Plucking Out a URL in the Real World

供职于Yahoo! Finance的工作时,我曾写过处理收录的财经新闻和数据的程序。新闻通常是以纯文本格式提供的,我的程序将其转化为HTML格式以便于显示(如果你在过去10年中曾经在http://finance.yahoo.com浏览过财经新闻,没准看过我处理过的新闻)。

因为接受的数据的“格式”(其实是无格式)很杂乱,从纯文本中识别(recognize)出hostname和URL又比验证(validate)它们困难得多,这任务就很不轻松。前面的内容并没有体现这一点,在本节,你会看到我在Yahoo!用来解决这个问题的程序。

这个程序从文本中提取几种类型的URL——mailto、http、https和ftp。如果我们在文本中找到‘http://’,就知道这肯定是一个URL的开头,所以我们可以直接用http://[-\w] +(\.\w[-\w]*)+来匹配主机名。我们知道,要处理的文本肯定是ASCII编码的英文字母,所以完全可以用-\w来取代-a-z0-9\w同样可以匹配下画线,在某些系统中,它还可以匹配所有的Unicode字符,但是我们知道,这个程序在运行时不会遇到这些问题。

不过,URL通常不是以http://或者mailto:开头的,例如:

…visit us at www.oreilly.com or mail to orders@oreilly.com…

在这种情况下,我们需要加倍小心。我在Yahoo!使用的正则表达式与前面那节的非常相似,只是有一点点不同:

(?i: [a-z0-9] (?:[-a-z0-9]*[a-z0-9])? \. )+ #  子域名s

                    #  .com之类的后缀. 要求小写

(?-i: com\b

    | edu\b

    | biz\b

    | org\b

    | gov\b

    | in(?:t|fo)\b    #  .int或者.info

    | mil\b

    | net\b

    | name\b

    | museum\b

    | coop\b

    | aero\b

    | [a-z][a-z]\b    #  双字母国家代码

)

在这个正则表达式中,我们用(?i:…)(?-i:…)用来规定正则表达式的某个部分是否区分大小写(F135)。我们希望匹配‘www.OReilly.com’,但不是‘NT.TO’这样的股票

代码(NT.TO是北电网络在多伦多证券交易市场的代号,因为要处理的是财经新闻和数据,这样的股票代码很多)。按规定,URL的结尾部分(例如‘.com’)可能是大写的,但我不准备处理这些情况。因为我需要保持平衡——匹配期望的文本(尽可能多的URL),忽略不期望的文本(股票代码)。我希望(?-i:...)只包括国家代码,但是在现实中,我们没有遇到大写的URL地址,所以不必这么做。

下面是从纯文本中查找URL的框架,我们可以在其中添加匹配主机名的子表达式:

\b

    #  匹配开头部分 (proto://hostname,或直接是hostname)

 (

     #  ftp://、http:// 或 https:// 开头部分

     (ftp|https?)://[-\w]+(\.\w[-\w]*)+

   |

     #  或者是用更准确的子表达式找到hostname

     full-hostname-regex

)

     #  可能出现端口号

 ( : \d+ )?

    #  下面部分可能出现,以/开头

 (

    / path-part

)?

我还没有谈论过正则表达式的path(路径)部分,它接在主机名后面(例如http://www. orielly.com/catalog/regex/中的划线部分)。path是最难正确匹配的文本,因为它需要一些猜测才能做得很漂亮。我们在第2章说过,通常出现在URL之后的文本也能被作为URL的一部分。例如:

Read his comments at http://www.oreilly.com/ask_tim/index.html. He...

我们观察之后就会发现,在‘index.html’之后的句号是一个标点,不应该作为URL的一部分,但是‘index.html’的点号却是URL的一部分。

肉眼很容易分辨这两种情况,但程序做起来却很难,所以必须想些聪明的办法来尽可能好地解决问题。第2章的例子使用逆序环视来确保URL不会以句末的句号结尾。我在Yahoo! Finance写程序时还没有逆序环视,所以我用的办法要复杂的多,不过效果是一样的。代码在下一页。

示例5-1:从财经新闻中提取URL

\b

#  匹配开头部分(proto://hostname,或直接是 hostname)

 (

    #  ftp://、http://或 https:// 开头部分

    (ftp|https?)://[-\w]+(\.\w[-\w]*)+

  |

    #  或者是用更准确的子表达式找到hostname

    (?i: [a-z0-9] (?:[-a-z0-9]*[a-z0-9])? \. )+       #  sub domains

    #  .com之类的后缀. 要求小写

    (?-i: com\b

        | edu\b

        | biz\b

        | gov\b

        | in(?:t|fo)\b     #  .int或者.info

        | mil\b

        | net\b

        | org\b

        | [a-z][a-z]\b     #  双字母国家代码

     )

)

#  可能出现端口号

 ( : \d+ )?

#  剩下的部分可能出现,以/开头 ...

 (

      /

    #  虽然很复杂,但确实管用

     [^.!,?;"'<>()\[\]{}\s\x7F-\xFF]*

     (?:

         [.!,?]+ [^.!,?;"'<>()\[\]{}\s\x7F-\xFF]+

      )*

)?

这里用到的办法与第2章第75页用到的办法有很多不同,比较起来也很有意思。下一页里使用此表达式的Java程序详细介绍了它的构造。

在实际生活中,我怀疑自己是否会写这样繁杂的正则表达式,但是作为取代,我会建立一个正则表达式“库(library)”,需要时取用。这方面一个简单的例子就是第76页的$HostnameRegex,以及下面的补充内容。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论