您好,脚本专家!正则表达式令您耳目一新

The Microsoft Scripting Guys

下载这篇文章的代码: HeyScriptingGuy2008_05.exe (150KB)

时间倒回至 2007 年 11 月 ,在去巴塞罗那参加 TechEd IT 论坛的途中,脚本专家们在巴黎逗留了一天。在中转途中的这一天里,我们有幸参观了卢浮宫 — 巴黎闻名于世的艺术博物馆。

那脚本专家们是如何找到卢浮宫的?这很简单:我们走向巴黎圣母院,然后左转。

噢,您想问的是我们在卢浮宫愉快吗?大部分时间都非常愉快。。唯一的问题是,卢浮宫像许多博物馆一样,“只准看,不准摸”。每个人都知道,如果为蒙娜丽莎添上眉毛的话,那她看上去会更漂亮。但不知什么原因,如果您妄图修改任何一幅画,都会让管理卢浮宫的人员气急败坏。

**备注:**实际上,两位脚本专家都非常喜欢蒙娜丽莎。这也是一个惊喜;尽管有诸多宣传,但我们仍担心看到的可能是一幅仿制品。不过它的确是真品;酷毙了。(尽管它可以添上一些眉毛。)但是,有趣的是我们都对同具盛名的“米洛的维纳斯”感到失望。我们两个都没发现它有什么吸引人的地方,而且撰写本专栏的脚本专家对于“米洛的维纳斯”所传达的总体思想感到非常迷惑。一尊没有手臂的女性雕像?那她该如何拖地或洗盘子呢?!

至可敬的女性读者(假设她们当中还有人没有拂袖而去):明显这是一个拼写错误。我们其实想说的是:一尊没有手臂的女性雕像?但是,她仍可以完成比任何男人多一倍的工作,而且完全不会出现一丝差错。

脚本专家对于最初的陈述可能产生的误解深感抱歉。

不管怎样,当我们把目光投向卢浮宫的稀释珍宝时,两位脚本专家突然冒出同样的想法:卫生间在哪里?在寻找途中,撰写本专栏的脚本专家又有了一个想法:脚本专家真虚伪。虽然我们对卢浮宫不让我们修改“蒙娜丽莎”感到有点气愤,但我们自己还不是犯过类似的错误。在 2008 年 1 月刊的《TechNet 杂志》中,**我们曾写过有关在脚本中使用正则表达式的文章。该文章是“只准看,不准摸”的最佳示例:我们介绍了如何使用正则表达式来找出文本文件中的问题,但我们并未介绍如何解决这些问题。见鬼!

**备注:**既然脚本专家是在 2007 年 11 月去的卢浮宫,那撰写本专栏的脚本专家是如何突然想起 2008 年 1 月才出版的《TechNet 杂志》**中的一篇文章呢?喔;简直是个谜,不是吗?大概与雷蒙德与巴黎之间的时差有关吧。

然而幸运的是,与管理卢浮宫的人有所不同,脚本专家们非常乐意承认自己犯过的错误。我们不应该只向您说明如何使用正则表达式执行搜索;还应说明如何使用正则表达式来执行替换。实际上,我们应向您展示类似图 1 中所示的脚本。

Figure 1 搜索和替换

      Set objRegEx = _
    CreateObject("VBScript.RegExp")

objRegEx.Global = True   
objRegEx.IgnoreCase = True
objRegEx.Pattern = "Mona Lisa"

strSearchString = _
    "The Mona Lisa is in the Louvre."
strNewString = _
    objRegEx.Replace(strSearchString, _
                     "La Gioconda")

Wscript.Echo strNewString 

现在,实话告诉您,这才是正则表达式最常见的用途:即将字符串值 Mona Lisa 的所有实例替换成 La Gioconda(意大利语“现在,我应该把这些眉毛放在哪里?”)。不可否认,使用 VBScript Replace 函数执行此替换操作要简单得多。但是,不用担心:我们将使用这个简短的脚本来解释如何使用正则表达式执行搜索和替换操作。完成后,我们将介绍可使用这些表达式实现的一些更为奇特的功能。

正如您所看到的那样,这个脚本实际上没有太多的内容。首先创建一个 VBScript.RegExp 对象实例;毫无疑问,该对象是为了使我们能够在 VBScript 脚本中使用正则表达式。创建对象之后,为对象的三个属性赋值:

Global 通过将此属性设为 True,我们告知脚本搜索(并替换)在目标文本中找到的所有 Mona Lisa 实例。如果 Global 属性为 False(默认值),则脚本将仅搜索并替换第一个 Mona Lisa 实例。

IgnoreCase 将 IgnoreCase 设为 True 即告诉脚本搜索不区分大小写,换句话说,我们希望将 mona lisa 和 Mona Lisa 视为相同的字符串。默认情况下,VBScript 执行区分大小写的搜索,即意味着由于大写字母和小写字母的原因,mona lisa 和 Mona Lisa 将被视为完全不同的值。

Pattern Pattern 属性保存我们要搜索的值。在本例中,我们仅搜索一个简单的字符串值:Mona Lisa。

接下来,将要搜索的文本分配给名为 strSearchString 的变量:

strSearchString = "The Mona Lisa is in the Louvre."

然后,调用正则表达式方法 Replace,并为该方法传递两个参数:要搜索的目标文本(变量 strSearchString)和替换文本(La Gioconda)。即以下表达式所实现的功能:

strNewString = objRegEx.Replace(strSearchString, "La Gioconda")

就是这样。修改后的文本将存储在变量 strNewString 中。如果现在回显 strNewString 的值,我们将得到以下结果:

The La Gioconda is in the Louvre.

虽然语法可能有点问题,但可以获得个大致的了解。

正如我们前面提到的,一切都进行得很好很顺利,但太过于复杂了;我们可使用以下几行代码来实现完全相同的功能(实际上,如果希望的话,我们甚至可在一行代码中实现此功能):

strSearchString = "The Mona Lisa is in the Louvre."
strNewString = Replace(strSearchString, "Mona Lisa", "La Gioconda")
Wscript.Echo strNewString

换句话说,让我们来想一想有没有使用正则表达式可以做到而使用 VBScript 的 Replace 函数无法实现的一些非常有趣的功能。

没人知道吗?好的,这里就有一个。我们脚本专家常常不得不将文本从一种类型的文档复制到另一种类型的文档。有时一切顺利;而有时则不起作用。在不起作用时,我们常常会遇到奇怪的单词间距问题,从而产生类似以下的文本:

Myer Ken, Vice President, Sales and Services

呀;看看这些附加的空格!而且,在这种情况下,Replace 函数的功能有限。为什么呢?好吧,让我们看看包含随机数量附加空格的情况:单词之间可能会有 7 个空格,也可能只有 2 个空格,或者也可能有 6 个空格。这样使得很难使用 Replace 来更正此问题。例如,如果尝试搜索 2 个连续的空格(使用 1 个空格替换这 2 个空格),那我们可以得到以下字符串:

Myer Ken, Vice President, Sales and  Services

这样虽然好一些,但好不了多少。还有一个办法,但它需要我们搜索任意数量的空格(如 39 个);执行替换;然后从起始数字减去 1;搜索 38 个空格;执行替换;依此类推。此外,可使用简单得多(且更加万无一失)的以下正则表达式脚本:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = " {2,}"

strSearchString = _
"Myer Ken, Vice President, Sales and Services"
strNewString = objRegEx.Replace(strSearchString," ")

Wscript.Echo strNewString

此脚本的关键(同时也是大多数正则表达式的关键)是 Pattern:

objRegEx.Pattern = " {2,}"

即搜索 2 个(或更多)连续的空格。我们如何知道此 Pattern 是搜索 2 个(或更多)空格?好的,在双引号标记内,我们可以看到一个空格,后面跟着这样的结构:{2,}。在正则表达式语法中,它表示搜索至少 2 个连续的前导字符(在本例中,即一个空格)的实例。如果有 3 个、4 个或 937 个连续的空格呢?没问题;也可将它们统统找出来。(如果出于某种原因,我们希望找出至少 2 个但不超过 8 个空格,那我们可使用语法 {2,8},其中 8 指定最大匹配数。)

换句话说,每当发现 2 个或更多连续的空格,我们都可以找出所有此类连续的空格,并使用 1 个空格来替换它们。它会对我们最初的那个包含附加空格的字符串值造成什么影响呢?结果如下:

Myer Ken, Vice President, Sales and Services

看到了吗?脚本专家确实可让事情变得更好。现在,要是卢浮宫的管理员能给我们一次修改“蒙娜丽莎”的机会该有多好啊。

还一个非常有趣而且并不罕见的情况。假设贵公司有一个电话号码簿,所有电话号码的格式均如下所示:

555-123-4567

但是现在,您的老板决定将所有电话号码更改为以下格式:

(555) 123-4567

您究竟该如何重新设置这些电话号码的格式呢?嗯,如果足够勇敢,我们可能会建议您使用类似如下的脚本:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\d{3})-(\d{3})-(\d{4})"

strSearchString = "555-123-4567"
strNewString = objRegEx.Replace _
(strSearchString, "($1) $2-$3")

Wscript.Echo strNewString

它实现的功能是搜索后跟破折号的 3 个数字 (\d{3}),然后是另外 3 个数字和破折号,接着是 4 个数字。换句话说,我们将搜索以下字符串,其中每个 X 代表从 0 到 9 的一个数字:

XXX-XXX-XXXX

备注:我们如何知道 \d{3} 会告诉脚本搜索 3 个连续的数字?喔,最好回忆一下,我们好像在哪里见过这样的情况。事实上,应该是在电影“达芬奇密码”令人震惊的最后一幕中,或者是在线 MSDN® 的 VBScript 语言参考中(请参阅 go.microsoft.com/fwlink/?LinkID=111387)。

现在,最酷的是我们可以使用正则表达式来搜索任意电话号码。但是,还有一个大问题。毕竟,我们无法使用任意电话号码替换该电话号码,相反,必须使用完全相同的电话号码,只是格式稍有不同。我们究竟该如何做呢?

为什么不试一试使用以下替换文本:

"($1) $2-$3"

$1、$2 和 $3 是正则表达式“反向引用”的示例。反向引用仅指可保存并重用的文本部分。在此特殊脚本中,我们将查找三个“子匹配”:

  • 一组 3 位数字
  • 另一组 3 位数字
  • 一组 4 位数字

自动向每个子匹配分配一个反向引用:第一个子匹配是 $1;第二个是 $2;以此类推,直到 $9。换句话说,在本脚本中,电话号码的三个部分都被自动分配了一个反向引用(如图 2 所示)。

Figure 2 电话号码反向引用

电话号码部分 反向引用
555 $1
123 $2
4567 $3

在替换字符串中,我们使用这些反向引用来确保重用正确的电话号码。我们的替换文本表示:提取第一个反向引用 ($1),并将其放在括号中。留一个空格,然后插入第二个反向引用 ($2),后面跟一个破折号。最后,添加第三个反向引用 ($3)。

该脚本将为我们带来什么结果呢?它将提供如下所示的电话号码:

(555) 123-4567

还不错吧,应该说很不错。

还可对此电话号码脚本进行变更。假设您的组织安装了全新的电话系统,并且由于方案变更,所有电话号码现在都将拥有相同的前缀,比如以前以 666、777 或 888 打头的号码现在都将以 333 打头。我们能否同时重新设置电话号码的格式并更改电话号码的前缀?噢,当然可以:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\d{3})-(\d{3})-(\d{4})"

strSearchString = "555-123-4567"
strNewString = objRegEx.Replace _
(strSearchString,"($1) 333-$3")

Wscript.Echo strNewString

看明白这段脚本的含义了吗?我们仅删除了替换文本中旧的前缀(反向引用 $2);并在其位置换用硬编码且标准的前缀值 333。在运行该修改后的脚本之后,电话号码 555-123-4567 将变成什么样子呢?完成后应类似如下:

(555) 333-4567

反向引用还有另一种常见的用法。假设我们有如下所示的字符串值:

Myer, Ken

有没有方法可以调换值的顺序,并使其像如下方式显示姓名:

Ken Myer

噢,要是连这都做不到,那我们岂不是太蠢了吗?以下正是可以执行此类事情的脚本:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\S+), (\S+)"

strSearchString = "Myer, Ken"
strNewString = objRegEx.Replace _

strSearchString,"$2 $1")

Wscript.Echo strNewString

在该特殊脚本中,我们将搜索后接逗号的一个词(即 \S+),然后是一个空格,再接着是另一个词。(在本例中,我们使用 \S+ 来代表一个“词”。)结构 \S+ 表示任意连续的非空格字符组。换句话说,可使用字母、数字、符号;事实上,应该说是除空格、制表符或回车换行符之外的所有字符。正如您所看到的那样,我们希望搜索以下两个子匹配:一个代表姓氏 ($1),另一个代表名字 ($2)。因此,可通过使用以下语法将用户名显示为“FirstName LastName”格式:

"$2 $1"

逗号去哪里了?噢,很明显,我们不再需要它了,因此只需把它丢掉即可。

备注:非常有趣,由于某些原因,我们也开始考虑使用脚本编辑器。嗯......

在结束今天的讨论之前,让我们再向您展示一个示例。(哦,好的,不是结束今天的讨论,而是在结束本月的讨论之前。)虽然并非绝对的万无一失;但毕竟,我们不希望像本文这样的入门文章太过于复杂。(而正则表达式确实可能演变成非常复杂的内容。)尽管如此,我们还是介绍一下以下脚本,在大多数情况下,此脚本可删除一个值(如 0000.34500044)前面的零:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "\b0{1,}\."

strSearchString = _
"The final value was 0000.34500044."
strNewString = objRegEx.Replace _
strSearchString,".")

script.Echo strNewString

与往常一样,该脚本起作用的唯一原因在于模式:“\b0{1,}\.”首先查找词边界 (\b);从而确保我们不会删除值(如 100.546)中的零。然后,搜索后跟小数点 (\.) 的一个或多个零“0{1,}”。如果找到匹配的模式,我们将使用一个小数点“.”替换所有这些零(和小数点)。如果一些按计划执行,则可将字符串转换成以下格式:

The final value was .34500044.

本月我们要讲的就是这些内容。在结束之前,需要强调的是,甚至在油漆尚未完全干透之前,蒙娜丽莎就已经是相当具有争议性的话题。这位神秘的妇女是谁?她为什么要这样微笑?她为什么没有眉毛?许多艺术历史学家认为蒙娜丽莎甚至并非女性,该画像描绘的其实是莱昂纳多·达芬奇自己。(如果真是这样,那他也太别出心裁了。)更有甚者,Unarius Educational Foundation 宣称该画像实际上是莱昂纳多在“更高世界”中的“灵魂写照”,并且正是该“灵魂写照”指引莱昂纳多成为举世闻名的大画家。非常巧合的是,它与本月的“您好,脚本专家!”撰写方式不符而合。

即表示请将您的所有投诉都发送到 the-twin-soul-of-the-scripting-guy-who-writes-that-column@the-other-microsoft.com。谢谢。

Scripto 博士的脚本谜题

每月一次的挑战!不仅测试您的解谜技能,还测试您的脚本编写技能。

2008 年 5 月:脚本数独

本月,我们将要玩的游戏是数独,但有一点点难度。不是在格子中使用数字 1 至 9,而是使用组成一个 Windows PowerShell™ cmdlet 的字母和符号。在最终解决方案中,其中一行将拼写出这个 cmdlet 的名称。

备注:如果不知道数独的玩法,Internet 上提供此游戏说明的网站多达数千个,我们就不在这里罗嗦啦,抱歉。

ANSWER:

Scripto博士的脚本谜题

答案:脚本数独,2008 年 5 月

The Microsoft Scripting Guys 为 Microsoft 工作,也就是受雇于 Microsoft。在玩、教或看棒球(以及各种其他活动)的闲暇之余,他们还负责维护 TechNet 脚本中心。要查看相关信息,请登录 www.scriptingguys.com

© 2008 Microsoft Corporation 与 CMP Media, LLC.保留所有权利;不得对全文或部分内容进行复制.