VBA编程——正则表达式和身份证号码处理

正则表达式(Regular Expression)是一种文本处理方法,功能强大、处理方式灵活,各个开发环境中都有实现。

在VBA中使用正则表达式需要使用RegExp对象,如下面的代码通过CreateObject()函数创建正则表达式对象。

VBA
Dim re
Set re = CreateObject("VBScript.RegExp")

如果需要通过New关键字创建对象,可以打开菜单项“工具”>>“引用”,在引用窗口选中“Microsoft VBScript Regular Expressions 5.5”并点击“确定”按钮确认,然后可以使用如下代码创建正则表达式对象。

VBA
Dim re
Set re = New RegExp

下面的代码演示了一个基本的应用,其功能是判断文本中是否包含数字字符0到9。

VBA
Sub Main()
    Dim re
    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "\d"
    Dim s As String
    s = "abc123efg"
    Debug.Print (re.Test(s))
End Sub

代码会显示True。从本例中可以看到,使用正则表达式的关键在于“模式(Pattern)”的定义,如代码中的“\d”就表示数字。

RegExp对象的属性包括:

  • Global属性指定是否全局搜索,即是否查询全部匹配内容,默认为False,当查询到第一个匹配项则停止搜索。
  • IgnoreCase属性指定是否忽略字母大小写,默认为False。
  • Multiline属性指定是否使用多行模式,默认为False。
  • Pattern属性指定模式。

RegExp对象的方法包括:

  • Execute()方法,搜索文本,并返回由所有匹配内容组成的集合。
  • Replace()方法,替换区域的内容,如将匹配内容替换为空白则可以完成删除操作。
  • Test()方法,测试文本是否与模式匹配,匹配时返回True,否则返回False。

正则表达式的模式主要可以定义匹配的内容、匹配的次数、匹配的位置、是否使用贪婪模式等信息;然后,根据模式找到文本中的内容,并可以进行删除、替换等操作。下面就先来了解模式的定义。

模式定义

首先来看定义匹配内容,如:

  • 英文圆点(.),匹配除换行符(\n)以外的任何字符。
  • \d和\D,分别匹配数字和非数字。
  • \w和\W,\w匹配英文字符、数字和下划线。\W则匹配\w以外的字符。
  • \s和\S,\s表示不可见字符,如空格符、回车符(\r)、换行符(\n)等。\S表示可见字符。
  • \f,换页符。
  • \n,换行符。
  • \r,回车符。
  • \t,水平制表符。
  • \v,垂直制表符。
  • \<数值>,字符的八进制编码。
  • \x<数值>,字符的十六进制编码。
  • \u<数值>,字符的Unicode编码。
  • \<字符>,对特殊字符进行转义,如匹配圆点使用\.,匹配星号使用\*、匹配\符号使用\\等。

匹配指定范围内的一个字符时,可以将字符范围定义在一对方括号中,可以一一列出,也可以使用范围,如只允许y或n可以写成[yn];如果不包含指定范围内的字符,可以在方括号中使用^符号开始,如[^yn]表示不允许y或n字符。指定范围是使用连接号,如[1-5]表示允许1到5数字中的一个,不包含1到5的数字使用[^1-5]。

多个匹配规则可以使用竖线(|)分隔,如十六进制数值可以使用0到9和字母A到F,可以写为“\d|[A-F]”。多个规则也可以使用一对圆括号定义为一组,如“(\d|[A-F])”。

模式中除指定匹配内容,还可以指定匹配的数量,如:

  • 星号(*),匹配0个或多个。*?为非贪婪模式,会尽可能少地匹配字符,稍后会演示贪婪模式和非贪婪模式之间的区别。
  • 加号(+),匹配1个或多个。+?为非贪婪模式。
  • 问号(?),匹配0个或1个。??为非贪婪模式。
  • {n},匹配n次。
  • {n,},匹配最少n次。
  • {n,m},匹配最小n次,最大m次。{n,m}?为非贪婪模式。

模式中匹配位置的常用字符有:

  • 符号^,放在模式最前面表示使用^后面的模式开始。
  • 符号$,放在模式最后面表示使用$前面的模式结束。
  • \b,表示单词边界,如单词前和单词后等。如"\btom\b"可以匹配单词tom,而不能匹配tommy。\B表示非单词边界。

接下来通过几个示例了解模式的基本应用,首先看下面的代码。

VBA
Sub Main()
    Dim re
    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "^1\d{10}$"
    Debug.Print (re.Test("12345678910"))
End Sub

代码中定义的模式使用数字1开始,并且结尾有10个数字,可以对11位手机号进行初步的格式判断。

下面的代码可以显示匹配的内容。

VBA
Sub Main()
    Dim re, mc
    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "a+"
    re.Global = True
    Set mc = re.Execute("aaa,aa,a")
    For Each m In mc
        Debug.Print (m.Value)
    Next
End Sub

代码使用使用默认的贪婪模式查询一个或多个a组成的内容,代码会显示aaa、aa、a。接下来,将re.Pattern属性设置为"a+?",即使用非贪婪模式,此时只要匹配一个a即返回匹配结果,然后继续向下搜索,执行结果会显示6个a。

替换与删除内容

使用RegExp对象的Replace()方法可以将匹配的内容替换为新的内容,其中,参数1为原始文本,参数2指定匹配内容替换文本。

下面的代码会将两个a的内容修改为"**"。

VBA
Sub Main()
    Dim re, mc, s1, s2
    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "a{2}"
    re.Global = True
    s1 = "aaa,aa,a"
    s2 = re.Replace(s1, "**")
    Debug.Print (s2)
End Sub

执行代码会显示"**a,**,a"。

下面的代码会删除三个a或两个a的部分。

VBA
Sub Main()
    Dim re, mc, s1, s2
    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "a{2,3}"
    re.Global = True
    s1 = "aaa,aa,a"
    s2 = re.Replace(s1, "")
    Debug.Print (s2)
End Sub

执行代码会显示",,a"。

处理匹配结果

RegExp对象的Execute()方法返回匹配结果的集合,其中,可以使用Count属性获取匹配内容的数量,使用索引访问匹配对象(Match对象)。每个Match对象又可以使用Value属性返回匹配内容,使用Length属性获取匹配内容的字符数量,FirstIndex属性获取匹配内容在原文本中的起始位置。下面的代码演示了相关应用。

VBA
Sub Main()
    Dim re, mc
    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "\bt.{1}m\b"
    re.Global = True
    Set mc = re.Execute("john,tom,smith,tim,")
    Debug.Print (mc.Count)
    '
    For Each m In mc
        Debug.Print (m.FirstIndex & "," & m.Length & "," & m.Value)
    Next
End Sub

代码中的模式指定为t开始、m结束,中间有一个字符的内容;执行代码首先会显示匹配内容的数量2,然后分别显示了tom和tim在字符串的起始位置索引值、字符串长度和匹配的内容。

处理18位身份证号码

首先对18位身份证号码进行基本的规则检查,如下面的代码,在mComm模块中定义card18BaseCheck()函数,用于检查18号身份证号码的基本规则。

VBA
' 18位身份证号码基本规则验证
Function card18BaseCheck(s As String) As Boolean
    Dim re
    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "^\d{6}[12]\d{10}[0-9X]$"
    If re.test(s) = False Then
        card18BaseCheck = False
        Exit Function
    End If
    ' 生日日期格式检查
    Dim strDate As String
    strDate = Mid(s, 7, 4) & "-" & Mid(s, 11, 2) & "-" & Mid(s, 13, 2)
    card18BaseCheck = IsDate(strDate)
End Function

对于18位身份证号码来说,前6位为行政区划,应为6位数字使用“^\d{6}”定义;第7位开始是年份,这里指定第7位只允许1和2,不考虑上一千年和下一千年,使用“[12]”定义;接下来是10位数字,包括年份后3位、2位月、2位日、3位编号,使用“\d{10}”定义;最后一位是校验码,可以是0到9的数字或X,使用“[0-9X]$”定义。

通过模式初步判断号码格式后,提取了其中的年、月、日,并使用连接号(-)组成了日期格式的字符串,最后用IsDate()函数判断此字符串是否正确的日期值。

card18BaseCheck()函数中还需要注意,第一,是否是正确的行政区划代码需要有完整的代码表进行判断;第二,已存在的身份证中的生日应该小于或等于当天(除非是穿越了^_^),在下一篇文章介绍日期和时间的处理以后可以根据需要添加判断条件。

接下来是通过身份证前17位计算第18位的校验码,通过以mComm模块中定义的card18CheckCode()函数来实现,如下面的代码。

VBA
' 计算身份证第18位
Function card18CheckCode(s As String) As String
    ' 基本规则验证
    If card18BaseCheck(s) = False Then
        card18CheckCode = ""
        Exit Function
    End If
    '17位对应系数
    Dim factor
    factor = Array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2)
    ' 计算前17位与系数相乘的和
    Dim sum As Long
    sum = 0
    For i = 1 To 17
        sum = sum + Int(Mid(s, i, 1)) * factor(i - 1)
    Next
    ' 求和的余数
    Dim n As Long
    n = sum Mod 11
    ' 返回对应的字符
    Dim chkCode
    chkCode = Array("1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2")
    card18CheckCode = chkCode(n)
End Function

代码中,首先对18位身份证号码的基本规则进行判断,如果不正确则返回空字符串;然后,计算前17位与对应系数乘积的和;接下来计算和除以11的余数,余数可能是0到10;最后通过余数做为索引返回对应的字符。大家可以用真实的身份证号码进行测试,如下面的代码。

VBA
Sub Main()
    Dim s As String
    s = "请填写身份证号码"
    Debug.Print (card18CheckCode(s))
End Sub

可以修改变量s的值为18位身份证号码来观察生成的校验码。

最后,在mComm模块中定义card18Check()函数用于判断18位身份证号码的格式是否正确,如下面的代码。

VBA
' 验证18位身份证号码
Function card18Check(s As String) As Boolean
    Dim chkCode As String
    chkCode = card18CheckCode(s)
    If Len(chkCode) <> 1 Then
        card18Check = False
    Else
        card18Check = (chkCode = Mid(s, 18, 1))
    End If
End Function

可以通过下面的代码测试card18Check()函数。

VBA
Sub Main()
    Dim s As String
    s = "请填写身份证号码"
    Debug.Print (card18Check(s))
End Sub

代码中可以修改变量s的值来观察运行结果。

正则表达式还有一些特性,如多行模式、组等,有需要可以深入学习。