Статьи

Scala Tutorial — регулярные выражения, сопоставление

Предисловие

Это пятая часть руководств для начинающих программистов, попадающих в Scala. Другие посты находятся в этом блоге, и вы можете получить ссылки на эти и другие ресурсы на странице ссылок курса по компьютерной лингвистике, для которого я их создаю. Кроме того, вы можете найти эту и другие серии учебников на странице JCG Java Tutorials .

Этот пост является первым из двух о регулярных выражениях (регулярных выражениях), которые необходимы для широкого круга задач программирования, в частности для задач компьютерной лингвистики. В этом руководстве объясняется, как использовать их со Scala, предполагая, что читатель уже знаком с синтаксисом регулярных выражений. В нем показано, как создавать регулярные выражения в Scala и использовать их с мощными возможностями сопоставления с шаблоном Scala, в частности, для назначения переменных и случаев в выражениях соответствия.

Создание регулярных выражений

Scala предоставляет очень простой способ создания регулярных выражений: просто определите регулярное выражение как строку и затем вызовите для нее метод r . Следующее определяет регулярное выражение, которое характеризует язык строки а ^ мб ^ п (за одним или несколькими символами « a » следует один или несколько символов «b» , необязательно совпадающий с числом символов « a »).

1
2
scala> val AmBn = "a+b+".r
AmBn: scala.util.matching.Regex = a+b+

Чтобы использовать метасимволы, такие как \ s , \ w и \ d , вы должны либо экранировать косую черту, либо использовать многоквартирные строки, которые называются необработанными строками. Ниже приведены два эквивалентных способа написания регулярного выражения, которое охватывает строки последовательности символов слова, за которой следует последовательность цифр.

1
2
3
4
5
scala> val WordDigit1 = "\\w+\\d+".r
WordDigit1: scala.util.matching.Regex = \w+\d+
  
scala> val WordDigit2 = """\w+\d+""".r
WordDigit2: scala.util.matching.Regex = \w+\d+

Предпочтительнее ли экранировать или использовать необработанные строки, зависит от контекста. Например, с вышеупомянутым я бы пошел с необработанной строкой. Однако для использования регулярного выражения для разделения строки на пробельные символы экранирование несколько предпочтительнее.

01
02
03
04
05
06
07
08
09
10
scala> val adder = "We're as similar as two dissimilar things in a pod.\n\t-Blackadder"
adder: java.lang.String =
We're as similar as two dissimilar things in a pod.
-Blackadder
  
scala> adder.split("\\s+")
res2: Array[java.lang.String] = Array(We're, as, similar, as, two, dissimilar, things, in, a, pod., -Blackadder)
  
scala> adder.split("""\s+""")
res3: Array[java.lang.String] = Array(We're, as, similar, as, two, dissimilar, things, in, a, pod., -Blackadder)

Примечание по именованию: в Scala принято использовать имена переменных с первой буквой в верхнем регистре для объектов Regex. Это делает их совместимыми с использованием сопоставления с образцом в операторах сопоставления, как показано ниже.

Сопоставление с регулярными выражениями

Выше мы видели, что использование метода r в String возвращает значение, являющееся объектом Regex (подробнее об этом в части scala.util.matching ниже). Как вы на самом деле делаете полезные вещи с этими объектами Regex? Есть несколько способов. Самым красивым и, возможно, самым распространенным для неисчислительного лингвиста является использование их в тандеме со стандартными возможностями Scala для сопоставления с образцом. Давайте рассмотрим задачу анализа имен и превращения их в полезные структуры данных, с помощью которых мы можем делать различные полезные вещи.

01
02
03
04
05
06
07
08
09
10
11
12
scala> val Name = """(Mr|Mrs|Ms)\. ([A-Z][a-z]+) ([A-Z][a-z]+)""".r
Name: scala.util.matching.Regex = (Mr|Mrs|Ms)\. ([A-Z][a-z]+) ([A-Z][a-z]+)
  
scala> val Name(title, first, last) = "Mr. James Stevens"
title: String = Mr
first: String = James
last: String = Stevens
  
scala> val Name(title, first, last) = "Ms. Sally Kenton"
title: String = Ms
first: String = Sally
last: String = Kenton

Обратите внимание на сходство с сопоставлением с образцом в таких типах, как Array и List.

1
2
3
4
5
6
7
8
9
scala> val Array(title, first, last) = "Mr. James Stevens".split(" ")
title: java.lang.String = Mr.
first: java.lang.String = James
last: java.lang.String = Stevens
  
scala> val List(title, first, last) = "Mr. James Stevens".split(" ").toList
title: java.lang.String = Mr.
first: java.lang.String = James
last: java.lang.String = Stevens

Конечно, обратите внимание, что здесь «.» был захвачен, а регулярное выражение вырезало его. Более существенным отличием регулярного выражения является то, что оно принимает только строки правильной формы и отклоняет другие, в отличие от простого разбиения и сопоставления с массивом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
scala> val Array(title, first, last) = "221B Baker Street".split(" ")
title: java.lang.String = 221B
first: java.lang.String = Baker
last: java.lang.String = Street
  
scala> val Name(title, first, last) = "221B Baker Street"
scala.MatchError: 221B Baker Street (of class java.lang.String)
at .<init>(<console>:12)
at .<clinit>(<console>)
at .<init>(<console>:11)
at .<clinit>(<console>)
at $export(<console>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.call(IMain.scala:592)
at scala.tools.nsc.interpreter.IMain$Request$$anonfun$10.apply(IMain.scala:828)
at scala.tools.nsc.interpreter.Line$$anonfun$1.apply$mcV$sp(Line.scala:43)
at scala.tools.nsc.io.package$$anon$2.run(package.scala:31)
at java.lang.Thread.run(Thread.java:680)

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

А пока давайте предположим, что ввод является подходящим. Это означает, что мы можем легко преобразовать список имен в виде строк в список кортежей, используя map и выражение соответствия.

1
2
3
4
5
scala> val names = List("Mr. James Stevens", "Ms. Sally Kenton", "Mrs. Jane Doe", "Mr. John Doe", "Mr. James Smith")
names: List[java.lang.String] = List(Mr. James Stevens, Ms. Sally Kenton, Mrs. Jane Doe, Mr. John Doe, Mr. James Smith)
  
scala> names.map(x => x match { case Name(title, first, last) => (title, first, last) })
res11: List[(String, String, String)] = List((Mr,James,Stevens), (Ms,Sally,Kenton), (Mrs,Jane,Doe), (Mr,John,Doe), (Mr,James,Smith))

Обратите внимание на критическое использование групп в регулярном выражении Name : количество групп равно числу переменных, инициализируемых в совпадении. Первая группа необходима для альтернатив Мистер, Миссис и Мисс . Без других групп мы получаем ошибку. (С этого момента я сокращу вывод MatchError.)

1
2
3
4
5
scala> val NameOneGroup = """(Mr|Mrs|Ms)\. [A-Z][a-z]+ [A-Z][a-z]+""".r
NameOneGroup: scala.util.matching.Regex = (Mr|Mrs|Ms)\. [A-Z][a-z]+ [A-Z][a-z]+
  
scala> val NameOneGroup(title, first, last) = "Mr. James Stevens"
scala.MatchError: Mr. James Stevens (of class java.lang.String)

Конечно, мы все еще можем соответствовать первой группе.

1
2
scala> val NameOneGroup(title) = "Mr. James Stevens"
title: String = Mr

Что если мы пойдем в другом направлении, создав больше групп, чтобы мы могли, например, разделить букву «М» в разных заголовках? Вот попытка.

1
2
3
4
5
scala> val NameShareM = """(M(r|rs|s))\. ([A-Z][a-z]+) ([A-Z][a-z]+)""".r
NameShareM: scala.util.matching.Regex = (M(r|rs|s))\. ([A-Z][a-z]+) ([A-Z][a-z]+)
  
scala> val NameShareM(title, first, last) = "Mr. James Stevens"
scala.MatchError: Mr. James Stevens (of class java.lang.String)

Случилось так, что была создана новая группа, поэтому теперь нужно сопоставить четыре группы.

01
02
03
04
05
06
07
08
09
10
11
scala> val NameShareM(title, titleEnding, first, last) = "Mr. James Stevens"
title: String = Mr
titleEnding: String = r
first: String = James
last: String = Stevens
  
scala> val NameShareM(title, titleEnding, first, last) = "Mrs. Sally Kenton"
title: String = Mrs
titleEnding: String = rs
first: String = Sally
last: String = Kenton

Таким образом, есть подгруппа захвата. Чтобы часть (r | rs | s) не создала группу соответствий, но при этом могла использовать ее для группировки альтернатив в дизъюнкции, используйте ? : оператор.

1
2
3
4
5
6
7
scala> val NameShareMThreeGroups = """(M(?:r|rs|s))\. ([A-Z][a-z]+) ([A-Z][a-z]+)""".r
NameShareMThreeGroups: scala.util.matching.Regex = (M(?:r|rs|s))\. ([A-Z][a-z]+) ([A-Z][a-z]+)
  
scala> val NameShareMThreeGroups(title, first, last) = "Mr. James Stevens"
title: String = Mr
first: String = James
last: String = Stevens

К этому моменту разделение M ничего не спасло (Mr | Mrs | Ms) , но есть много ситуаций, где это весьма полезно.

Мы также можем использовать обратные ссылки на регулярные выражения. Скажем, мы хотим сопоставить имена, такие как « Мистер Джон Бон », « Мистер Джо Доу » и « Миссис Джилл Хилл ».

1
2
3
4
5
6
7
8
scala> val RhymeName = """(Mr|Mrs|Ms)\. ([A-Z])([a-z]+) ([A-Z])\3""".r
RhymeName: scala.util.matching.Regex = (Mr|Mrs|Ms)\. ([A-Z])([a-z]+) ([A-Z])\3
  
scala> val RhymeName(title, firstInitial, firstRest, lastInitial) = "Mr. John Bohn"
title: String = Mr
firstInitial: String = J
firstRest: String = ohn
lastInitial: String = B

Тогда мы могли бы собрать вещи вместе, чтобы получить имена, которые мы хотели.

1
2
3
4
5
scala> val first = firstInitial+firstRest
first: java.lang.String = John
  
scala> val last = lastInitial+firstRest
last: java.lang.String = Bohn

Но мы можем добиться большего, используя встроенную группу и просто отбрасывая результат совпадения с подчеркиванием _ .

1
2
3
4
5
6
7
scala> val RhymeName2 = """(Mr|Mrs|Ms)\. ([A-Z]([a-z]+)) ([A-Z]\3)""".r
RhymeName2: scala.util.matching.Regex = (Mr|Mrs|Ms)\. ([A-Z]([a-z]+)) ([A-Z]\3)
  
scala> val RhymeName2(title, first, _, last) = "Mr. John Bohn"
title: String = Mr
first: String = John
last: String = Bohn

Примечание : мы не можем использовать оператор ?: С ([az] +), чтобы остановить совпадение, потому что нам нужна именно эта строка, чтобы соответствовать \ 3 позже.

Использование регулярных выражений для назначения через сопоставление с образцом требует полного совпадения строк.

1
2
3
4
5
6
7
scala> val Name(title, first, last) = "Mr. James Stevens"
title: String = Mr
first: String = James
last: String = Stevens
  
scala> val Name(title, first, last) = "Mr. James Stevens walked to the door."
scala.MatchError: Mr. James Stevens walked to the door. (of class java.lang.String)

Это важный аспект использования их в выражениях совпадений. Рассмотрим приложение, которое должно уметь анализировать телефонные номера в разных форматах, например ( 123) 555-5555 и 123-555-5555 . Вот регулярные выражения для этих двух шаблонов и их использование для анализа этих чисел.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
scala> val Phone1 = """\((\d{3})\)\s*(\d{3})-(\d{4})""".r
Phone1: scala.util.matching.Regex = \((\d{3})\)\s*(\d{3})-(\d{4})
  
scala> val Phone2 = """(\d{3})-(\d{3})-(\d{4})""".r
Phone2: scala.util.matching.Regex = (\d{3})-(\d{3})-(\d{4})
  
scala> val Phone1(area, first3, last4) = "(123) 555-5555"
area: String = 123
first3: String = 555
last4: String = 5555
  
scala> val Phone2(area, first3, last4) = "123-555-5555"
area: String = 123
first3: String = 555
last4: String = 5555

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

1
2
3
4
def normalizePhoneNumber (number: String) = number match {
  case Phone1(x,y,z) => (x,y,z)
  case Phone2(x,y,z) => (x,y,z)
}

Действие, которое предпринимается для каждого совпадения, состоит в том, чтобы просто упаковать отдельные значения в Tuple3 — более интересные вещи можно было бы сделать, если бы вы искали коды стран, имели дело с несколькими странами и т. Д. Суть в том, чтобы увидеть, как регулярные выражения используются для случаев, чтобы захватывать значения и присваивать их локальным переменным, каждый раз в зависимости от формы вводимой строки. (В следующем уроке мы увидим, как защитить такой метод от входных данных, которые не являются телефонными. цифры и тому подобное.)

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

1
2
3
4
5
6
7
8
scala> val numbers = List("(123) 555-5555", "123-555-5555", "(321) 555-0000")
numbers: List[java.lang.String] = List((123) 555-5555, 123-555-5555, (321) 555-0000)
  
scala> numbers.map(normalizePhoneNumber)
res16: List[(String, String, String)] = List((123,555,5555), (123,555,5555), (321,555,0000))
  
scala> numbers.map(normalizePhoneNumber).filter(n => n._1=="123")
res17: List[(String, String, String)] = List((123,555,5555), (123,555,5555))

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

Иногда кто-то хочет создать регулярное выражение из более мелких составных частей, например, определить, что такое существительное, а затем искать последовательность словосочетаний. Чтобы сделать это, мы сначала должны увидеть более длинную форму создания регулярного выражения.

1
2
scala> val AmBn = new scala.util.matching.Regex("a+b+")
AmBn: scala.util.matching.Regex = a+b+

Это первый раз в этих уроках, когда мы явно создаем объект, используя зарезервированное слово new . Позже мы рассмотрим объекты более подробно, но сейчас вам нужно знать, что Scala обладает широкими функциональными возможностями, которые по умолчанию недоступны. В основном, мы работали с такими вещами, как Strings, Ints, Doubles, Lists и т. Д., И по большей части вам показалось, что они «просто» Strings, Ints, Doubles и Lists. Однако это не так: на самом деле они полностью определены как:

  • java.lang.String
  • scala.Int
  • scala.Double
  • scala.List

И, в случае с последним, scala.List — это тип, который фактически поддерживается конкретной реализацией в scala.collection.immutable.List . Итак, когда вы видите «Список», Scala на самом деле скрывает некоторые детали; самое главное, это позволяет использовать очень распространенные типы с очень небольшим суетой.

Что scala.util.matching.Regex говорит вам, что класс Regex является частью пакета scala.util.matching (и что scala.util.matching является подпакетом scala.util , который сам по себе является подпакетом scala пакет). К счастью, вам не нужно вводить scala.util.matching каждый раз, когда вы хотите использовать Regex: просто используйте оператор импорта , а затем используйте Regex без дополнительной спецификации пакета.

1
2
3
4
5
scala> import scala.util.matching.Regex
import scala.util.matching.Regex
  
scala> val AmBn = new Regex("a+b+")
AmBn: scala.util.matching.Regex = a+b+

Другая вещь, чтобы объяснить, это новая часть. Опять же, мы рассмотрим это более подробно позже, а пока подумайте об этом следующим образом. Класс Regex подобен фабрике для создания объектов regex, и способ, которым вы запрашиваете (упорядочиваете) один из этих объектов, заключается в том, чтобы сказать « new Regex (…) », где указывает строку, которая должна использоваться для определения свойств этот объект. На самом деле вы уже довольно часто этим занимались, создавая списки, целые и двойные числа, но, опять же, для этих основных типов Scala предоставил специальный синтаксис для упрощения их создания и использования.

Хорошо, но зачем использовать новый Regex («a + b +»), когда «a + b +». R можно использовать для того же? И вот почему: последнему нужно дать полную строку, но первое можно построить из нескольких строковых переменных. В качестве примера, скажем, вы хотите, чтобы регулярное выражение соответствовало строкам вида « преследовали / собаку / кошку / мышь / птицу / съели собаку / кошку / мышь / птицу », например « собака преследовала кошку » и « Кот преследовал птицу Следующее может быть первой попыткой.

1
2
scala> val Transitive = "(a|the) (dog|cat|mouse|bird) (chased|ate) (a|the) (dog|cat|mouse|bird)".r
Transitive: scala.util.matching.Regex = (a|the) (dog|cat|mouse|bird) (chased|ate) (a|the) (dog|cat|mouse|bird)

Это работает, но мы также можем построить его, не повторяя одно и то же выражение дважды, используя переменную, содержащую строку, определяющую регулярное выражение (но не являющееся объектом Regex) и создающую регулярное выражение с этим.

1
2
3
4
5
scala> val nounPhrase = "(a|the) (dog|cat|mouse|bird)"
nounPhrase: java.lang.String = (a|the) (dog|cat|mouse|bird)
  
scala> val Transitive = new Regex(nounPhrase + " (chased|ate) " + nounPhrase)
Transitive: scala.util.matching.Regex = (a|the) (dog|cat|mouse|bird) (chased|ate) (a|the) (dog|cat|mouse|bird)

В следующем уроке будет показано, как использовать API пакета scala.util.matching для более тщательного сопоставления с регулярными выражениями, например, для поиска нескольких совпадений и выполнения подстановок.

Ссылка: Первые шаги в Scala для начинающих программистов, часть 5 от нашего партнера JCG Джейсона Болдриджа в блоге Bcomposes .

Статьи по Теме :