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

扩展的例子

Extended Examples

下面的几个例子讲解了一些关于正则表达式的重要诀窍。讨论会稍微多一些,关于解决办法和错误思路的着墨也会更多一些,最终会给出正确答案。

在Java中通过变量构建正则表达式

String SubDomain  =       "(?i:[a-z0-9]|[a-z0-9][-a-z0-9]*[a-z0-9])";

String TopDomains = "(?x-i:com\\b         \n" +

                        "     |edu\\b          \n" +

                        "     |biz\\b                \n" +

                        "     |in(?:t|fo)\\b       \n" +

                        "     |mil\\b                \n" +

                        "     |net\\b                \n" +

                        "     |org\\b                \n" +

                        "     |[a-z][a-z]\\b   \n" + // country codes

                        ")                         \n";

String Hostname =  "(?:" + SubDomain + "\\.)+" + TopDomains;

String NOT_IN   = ";\"'<>()\\[\\]{}\\s\\x7F-\\xFF";

String NOT_END  = "!.,?";

String ANYWHERE = "[^" + NOT_IN + NOT_END + "]";

String EMBEDDED = "[" + NOT_END + "]";

String UrlPath =   "/"+ANYWHERE + "*("+EMBEDDED+"+"+ANYWHERE+"+)*";

String Url =

  "(?x:                                                       \n"+

  "  \\b                                                      \n"+

  "  ##  匹配hostname                                       \n"+

  "  (                                                         \n"+

  "    (?: ftp | http s? ): // [-\\w]+(\\.\\w[-\\w]*)+ \n"+

  "   |                                                       \n"+

  "    " + Hostname + "                                    \n"+

  "  )                                                        \n"+

  "    #  可能出现端口号                                    \n"+

  "  (?: :\\d+ )?                                          \n"+

  "                                                            \n"+

  "    #  下面的部分可能出现,以\开头                       \n"+

  "  (?: " + UrlPath + ")?                                   \n"+

  " )";

// 现在把正则表达式编译为正则对象

Pattern UrlRegex = Pattern.compile(Url);

// 现在准备在文本中应用,寻找url...

  ……

保持数据的协调性

Keeping in Sync with Your Data

我们来看一个长一点的例子,它有点极端,但很清楚地说明了保持协调的重要性(同时提供了一些保持协调的方法)。

假设,需要处理的数据是一系列连续的5位数美国邮政编码(ZIP Codes),而需要提取的是以44开头的那些编码。下面是一点抽样,我们需要提取的数值用粗体表示:

03824531449411615213441829505344272752010217443235

最容易想到的\d\d\d\d\d,它能匹配所有的邮政编码。在Perl中可以用@zips=m/\d\d\ d\d\d/g;来生成以邮政编码为元素的list(为了让这些例子看起来更整洁,我们假设需要处理的文本在Perl的默认目标变量$_中,见F79)。如果使用其他语言,也只需要循环调用正则表达式的find方法。我们关注的是正则表达式本身,而不是语言的实现机制,所以下面继续使用Perl。

回到\d\d\d\d\d,下面提到的这一点很快就会体现出其价值;在整个解析过程中,这个正则表达式任何时候都能够匹配——绝对没有传动装置的驱动和重试(我假设所有的数据都是规范的,此假设与具体情况密切相关)。

很明显,把\d\d\d\d\d改为44\d\d\d来查找以44开头的邮政编码不是个好办法——匹配失败之后,传动装置会驱动前进一个字符,对44…的匹配不再是从每个邮政编码的第一位开始。44\d\d\d会错误地匹配‘…5314494116…’。

当然,我们可以在正则表达式的开头添加\A,但是这样只能对付一行文本中的第一个邮政编码。我们需要手动保持正则引擎的协调,才能忽略不需要的邮政编码。这里的关键是,要跳过完整的邮政编码,而不是使用传动装置的驱动过程(bump-along)来进行单个字符的移动。

根据期望保持匹配的协调性

下面列举了几种办法用来跳过不需要的邮政编码。把它们添加到正则表达式44\d\d\d之前,可以获得期望的结果。非捕获型括号用来匹配不期望的邮政编码,这样能够快速地略过它们,找到匹配的邮政编码,在第一个$1的捕获括号中:

(?:[^4]\d\d\d\d|\d[^4]\d\d\d)*…

这种硬办法(brute-force method)主动略过非44开头的邮政编码(当然,用[1235-9]替代[^4]可能更合适,但我之前说过,假设处理的是规范的数据)。注意,我们不能使用(?:[^4][^4]\d\d\d)*,因为它不会匹配(也就无法略过)43210这样不期望的邮政编码。

(?:(?!44)\d\d\d\d\d)*…

这个办法跳过非44开头的邮政编码。其中的想法与之前并无差别,但用正则表达式写出来就显得大不一样。比较这两段描述和相关的正则表达式就会发现,在这里,期望的邮政编码(以44开头)导致逆序环视(?!44)失败,于是略过停止。

(?:\d\d\d\d\d)*?…

这个办法使用忽略优先量词,只有在需要的时候才略过某些文本。我们把它放在真正需要匹配的正则表达式前面,所以如果那个表达式失败,它就会匹配一个邮政编码。忽略优先(…)*?导致这一切的发生。因为存在忽略优先量词,(?:\d\d\d\d\d)甚至都不会尝试匹配,在后面的表达式失败之前。星号确保了,它会重复失败,直到最终找到匹配文本,这样就能只跳过我们希望跳过的文本。

把这个表达式和(44\d\d\d)合起来,就得到:

@zips=m/(?:\d\d\d\d\d)*?(44\d\d\d)/g;

它能够提取以44开头的邮编,而主动跳过其他的邮编(在“@array = m/…/g”的情况下,Perl会用每次尝试中找到的匹配文本来填充这个数组,F311)。这个表达式能够重复应用于字符串,因为我们知道每次匹配的“起始匹配位置”都是某个邮政编码的开头位置,也就保证下一次匹配是从一个邮政编码的开始,这正是正则表达式期望的。

不匹配时也应当保证协调性

我们是否能保证,每次正则表达式都在邮政编码字符串的开头位置应用?显然不是!我们手动跳过了不符合要求的邮政编码,可一旦不需要继续匹配,本轮匹配失败之后自然就是驱动过程和重试,这样就会从邮政编码字符串之中的某个位置开始——我们的方法不能处理这种情况。

再来看数据样本:

03824531449411615213441829503544272æ7æ5æ2æ010217443235

匹配的代码以粗体标注(第三组不符合要求),主动跳过的代码以下画线标注,通过驱动过程-重试略过的字符也标记出来。在44272匹配之后,目标文本中再也找不到匹配,所以本轮尝试宣告失败。但总的尝试并没有宣告失败。传动机构会进行驱动,从字符串的下一个字符开始应用正则表达式,这样就破坏了协调性。在第四次驱动之后,正则表达式略过10217,错误地匹配44323。

如果在字符串的开头应用,这三个表达式都没有问题,但是传动装置的驱动过程会破坏协调性。如果我们能取消驱动过程,或者保证驱动过程不会添麻烦,问题就解决了。

办法之一是禁止驱动过程,即在前两种办法中的(44\d\d\d)之后添加?,将其改为匹配优先的可选项。这样,刻意安排的(?:(?!44)\d\d\d\d\d)*…(?:[^4]\d\d\d\d|\d

[^4]\d\d\d)*…就只会在两种情况下停止:发生符合要求的匹配,或者邮政编码字符串结束(这也是此方法不适用于第三个表达式的原因)。这样,如果存在符合要求的邮政编码,(44\d\d\d)?就能匹配,而不会强迫回溯。

这个办法仍然不够完善。原因之一是,即便目标字符串中没有符合要求的邮政编码,也会匹配成功,接下来的处理程序会变复杂。不过,其优点在于速度很快,因为不需要回溯,也不需要传动装置进行任何驱动过程。

使用\G保证协调

更通用的办法是在这三个表达式末尾添加\G(F130)。因为每个表达式的每次匹配都以符合要求的邮政编码结尾,下次匹配开始时就不会进行驱动。而如果有驱动过程,开头的\G会立刻导致匹配失败,因为在大多数流派中,只有在未发生驱动过程的情况下,它才能成功匹配(但在Ruby和其他规定\G表示“本次匹配起始位置”的流派中不成立F131)

所以第二个表达式就变成了:

    @zips = m/\G(?:(?!44)\d\d\d\d\d)*(44\d\d\d)/g;

匹配之后不需要进行任何特殊检查。

本例的意义

我首先承认,这个例子有点极端,不过,它包含了许多保证正则表达式与数据协调性的知识。如果现实生活中需要处理这样的问题,我可能不会完全用正则表达式来解决。我会直接用\d\d\d\d\d来提出每个邮政编码,然后检查它是否以‘44’开头。在Perl中是这样:

@zips = ( );     #  确保数组为空

while (m/(\d\d\d\d\d)/g) {

   $zip = $1;

   if (substr($zip, 0, 2) eq "44") {

       push @zips, $zip;

   }

}

\G有兴趣的读者请参考132页的补充内容,尽管本书写作时只能举Perl的例子。

解析CSV文件

Parsing CSV Files

解析CS(逗号分隔值)文件有点麻烦,因为每个程序都有自己的CSV文件格式。首先来看如何解析Microsoft Excel生成的CSV文件,然后再看其他格式(注3)。幸运的是,Microsoft的格式是最简单的。以逗号分隔的值要么是“纯粹的”(仅仅包含在括号之前),要么是在双引号之间(这时数据中的双引号以一对双引号表示)。

下面是个例子:

Ten Thousand,10000, 2710 ,,"10,000","It's ""10 Grand"", baby",10K

这一行包含七个字段(fields):

Ten–Thousand

10000

–2710–

空字段

10,000

It's–"10–Grand",–baby

10K

为了从此行解析出各个字段,我们的正则表达式需要能够处理两种格式。非引号格式包含引号和逗号之外的任何字符,可以用[^",]+匹配。

双引号字段可以包含逗号、空格,以及双引号之外的任何字符。还可以包含连在一起的两个双引号。所以,双引号字段可以由"…"之间任意数量的[^"]|""匹配,也就是"(?:[^"] |"")*"(为效率考虑,我们可以使用固化分组(?>…)来替代(?:…),不过这个话题留到下一章F259)。

综合起来,[^",]+|"(?:[^"]|"")*"能够匹配一个字段。这可能有点难看懂,下面我们给出宽松排列(F111)格式:

#  引号和逗号之外的文本...

[^",]+

#  ...或者是...

|

#  ...双引号字段(其中容许出现连在一起的成对双引号)

" #  起始双引号

  (?: [^"] | "" )*

" #  结束双引号

现在这个表达式可以实际应用到包含CSV文本行的字符串上了,但如果我们希望真正利用匹配结果,就应该知道具体是哪个多选分支匹配了。如果是双引号字符串,就需要去掉首尾两端的双引号,把其中紧挨着的两个双引号替换为单个双引号。

我能想到的办法有两个。其一是检查匹配结果的第一个字符是否双引号,如果是,则去掉第一个和最后一个字符(双引号),然后把中间的‘""’替换为‘"’。这办法够简单,但如果使用捕获型括号会更简单。如果我们给捕获字段的每个子表达式添加捕获型括号,可以在匹配之后检查各个分组的值:

#  引号和逗号之外的文本...

( [^",]+ )

#  ... 或者是...

   |

#  ...双引号字段(其中容许出现连在一起的成对双引号)

"  #  起始双引号

 ( (?: [^"] | "" )* )

"  #  结束双引号

如果是第一个分组捕获,则不需要进行任何处理,如果是第二个分组,则只需要把‘""’替换为‘"’即可。

下面给出Perl的程序,稍后(找出某些bug之后)给出Java和VB.NET(在第10章给出PHP的程序F480)。下面是Perl程序,假设数据位于$line中,而且已经去掉了结尾的换行符(换行符不属于最后的字段!):

while ($line =~ m{

           #  引号和逗号之外的文本...

           ( [^",]+ )

           #  ...或者是...

             |

           #  ...双引号字段(其中容许出现连在一起的成对双引号)

           "  #  起始双引号

              (     (?: [^"] | "" )* )

           " #  结束双引号

        }gx)

{

  if (defined $1) {

      $field = $1;

  } else {

      $field = $2;

      $field =~ s/""/"/g;

  }

  print "[$field]"; #  输出$field供调试

  现在可以处理$field了...

}

将其应用于测试数据,结果为:

[Ten–Thousand][10000][–2710–][10,000][It's–"10–Grand",–baby][10K]

看来没问题,但不幸的是它不会输出为空的第四个字段。如果“处理$field”是将字段的值存入数组,完成后访问数组的第五个元素得到第五个字段(“10,000”)。这显然不对,因为数组的元素与空字段不对应。

想到的第一个办法是把[^",]+改为[^",]*,这看来是显而易见的,但它正确吗?

测试一下,下面是结果:

[Ten–Thousand][][10000][][–2710–][][][][10,000][][][It's–"10–Grand", …

哇,现在出来了一堆空字段!仔细检查检查,就不会这么吃惊。(…)*的匹配可以不占用任何字符。如果真的遇到空字段,确实能匹配,那么考虑第一个字段匹配之后的情况呢,此时正则表达式从‘Ten–Thousandæ,10000…’开始应用。如果表达式中没有元素可以匹配逗号(就本例来说),就会发生长度为0的成功匹配。实际上,这样的匹配可能有无穷多次,因为正则引擎可能在同一位置重复这样的匹配,现代的正则引擎会强迫进行驱动过程,所以同一位置不会发生两次长度为0的匹配(F131)。所以每个有效匹配之间还有一个空匹配,在每个引号字段之前会多出一个空匹配(而且数组末尾还会有一个空匹配,只是此处没有列出来)。

分解驱动过程

要解决问题,我们就不能依赖传动机构的驱动过程来越过逗号。所以,我们需要手工来控制。能想到的办法有两个:

1. 手工匹配逗号。如果采取此办法,需要把逗号作为普通字段匹配的一部分,在字符串中“迈步(pace ourselves)”。

2. 确保每次匹配都从字段能够开始的位置开始。字段可以从行首,或者是逗号开始。

可能更好的办法是把两者结合起来。从第一种办法(匹配逗号本身)出发,只需要保证逗号出现在第一个字段之外的所有字段开头。或者,保证逗号出现在最后一个字段之外的所有字段的末尾。可以在表达式前面添加^|,,或者后面添加$|,,用括号控制范围。

在前面添加,就得到:

(?:^|,)

(?:

    #  引号和逗号之外的文本....

    ( [^",]* )

  #  ...或者是..

  |

    #  ... 双引号字段(其中容许出现连在一起的成对双引号)

    "  #  起始双引号

     ( (?: [^"] | "" )* )

    "  #  结束双引号

)

看起来它应当没错,但实际的结果却是:

[Ten–Thousand][10000][–2710–][][][000][][–baby][10K]

而我们期望的是:

[Ten–Thousand][10000][–2710–][][10,000][It's–"10•Grand",–baby][10K]

问题出在哪里呢?似乎是双引号字段没有正确处理,所以问题出在它身上,对吗?不对,问题在前面。或许176页的告诫有所帮助:如果多个多选分支能够在同一位置匹配必须小心地排列顺序。第一个多选分支[^",]*不需要匹配任何字符就能成功,除非之后的元素强迫,否则第二个多选分支不会获得尝试的机会。而这两个多选分支之后没有任何元素,所以第二个多选分支永远不会得到尝试的机会,这就是问题所在!

哇,现在我们已经找到了问题所在。OK,交换一下多选分支的顺序:

 (?:^|,)

 (?: #  或者是匹配双引号字段(其中容许出现连在一起的成对双引号)...

        " # (起始双引号)

         ( (?: [^"] | "" )* )

        " # (起始双引号)

  |

    #  ... 或者是引号和逗号之外的文本...

          ( [^",]* )

)

对了!至少对测试数据来说是对了。如果数据变了,还是这样吗?本节的标题是“分解驱动过程”,而最保险的办法就是以完整测试作为基础的思考,故可以用\G来确保每次匹配从上一次匹配结束的位置开始。考虑到构建和应用正则表达式的过程,这样做应该绝对没问题。如果在表达式开始添加\G,就会禁止引擎的驱动过程。我们希望这样修改不会出问

题,但是结果并非如此。之前输出

[Ten–Thousand][10000][–2710–][][][000][][–baby][10K]

的正则表达式添加\G之后,得到

[Ten–Thousand][10000][–2710–][][]

如果起初没看明白,这样看会更明显。

 

CSV Processing in Java

这里有一个使用Sun的java.util.regex解析CSV的例子。这段程序着眼于简洁的、更有效的版本——第8章(F401)将会介绍。

import java.util.regex.*;

         .

         .

         .

String regex = // 把双引号字段存入group(1)、非引号字段存入group(2)

   "\\G(?:^|,)                                                   \n"+

   "(?:                                                            \n"+

   "   # 要么是双引号字段...                                     \n"+

   "   \"             # 字段起始双引号                         \n"+

   "    (  (?: [^\"]++ | \"\" )*+ )                         \n"+

   "   \"             # 字段结束双引号                            \n"+

   " |# ... 要么是 ...                                         \n"+

   "            # 非引号非逗号文本 ...                            \n"+

   "    ( [^\",]*)                                                \n"+

   " )                                                              \n";

// 创建使用上面正则表达式的matcher,暂时不指定需要应用的文本

Matcher mMain = Pattern.compile( regex, Pattern.COMMENTS).matcher("");

  // 为""创建一个matcher,暂时不指定需要应用的文本

Matcher mQuote = Pattern.compile("\"\"").matcher("");

         .

         .

         .

// 上面都是准备工作,下面的代码逐行处理文本

mMain.reset( line); //下面处理line中的CSV文本

while ( mMain.find())

{

    String field;

    if ( mMain.start(2) >= 0)

         field = mMain.group(2);     // 非引号字段,直接使用

    else

         // 引号字段,替换其中的成对双引号

         field = mQuote.reset(mMain.group(1)).replaceAll("\"");

    // 处理字段...

    System.out.println("Field [" + field + "]");

}

另一个办法

本节的开头提到有两种办法正确匹配各个字段。之二是确保匹配只能在容许出现字段的地方开始。从表面上看,这类似于添加^|,,只是使用了逆序环视(?<=^|,)

不幸的是,按照第3章(F133)的解释,即使支持逆序环视,也不见得会支持变长的逆序环视,所以此方法可能无法使用。如果问题在于长度可变,我们可以把(?<=^|,)替换为(?:^|(?<=,)),但是相比第一种办法,它太麻烦了。而且,它仍然依赖传动装置的驱动过程来越过逗号,如果别的地方出了什么差错,它会容许在‘…"10, æ000"…’处的匹配。总的来说就是,不如第一种办法保险。

不过我们可以略施小计——要求匹配在逗号之前(或者是一行结束之前)结束。在表达式结尾添加(?=$|,)可以确保它不会进行错误的匹配。实际生活中,我会这样做吗?直率地说我觉得第一种方法很合用,所以遇到这种情况我可能不会采取第二种办法,不过如果需要,这技巧却是很有用的。

进一步提高效率

尽管在下一章之前都不会谈论效率,但对于支持固化分组(F139)的系统,我还是愿意在这里给出提高效率的修改:把匹配双引号字段的子表达式从(?:[^"]|"")*改为(?>[^"]+|"")*。下一页用VB.NET的例子做了说明。

如果像Sun的Java regex package那样支持占有优先量词(F142),也可以使用占有优先量词。Java CSV程序的补充内容说明了这一点。

这些修改背后的道理会在下一章讲解,最终我们会在271页给出效率最高的办法。

其他CSV格式

Micorsoft的CSV格式很流行,因为它是Microsoft的CSV格式,但其他程序可能有不同格式,我见过的情况还有:

l   使用任意字符,例如';'或者制表符作为分隔。(不过这样名字还能叫“逗号分隔值”吗?)

l   容许分隔符之后出现空格,但不把它们作为值的一部分。

l   用反斜线转义引号(例如用‘\"’而不是‘""’类表示值内部的引号)。通常这意味着反斜线可以在任何字符前出现(并忽略)。

这些变化都很容易处理。第一种情况只需要把逗号替换为对应的分隔符,第二种只需要在第一个分隔符之后添加\s*,例如以(?:^|,\s*)开头。

第三种情况,我们可以用之前的办法(F198),把[^"]+|""替换为[^\\"]+|\\.。当然,我们必须把后面的s/""/"/g改为更通用的s/\\(.)/$1/g,或者对应语言中的代码。

 

VB.NET的CSV处理

Imports System.Text.RegularExpressions

    ……

Dim FieldRegex as Regex = New Regex( _

       "(?:^|,)                                                   " & _

       "(?:                                                          " & _

       "   (?#  要么是双引号字段 ...)                        " & _

       "   ""  (?#  字段起始双引号)                         " & _

       "    (   (?> [^""]+ | """" )* )                       " & _

       "   ""  (?#  字段结束双引号)                            " & _

       " (?# ... or ...)                                        " & _

       " |                                                           " & _

       "   (?#  ... 非引号非逗号文本 ...)                       " & _

       "   ( [^"",]*)                                            " & _

       " )", RegexOptions.IgnorePatternWhitespace)

Dim QuotesRegex as Regex = New Regex(" "" "" ")     '#  双引号字符串

        .

        .

        .

Dim FieldMatch as Match = FieldRegex.Match(Line)

While FieldMatch.Success

   Dim Field as String

   If FieldMatch.Groups(1).Success

     Field = QuotesRegex.Replace(FieldMatch.Groups(1).Value, """")

   Else

     Field = FieldMatch.Groups(2).Value

   End If

   Console.WriteLine("[" & Field & "]")

   ' 现在可以处理'Field'

   FieldMatch = FieldMatch.NextMatch

End While

查看所有评论(0)条】

最近评论



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