您好,脚本专家!这里没有错误的正则表达式

The Microsoft Scripting Guys

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

人非圣贤, 孰能无错。(什么,您以为脚本专家团队是在无中生有的吗?)如果说错误也有什么好处的话,那它的好处到底是什么呢?您肯定以为我们会说错误的唯一好处是可以从中吸取教训,对不对?喔,那您可就错啦:您能够从错误中学到的唯一知识就是最好在一开始的时候就不犯错误。

注意:不好意思,我们不会再拿 Microsoft Bob 开玩笑了,至少今天不会。我们以前就因取笑 Microsoft Bob 犯过错误,在当时我们确实从中吸取了教训。以至于每次再谈起 Microsoft Bob 的时候,我们都三缄其口。

老实说,脚本专家们不能去考虑犯错误会带来哪些好处,因为我们毕生都致力于减少我们所犯的错误。(不可否认,减少犯错误机会的唯一方法就是减少我们实际要做的工作。但那又是另外一回事了。)

现在,令人满意的是我们已经在尽力减少我们的错误了;但是如果和我们共事的人仍然继续犯错,那对我们来说可就不太妙了。(又来了,我们不是已经告诉过您:不要再开 Microsoft Bob 的玩笑了!)如果您曾经编写过前端应用程序,比如数据库程序或 Active Directory® 程序(我知道你们大多数人都编过),那您肯定清楚我们在说什么:您的前端数据输入程序实际上就是用户在进行数据输入。

假设您希望用户输入姓氏:Ken,其中第一个字母大写,其余字母小写。那么如果用户输入:KEN,会发生什么情况?假设您希望用户以下列格式输入日期:01/23/2007。那么如果用户输入January 23, 2007,会发生什么情况?假设 — 呃,您可能已经明白我的意思了吧。正如我们所说的,您的前端数据输入程序实际上就是用户在进行数据输入。而且,不管您承不承认,那些输入数据的伙计们总是免不了出错。当然,除非您能采取一些措施来帮助他们避免错误。

确保数据有效

让这场游戏结束吧

您是否想知道脚本专家们整年都在盼望着哪项活动?如果在过去两年中您曾在二月份来过脚本中心,那您可能会猜想这项活动是“冬季脚本编写比赛”。呵呵,您这样想可就错了。脚本专家们盼望的是脚本编写比赛的结束,在经过了两周无休止的兴奋和激动之后,脚本专家们将沐浴在又一次脚本编写比赛成功举行的光环里,然后在接下来的一个月里好好地休息休息。

所以,为了迎接最值得我们期待的年度盛事 — 脚本编写比赛的结束 — 现在该开始 2008 年冬季脚本编写比赛了!欢迎加入脚本中心,从 2008 年 2 月 15 日到 3 月 3 日这两周多的时间里,我们将体验编写脚本的快乐和时下最高水平的脚本编写比赛。

“脚本编写比赛”是测试和提高您脚本编写能力的最佳方式。而且,由于今年脚本专家们希望在比赛后能多睡几天,我们打算把今年的比赛办得比往年更盛大、更出色!再次重申,届时我们将分为两组:初学者组和专家组。这意味着无论您是脚本编写的新手,还是经验丰富的专家,或者介于两者之间,都可以来参加比赛。

今年都有哪些新内容呢?我们将新增一种脚本语言。参赛者可以使用 VBScript、Windows PowerShell,或者新增加的脚本语言 — Perl。(我们知道 Perl 不是新语言,但它是第一次在比赛中使用。)实际比赛时很可能会更加有趣,但由于我们现在是提前几个月做零星的推测,所以将来的情况还得拭目以待。届时您可以到脚本中心来看一看具体情况。

不用猜,肯定会有奖品!什么奖品?呵呵,这可不能事先告诉您 — 到时候来脚本中心寻找答案吧!microsoft.com/technet/scriptcenter/funzone/games/default.mspx

大多数人可能对输入数据的有效性进行了基本的检查。在大多数情况下,这种简单的数据输入验证的确非常有效。怎么?想要使字符串值 strName 的长度小于或等于 20 个字符?下面这一小段代码块可以实现这个功能:

If Len(strName) > 20 Then
    Wscript.Echo "Invalid name; a " & _
    "name must have 20 or fewer characters."
End If

但假设 strName 是新的部件编号,而且部件编号必须符合特定的格式:四位数字加两个大写字母(类似 1234AB)。如果只使用传统的 VBScript,您能验证 strName 是否符合上述部件编号的格式要求吗?是的,当然能。但您肯定不会这样做。相反,您会考虑使用正则表达式。

确保值中只出现数字

正则表达式在 20 世纪 50 年代被提出,最初是用数学符号的形式来描述的。20 世纪 60 年代,此数学模型被引入 QED 文本编辑器,作为在文本文件中搜索字符模式的一种方法。后来 — 后来怎么样了?噢。很明显,并不是每个人都像我们这样沉迷于探究正则表达式的历史。那好吧。言归正传,让我们回到正题。

是否想确保某个变量(这里以变量 strSearchString 为例)仅包含数字?图 1 显示了一种实现方法。

Figure 1 请仅用数字

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

objRegEx.Global = True   
objRegEx.Pattern = "\D"

strSearchString = "353627"

Set colMatches = _
  objRegEx.Execute(strSearchString)  

If colMatches.Count > 0 Then
    Wscript.Echo "Invalid: a " & _
      "non-numeric character was found."
Else
    Wscript.Echo "This is a valid entry."
End If

您可以看到,该脚本首先创建一个 VBScript.RegExp 对象实例,该对象使我们能够在脚本中使用正则表达式。(噢,不!我们才是主讲人!)然后,我们为正则表达式对象的两个关键属性赋值:Global 和 Pattern。

Global 属性决定该脚本是应该匹配所有匹配的模式项,还是找到第一个匹配模式项后就停止。通常,最好把这个值设为 True,即搜索所有匹配的模式项。(默认设置为 False。)在大多数情况下,需要找出所有匹配的模式项。即使不需要搜索出所有项,仅搜索第一个匹配模式项的唯一优势只在于脚本的运行速度更快,因为它在搜索到所需的字符串时就会停止,而不会一直搜索到文件结尾。理论上说,这听起来不错。但实际上,搜索通常很快就会完成,所以您不会注意到有多大的差别。

下面要介绍实质部分:Pattern 是指定我们要查找的字符(以及字符方案)的地方。在示例脚本中,我们要搜索数字 0 到 9 以外的任何字符。如果发现这样的字符(字母、标点符号或任何其他字符),那我们就会知道赋给 strSearchString 的值不是有效值。在正则表达式中,语法 \D 用于匹配任何非数字字符;因此我们为 Pattern 属性赋值 \D。

注意:我们是如何知道 \D 会匹配任何非数字字符的呢?嗯,这可就说来话长了。知道吗?几年前 — 哦,对了。我们可以在 MSDN® 上的 VBScript 语言参考中查一查,网址是:msdn2.microsoft.com/1400241x

在为 Global 和 Pattern 属性赋值之后,我们的下一步工作是为变量 strSearchString 赋值:

strSearchString = "353627"

我们如何才能知道此字符串中是否有非数字字符呢?很简单。只要调用 Execute 方法就可以了,它会搜索变量中的任何非数字字符:

Set colMatches = _
objRegEx.Execute(strSearchString)

当调用 Execute 方法时,任何搜索到的匹配项 — 即 Pattern 的实例 — 都会自动存储在 Matches 集合中。(在我们的示例脚本中,我们将此集合命名为 colMatches。)如果我们想知道我们的变量中是否包含非数字字符(我们确实想知道),则只需检查一下该集合的 Count 属性值,它会告诉我们集合中有多少个项目:

If colMatches.Count > 0 Then
    Wscript.Echo "Invalid: a " & _
      "non-numeric character was found."
Else
    Wscript.Echo "This is a valid entry."
End If

如果 Count 值大于 0,那只能表明一件事:在变量值的某处发现了非数字字符。(如果变量值中的所有字符都是数字,则该集合将为空集,Count 应该等于 0。)在示例脚本中,我们得到的结果是在其中发现了非数字字符。在实际脚本或数据库前端程序中,您很可能得到类似的消息,但程序会回退以便用户再次输入。但我们提醒您一定要重视这种情况;作为脚本专家,我们应该方方面面都要考虑到。

举个例子?就像某天在 TechNet 杂志工作的人可能会真的开始阅读我们每月发给他们的文章。我们情愿他们不要从错误中吸取教训。

噢,问得好:我们为什么不能只用 IsNumeric 函数来确定 strSearchString 是否包含数字呢?当然能,前提是 strSearchString 可以是任何数字。但如果要求 strSearchString 必须是正整数该怎么办?这可能就会出现问题;IsNumeric 将下面两个数字都视为有效数字:

-45
672.69

为什么它们都会被视为有效数字呢?因为它们确实是有效数字;只不过不是正整数而已。但正则表达式会把这些值标记为无效条目,因为正则表达式把减号 (–) 和小数点 (.) 都视为非数字字符。如果您一直在阅读本专栏并纳闷“为什么我要阅读这个专栏呢?”,那好,这里有个绝好的原因。

您说什么?这还不够好?哇,这个月大家怎么这么挑剔啊!那好,让我们看一看可以使用正则表达式完成的其他一些类型的数据输入验证。

确保在值中不出现数字

我们刚才介绍了怎样确保在值中只出现数字。如果反其道而行之,我们想确保在值中不出现数字该怎么办呢?嗯,可以使用这个搜索模式:

objRegEx.Pattern = "[0-9]"

它起什么作用?嗯,在正则表达式这个奇妙古怪的世界中,方括号([ 和 ])能够指定一组字符或特定的字符范围。Pattern [0-9] 是什么意思?它表示我们要搜索的是 0 到 9 内的任意字符 — 换句话说,也就是所有数字。如果我们只想搜索偶数(即确保值中不包括 1、3、5、7 或 9 中的任何一个数字),我们可以使用下面这个模式:

objRegEx.Pattern = "[13579]"

请注意因为没有使用连字符,所以我们不是搜索字符范围。事实上,我们是在精确搜索字符 1、3、5、7 和 9。

当然,模式 [0-9] 只搜索数字,它不会搜索标点符号或其他既非字母也非数字的字符。您觉得我们是否能创建一种可搜索任意非字母字符的模式?当然能。使用正则表达式几乎可以完成您想到的任何事情:

objRegEx.Pattern = "[^A-Z][^a-z]"

在这个例子中,我们实际上在模式中组合了两个标准:我们要搜索 [^A-Z] 和 [^a-z]。您可能已经看出来了,A-Z 表示从大写 A 到大写 Z 的字符范围。那符号 ^ 表示什么意思呢?当包含在一对方括号中时,^ 表示“搜索任何不在此字符集中的字符”。因此,[^A-Z] 表示“搜索任何非大写字母字符。”同理,[^a-z] 表示“搜索任何非小写字母字符。”最终的结果是:此模式表示我们要搜索既不是大写字母也不是小写字母的任意字符。这包括数字、标点符号和任何其他可从键盘上找到的怪异字符。

或者,我们也可以将 IgnoreCase 属性设置为 True:

objRegEx.IgnoreCase = True

这样我们的搜索将不区分字符的大小写。然后我们就可以使用此模式 — 与 IgnoreCase 属性组合 — 搜索既不是大写字母也不是小写字母的任意字符了:

objRegEx.Pattern = "[^A-Z]"

确保部件编号有效

现在,让我们展开想像的翅膀。(相信我们,您会发现正则表达式真的很让人着迷;欢迎来网站 regexlib.com 浏览一些示例。)在上文中曾提到过这样的情况:假设 strName 是新的部件编号,而且部件编号必须符合特定的格式:四位数字加两个大写字母(例如 1234AB)。如何来验证某个变量是否符合此模式要求呢?当然,我们可以这样做:

objRegEx.Pattern = "^\d{4}[A-Z]{2}"

此模式有什么含义呢?太巧了:我们正要对此模式进行解释。在这个例子中,我们有三个搜索标准:

^\d{4} 

\d 表示我们正在搜索数字(范围 0 到 9 内的字符)。那 {4} 是什么意思呢?它表示只精确匹配四个字符(即精确匹配四个数字)。因此,我们要搜索的是连续四个数字。

字符 ^ 又是什么意思呢?嗯,如果用在一对方括号外部,^ 表示该值必须以 ^ 后面的模式开头,也就是说 AA1234AB 不被视为匹配项。(为什么?因为该值实际上是以 AA 开头,而不是以连续四个数字开头。)如果需要,可以使用字符 $ 来指定必须在字符串尾部进行匹配。但这次我们不打算这样做。(嗯,至少在这个示例中不会。)

[A-Z]{2} 

您已经知道了 [A-Z] 这一部分的含义;它表示任意大写字母。那 {2} 呢?答对了:四个数字后面必须跟两个大写字母。

是不是很简单?在这个示例中,通过这种方法,如果 Count 等于 0,则表示有无效值。为什么呢?因为这次我们搜索的不是会使字符串无效的单个字符;相反,我们搜索的是精确的模式匹配。如果没有得到这样的匹配,那就表示有无效值。它还意味着集合的 Count 属性将为 0。

注意:另外两个很有用的结构是:{3,} 和 {3,7}。{3,} 表示必须至少有 3 个连续的字符(或表达式);最少 3 个,但没有最大限制。\d{3,} 匹配 123,也匹配 1234,而且还可以匹配 123456789。{3,7} 表示必须至少有 3 个连续的字符(或表达式),但最多不能超过 7 个。因此可以匹配 123456,但不匹配 123456789(因为它超过了 7 个连续数字)。

下面是另一个有用的模式。假设您的部件编号以 4 个数字开头,然后跟字母 US,最后是两个结束字符(结束字符可以是字母、数字或任意字符)。以下是一种匹配方法:

objRegEx.Pattern = "^\d{4}US.."

您可以看到,此模式以四个数字 (\d) 开头,后面必须跟字符 US(只能是 US;任何其他字符都不会匹配)。在字符 US 后面,我们需要另外两个这样的字符。在正则表达式中用什么代表任意字符?稍等一下;让我查一查... 噢,找到了:任意单个字符用句点 (.) 表示。很明显,这两个字符可以用两个句点来表示:

..

嗯,这太简单了。让我们来个难点的。假设中间的两个字符代表部件制造国家。如果制造工厂在美国、英国和西班牙,那有效的双字符代码包括:US、UK、ES。我们如何在正则表达式中进行多项选择呢?

好的,且看下面这个模式:

objRegEx.Pattern = "^\d{4}(US|UK|ES).."

此处的关键在于这个结构:(US|UK|ES)。我们将三个可接受的值(US、UK 和 ES)放在括号中,并将各个值用管道 (|) 字符分开。这就是在正则表达式中进行多项选择的方法。

但是等一等:我们不是说过可以将多个选项放在方括号内吗?[13579] 那个例子不是也可以进行多项选择吗?是的,确实可以。但是,它只对单个字符有效;[13579] 始终被解释为 1、3、5、7 和 9。要使用 135 和 79 作为选项,则需要使用以下语法:(135|79)。您还可以用圆括号来表示要搜索的单个词语:

objRegEx.Pattern = "(scripting)"

或者直接省略圆括号:

objRegEx.Pattern = "scripting"

注意:最好再了解一下如果需要在搜索术语中包括圆括号该怎么办;如果我想搜索 (scripting) 该怎样做呢?对于这种情况,由于左括号和右括号都是正则表达式中的保留字符,所以要搜索它们就必须在每个字符的前面加上 \。即:

objRegEx.Pattern = "\(scripting\)"

自由学习表达式

本月我们要讲的就是这些内容。如果您想了解更多有关正则表达式的知识,欢迎收听“系统管理员字符串理论”网络广播:正则表达式介绍 (microsoft.com/technet/scriptcenter/webcasts/archive.mspx)。当您开始学习和使用正则表达式时,请谨记 Sophia Loren 的名言:“错误是人为了一生所支付的部分代价。”

喔。脚本专家们的生活真是出人意料地充实啊!

Scripto 博士的脚本谜题

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

2008 年 1 月:卷绕名词

本月 Scripto 博士决定使用 Windows PowerShell。当然,要是不使用 cmdlet,如果您不是行家里手,就无法使用 Windows PowerShell(Scripto 博士可是样样精通的)。对于这个谜题,Scripto 博士在网格中隐藏了一些 cmdlet,需要您把它们找出来。

现在,我们不得不承认如果使用整个 cmdlet,那么这个谜题可能会有一点麻烦。由于 cmdlet 具有动词-名词结构,因此很多词以 Get-、Set- 等开头。所以我们撇开 cmdlet 的动词部分(和连字符),只留下名词。(但我们将告诉您所有动词都是 "Get-"。)

您本月的任务是理清这些 cmdlet 名词。我们已经为您提供了隐藏在谜题中的名词列表。(我们是不是有点太慷慨了?)每个单词都可以垂直和水平、向前或向后排列,但不能对角排列。我们还以第一个单词 (Location) 为例做了说明。祝您好运!

单词列表

Alias
ChildItem
Credential
Date
EventLog
ExecutionPolicy
Location
Member
PSSnapin
TraceSource
Unique
Variable
WMIObject

ANSWER:

Scripto 博士的脚本谜题

答案:卷绕名词,2008 年 1 月

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

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