Регулярные выражения

Регулярные выражения предназначены для выполнения сложного поиска или замены в строке. В языке VBA нет встроенной поддержки регулярных выражений, однако можно получить доступ через объект VBScript.RegExp. Создать объект позволяет следующий код:

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

Примечание

Мне знакомы программисты, которые являются профессионалами, но так и не научились использовать регулярные выражения. Если вы только начинаете разбираться в программировании, то советую отложить изучение этой главы, так как синтаксис регулярных выражений очень сложен для понимания. Однако, если вы поймете принцип постоения шаблона, то в ваших руках окажется очень мощный инструмент поиска и замены в строке.

Существует и обратная сторона медали. Очень часто начинающие программисты, поняв синтаксис регулярных выражений, начинают использовать их направо и налево. Регулярные выражения являются очень мощным средством, но следует учитывать, что поиск выполняется значительно медленнее, чем при использовании обычных функций, предназначенных для работы со строками. Поэтому используйте регулярные выражения только там, где это действительно оправданно. В остальных случаях используйте обычные функции.

Синтаксис регулярных выражений

После создания объекта нужно задать значения следующим свойствам:

Dim re
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "^[а-яё]+$"        ' Задаем шаблон
re.IgnoreCase = False           ' Зависит от регистра
Debug.Print re.Test("АБВГДЕЁ")  ' False (не совпадает)
re.IgnoreCase = True            ' Не зависит от регистра
Debug.Print re.Test("АБВГДЕЁ")  ' True  (совпадает)
Dim re
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "[0-9]+"
re.Global = False
Debug.Print re.Replace("12 5 45", "+")  ' + 5 45
re.Global = True                        ' Глобальный поиск
Debug.Print re.Replace("12 5 45", "+")  ' + + +

Шаблон может содержать комбинации следующих символов, имеющих специальное значение:

Внутри шаблона символы ., ^, $, *, +, ?, {, [, ], \, |, ( и ) имеют специальное значение. Если эти символы должны трактоваться как есть, то их следует экранировать с помощью слеша. Некоторые специальные символы теряют свое специальное значение, если их разместить внутри квадратных скобок. В этом случае экранировать их не нужно. Например, метасимвол "точка" соответствует любому символу, кроме символа перевода строки. Если необходимо найти именно точку, то перед точкой необходимо указать символ \ или разместить точку внутри квадратных скобок ([.]). Продемонстрируем это на примере проверки правильности введенной даты (листинг 7.1).

Листинг 7.1. Проверка правильности ввода даты

Dim re, d As String
Set re = CreateObject("VBScript.RegExp")

d = "29,12.2012"  ' Вместо точки указана запятая

re.Pattern = "^[0-3][0-9].[01][0-9].[12][09][0-9][0-9]$"
' Символ "\" не указан перед точкой
If re.Test(d) = True Then
   Debug.Print "Дата введена правильно"
Else
   Debug.Print "Дата введена неправильно"
End If
' Так как точка означает любой символ,
' выведет: Дата введена правильно

re.Pattern = "^[0-3][0-9]\.[01][0-9]\.[12][09][0-9][0-9]$"
' Символ "\" указан перед точкой
If re.Test(d) = True Then
   Debug.Print "Дата введена правильно"
Else
   Debug.Print "Дата введена неправильно"
End If
' Так как перед точкой указан символ "\",
' выведет: Дата введена неправильно

re.Pattern = "^[0-3][0-9][.][01][0-9][.][12][09][0-9][0-9]$"
' Точка внутри квадратных скобок
If re.Test(d) = True Then
   Debug.Print "Дата введена правильно"
Else
   Debug.Print "Дата введена неправильно"
End If
' Выведет: Дата введена неправильно

Примечание

Регулярное выражение не может проверить реальность существования даты. Можно проверить только совпадение с шаблоном. Например, дата 30.02.2012 является допустимой для шаблона, но в реальности не существует. В большинстве случаев проверки на совпадение с шаблоном достаточно, но если необходима проверка реальности существования даты, то следует воспользоваться функциями для работы с датами, которые мы рассмотрим в следующей главе.

Метасимвол "точка" соответствует любому символу, кроме символа перевода строки. Чтобы обозначить любой символ, включая символ перевода строки, следует воспользоваться комбинаций [\s\S]. Пример:

Dim re
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "^.$"
Debug.Print re.Test(vbLf) ' False
re.Pattern = "^[\s\S]$"
Debug.Print re.Test(vbLf) ' True

В этом примере мы осуществляли привязку к началу и концу строки с помощью следующих метасимволов:

Если свойство MultiLine равно значению True, то поиск производится в строке, состоящей из нескольких подстрок, разделенных символом новой строки (\n). В этом случае символ ^ соответствует привязке к началу каждой подстроки, а символ $ соответствует позиции перед символом перевода строки (листинг 7.2).

Листинг 7.2. Пример использования многострочного режима

Dim re, Matches, Item, s As String
s = "123" & vbLf & "456"
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "^.+$"      ' Точка не соответствует \n
re.Global = True         ' Глобальный поиск
re.MultiLine = False     ' Однострочный режим
Debug.Print re.Test(s)   ' False (Ничего не найдено)
re.MultiLine = True      ' Многострочный режим
Set Matches = re.Execute(s)
For Each Item In Matches
   Debug.Print Item.Value
Next
' 123
' 456

Привязку к началу и концу строки следует использовать, если строка должна полностью соответствовать регулярному выражению. Например, привязку нужно использовать для проверки, содержит ли строка число (листинг 7.3).

Листинг 7.3. Проверка наличия целого числа в строке

Dim re
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "^[0-9]+$"
re.MultiLine = False             ' Однострочный режим
Debug.Print re.Test("245")       ' True
Debug.Print re.Test("Строка245") ' False

Если убрать привязку к началу и концу строки, то любая строка, содержащая хотя бы одну цифру, будет соответствовать шаблону (листинг 7.4).

Листинг 7.4. Отсутствие привязки к началу или концу строки

re.Pattern = "[0-9]+"
Debug.Print re.Test("Строка245") ' True

Можно указать привязку только к началу или только к концу строки (листинг 7.5).

Листинг 7.5. Привязка к началу и концу строки

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

re.Pattern = "[0-9]+$"
If re.Test("Строка245") = True Then
   Debug.Print "Есть число в конце строки"
Else
   Debug.Print "Нет числа в конце строки"
End If
' Выведет: Есть число в конце строки

re.Pattern = "^[0-9]+"
If re.Test("Строка245") = True Then
   Debug.Print "Есть число в начале строки"
Else
   Debug.Print "Нет числа в начале строки"
End If
' Выведет: Нет числа в начале строки

В квадратных скобках [] можно указать символы, которые могут встречаться на этом месте в строке. Можно перечислять символы подряд или указать диапазон через тире:

Внимание!

Буква "ё" не входит в диапазон [а-я].

Значение можно инвертировать, если после первой скобки указать символ ^. Таким образом можно указать символы, которых не должно быть на этом месте в строке:

Как вы уже знаете, точка теряет свое специальное значение, если ее заключить в квадратные скобки. Кроме того, внутри квадратных скобок могут встретиться символы, которые имеют специальное значение (например, ^ и -). Символ ^ теряет свое специальное значение, если он не расположен сразу после открывающей квадратной скобки. Чтобы отменить специальное значение символа -, его необходимо указать после перечисления всех символов, перед закрывающей квадратной скобкой, или сразу после открывающей квадратной скобки. Все специальные символы можно сделать обычными, если перед ними указать символ \.

Вместо перечисления символов можно использовать стандартные классы:

Количество вхождений символа в строку задается с помощью квантификаторов:

Все квантификаторы являются "жадными". При поиске соответствия ищется самая длинная подстрока, соответствующая шаблону, и не учитываются более короткие соответствия. Рассмотрим это на примере. Получим содержимое всех тегов <b>, вместе с тегами:

Dim re, Matches, Item, s As String
s = "<b>Text1</b>Text2<b>Text3</b>"
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "<b>.*</b>"
re.Global = True         ' Глобальный поиск
Set Matches = re.Execute(s)
For Each Item In Matches
   Debug.Print Item.Value
Next
' <b>Text1</b>Text2<b>Text3</b>

Вместо желаемого результата мы получили полностью строку. Чтобы ограничить "жадность", необходимо после квантификатора указать символ ? (листинг 7.6).

Листинг 7.6. Ограничение жадности квантификаторов

Dim re, Matches, Item, s As String
s = "<b>Text1</b>Text2<b>Text3</b>"
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "<b>.*?</b>"
re.Global = True         ' Глобальный поиск
Set Matches = re.Execute(s)
For Each Item In Matches
   Debug.Print Item.Value
Next
' <b>Text1</b>
' <b>Text3</b>

Этот код вывел то, что мы искали. Если необходимо получить содержимое без тегов, то нужный фрагмент внутри шаблона следует разместить внутри круглых скобок (листинг 7.7). В этом случае фрагмент будет доступен через коллекцию Submatches.

Листинг 7.7. Получение значения определенного фрагмента

Dim re, Matches, Submatches, Item, SubItem, s As String
s = "<b>Text1</b>Text2<b>Text3</b>"
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "<b>(.*?)</b>"
re.Global = True         ' Глобальный поиск
Set Matches = re.Execute(s)
For Each Item In Matches
   Debug.Print Item.Value
   Set Submatches = Item.Submatches
   For Each SubItem In Submatches
      Debug.Print "      " & SubItem
   Next
Next
' <b>Text1</b>
'       Text1
' <b>Text3</b>
'       Text3

Круглые скобки часто используются для группировки фрагментов внутри шаблона. В этом случае не требуется, чтобы фрагмент запоминался и был доступен в результатах поиска. Чтобы избежать захвата фрагмента после открывающей круглой скобки следует разместить символы ?: (листинг 7.8).

Листинг 7.8. Ограничение захвата фрагмента

Dim re, Matches, Submatches, Item, SubItem, s As String
s = "test text"
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "([a-z]+((st)|(xt)))"
re.Global = True         ' Глобальный поиск
Set Matches = re.Execute(s)
For Each Item In Matches
   Debug.Print Item.Value
   Set Submatches = Item.Submatches
   For Each SubItem In Submatches
      Debug.Print "      " & SubItem
   Next
Next
' test
'       test
'       st
'       st
'
' text
'       text
'       xt
'
'       xt
re.Pattern = "([a-z]+(?:(?:st)|(?:xt)))"
Set Matches = re.Execute(s)
For Each Item In Matches
   Debug.Print Item.Value
   Set Submatches = Item.Submatches
   For Each SubItem In Submatches
      Debug.Print "      " & SubItem
   Next
Next
' test
'       test
' text
'       text

В первом примере мы получили два фрагмента полностью соответствующих шаблону и по четыре субфрагмента, которые соответствуют фрагментам, заключенным в шаблоне в круглые скобки. Три последних субфрагмента являются лишними. Чтобы они не выводились в результатах, мы добавили символы ?: после каждой открывающей круглой скобкой. Теперь результат состоит только из фрагментов, полностью соответствующих регулярному выражению.

Обратите внимание на регулярное выражение в предыдущем примере:

"([a-z]+((st)|(xt)))"

Здесь мы использовали метасимвол |, который позволяет сделать выбор между альтернативными значениями. Выражение n|m соответствует одному из символов: n или m. Пример:

красн((ая)|(ое)) — красная или красное, но не красный.

К найденному фрагменту в круглых скобках внутри шаблона можно обратиться с помощью механизма обратных ссылок. Для этого порядковый номер круглых скобок в шаблоне указывается после слеша, например, \1. Нумерация скобок внутри шаблона начинается с 1. Для примера получим текст между одинаковыми парными тегами (листинг 7.9).

Листинг 7.9. Обратные ссылки

Dim re, Matches, Submatches, Item, SubItem, s As String
s = "<b>Text1</b>Text2<I>Text3</I><b>Text4</b>"
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "<([a-zA-Z]+)>(.*?)</\1>"
re.Global = True
Set Matches = re.Execute(s)
For Each Item In Matches
   Debug.Print Item.Value
   Set Submatches = Item.Submatches
   For Each SubItem In Submatches
      Debug.Print "      " & SubItem
   Next
Next
' <b>Text1</b>
'       b
'       Text1
' <I>Text3</I>
'       I
'       Text3
' <b>Text4</b>
'       b
'       Text4

Внутри круглых скобок могут быть расположены следующие конструкции:

Dim re, Matches, Item, s As String
s = "text1, text2, text3 text4"
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "\w+(?=[,])"
re.Global = True
Set Matches = re.Execute(s)
For Each Item In Matches
   Debug.Print Item.Value
Next
' text1
' text2
Dim re, Matches, Item, s As String
s = "text1, text2, text3 text4"
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "[a-z]+[0-9](?![,])"
re.Global = True
Set Matches = re.Execute(s)
For Each Item In Matches
   Debug.Print Item.Value
Next
' text3
' text4

Рассмотрим небольшой пример. Предположим, необходимо получить все слова, расположенные после тире, причем перед тире и после слов должны следовать пробельные символы:

Dim re, Matches, Item, s As String
s = "-word1 -word2 -word3 -word4 -word5"
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "\s\-([a-z0-9]+)\s"
re.Global = True
Set Matches = re.Execute(s)
For Each Item In Matches
   Debug.Print Item.Value
Next
' -word2
' -word4

Как видно из примера, мы получили только два слова вместо пяти. Первое и последнее слово не попали в результат, т. к. расположены в начале и конце строки. Чтобы эти слова попали в результат, необходимо добавить альтернативный выбор (^|\s) для начала строки и (\s|$) для конца строки. Чтобы найденные выражения внутри круглых скобок не попали в результат, следует добавить символы ?: после открывающей скобки:

re.Pattern = "(?:^|\s)\-([a-z0-9]+)(?:\s|$)"
' -word1
' -word3
' -word5

Первое и последнее слово успешно попали в результат. Почему же слова "word2" и "word4" не попали? Ведь перед тире есть пробел и после слова есть пробел. Чтобы понять причину, рассмотрим поиск по шагам. Первое слово успешно попадает в результат, т. к. перед тире расположено начало строки и после слова есть пробел. После поиска указатель перемещается, и строка для дальнейшего поиска примет следующий вид:

"-word1 <Указатель>-word2 -word3 -word4 -word5"

Обратите внимание на то, что перед фрагментом "-word2" больше нет пробела и тире не расположено вначале строки. Поэтому следующим совпадением будет слово "word3", и указатель снова будет перемещен:

"-word1 -word2 -word3 <Указатель>-word4 -word5"

Опять перед фрагментом "-word4" нет пробела и тире не расположено вначале строки. Поэтому следующим совпадением будет слово "word5" и поиск будет завершен. Таким образом, слова "word2" и "word4" не попадают в результат, т. к. пробел до фрагмента уже был использован в предыдущем поиске. Чтобы этого избежать следует воспользоваться положительным просмотром вперед (?=...):

Dim re, Matches, Submatches, Item, SubItem, s As String
s = "-word1 -word2 -word3 -word4 -word5"
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "(?:^|\s)\-([a-z0-9]+)(?=\s|$)"
re.Global = True
Set Matches = re.Execute(s)
For Each Item In Matches
   Set Submatches = Item.Submatches
   For Each SubItem In Submatches
      Debug.Print SubItem
   Next
Next
' word1
' word2
' word3
' word4
' word5

В этом примере мы заменили фрагмент (?:\s|$) на (?=\s|$). Поэтому все слова успешно попали в результат.

Предыдущая статья Все статьи Следующая статья