Статьи

Scala Tutorial – регулярные выражения, сопоставления и замены с помощью API scala.util.matching

Предисловие

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

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

Использование регулярных выражений для захвата значений для назначения переменных и падежей в выражениях совпадений — это очень чистый, продуманный и очень полезный признак поддержки регулярных выражений в языке Scala. Однако их использование для более сложного сопоставления строк и подстановки, честно говоря, гораздо менее прямолинейно, чем в языках со встроенной поддержкой регулярных выражений, таких как Perl (который, если говорить как тот, кто много кодировал в Perl, — вы * не * хотите использовать для общего программирования). Scala полностью совместим с тем, что вы можете полностью использовать регулярные выражения, но вам нужно будет использовать его через Regex API. Другими словами, вам нужно использовать несколько команд, не все из которых так просты, как могли бы быть. (Это не напыщенная речь, хотя я, конечно, хотел бы, чтобы регулярные выражения поддерживались более естественным образом в Scala.)

Хотя я буду ссылаться на то, что я делаю ниже, как на использование API-интерфейса Regex, я сначала отмечу, что это звучит как нечто большее, чем на самом деле. Это просто означает, что вы напрямую используете классы и объекты из пакета scala.util.matching , а не используете специальный синтаксис и интеграцию с сопоставлением с шаблоном Scala, которое мы видели в предыдущем посте.

Более обширное соответствие

Во-первых, давайте сделаем то, что мы сделали с сопоставлением с шаблоном в предыдущем посте, но теперь используем класс Regex и доступные ему методы для достижения тех же целей. Затем мы можем начать работать с поиском нескольких совпадений и выполнением замен.

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

01
02
03
04
05
06
07
08
09
10
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 smith = "Mr. John Smith"
smith: java.lang.String = Mr. John Smith
  
scala> val Name(title, first, last) = smith
title: String = Mr
first: String = John
last: String = Smith

Вместо того, чтобы делать это таким образом, давайте вместо этого будем использовать методы API. Мы начнем с использования регулярного выражения, чтобы найти совпадения, если таковые имеются. Метод findAllIn из Regex делает это для нас.

1
2
scala> val matchesFound = Name.findAllIn(smith)
matchesFound: scala.util.matching.Regex.MatchIterator = non-empty iterator

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

1
2
scala> matchesFound.foreach(println)
Mr. John Smith

Однако, в отличие от списков, вы можете сделать это только один раз. Как показано ниже, после того, как вы выполните итерацию один раз, его элементы израсходованы.

1
2
3
4
5
6
7
scala> val matchesFound = Name.findAllIn(smith)
matchesFound: scala.util.matching.Regex.MatchIterator = non-empty iterator
  
scala> matchesFound.foreach(println)
Mr. John Smith
  
scala> matchesFound.foreach(println)

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

1
2
3
4
5
6
7
scala> val matchesFound = Name.findAllIn(smith)
matchesFound: scala.util.matching.Regex.MatchIterator = non-empty iterator
  
scala> matchesFound(0)
<console>:11: error: scala.util.matching.Regex.MatchIterator does not take parameters
matchesFound(0)
^

Если вы хотите сделать это, вам нужно просто вызвать toList в MatchIterator .

1
2
3
4
5
6
7
8
scala> val matchList = Name.findAllIn(smith).toList
matchList: List[String] = List(Mr. John Smith)
  
scala> matchList.foreach(println)
Mr. John Smith
  
scala> matchList.foreach(println)
Mr. John Smith

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

Обратите внимание, что у нас есть List [String] . Это означает, что мы можем видеть, какие части строки совпадают, что может включать в себя несколько совпадений.

1
2
3
4
5
scala> val sentence = "Mr. John Smith said hello to Ms. Jane Hill and then to Mr. Bill Brown."
sentence: java.lang.String = Mr. John Smith said hello to Ms. Jane Hill and then to Mr. Bill Brown.
  
scala> val matchList = Name.findAllIn(sentence).toList
matchList: List[String] = List(Mr. John Smith, Ms. Jane Hill, Mr. Bill Brown)

Это будет полезно во многих контекстах, но не позволит нам получить доступ к группам совпадений, которые были определены в регулярном выражении. Для этого нам нужно использовать метод matchData , который преобразует MatchIterator (который предлагает Strings в качестве его элементов) в Iterator [Match] (который предлагает объекты Match в качестве своих элементов).

1
scala> val matchList = Name.findAllIn(smith).matchDatamatchList: java.lang.Object with Iterator[scala.util.matching.Regex.Match] = non-empty iterator

Давайте преобразовать это в список, а затем захватить первый элемент.

1
2
3
4
5
scala> val matchList = Name.findAllIn(smith).matchData.toList
matchList: List[scala.util.matching.Regex.Match] = List(Mr. John Smith)
  
scala> val firstMatch = matchList(0)
firstMatch: scala.util.matching.Regex.Match = Mr. John Smith

Этот объект Match содержит захваченные группы, к которым мы можем получить доступ с помощью метода group. Первый индекс 0 возвращает все совпадения, а остальные получают доступ к захваченным группам.

01
02
03
04
05
06
07
08
09
10
11
scala> firstMatch.group(0)
res8: String = Mr. John Smith
  
scala> val title = firstMatch.group(1)
title: String = Mr
  
scala> val first = firstMatch.group(2)
first: String = John
  
scala> val last = firstMatch.group(3)
last: String = Smith

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

1
2
3
4
scala> val (title, first, last) = (firstMatch.group(1), firstMatch.group(2), firstMatch.group(3))
title: String = Mr
first: String = John
last: String = Smith

Обновление : есть более краткий способ сделать это, используя диапазон от 1 до 3 и сопоставить firstMatch.group с этим диапазоном. Это создает Seq (uence), по которому мы можем сопоставить шаблон. (Спасибо @missingfaktor.)

1
val Seq(title, first, last) = 1 to 3 map firstMatch.group

Это должно продемонстрировать, почему поддержка Scala для регулярных выражений в сопоставлении паттернов очень хороша для этого. Что вы получаете с API — это возможность сопоставлять несколько экземпляров шаблона в строке, а затем выполнять вычисления с результатами сопоставления на лету. Например, давайте вернемся к предложению с несколькими именами в нем и используем регулярное выражение Name, чтобы передать привет каждому найденному в нем имени.

1
2
3
4
scala> Name.findAllIn(sentence).matchData.foreach(m => println("Hello, " + m.group(0)))
Hello, Mr. John Smith
Hello, Ms. Jane Hill
Hello, Mr. Bill Brown

Конечно, вы можете распечатать только части имен, такие как заголовок и фамилия.

1
2
3
4
scala> Name.findAllIn(sentence).matchData.foreach(m => println("Hello, " + m.group(1) + ". " + m.group(3)))
Hello, Mr. Smith
Hello, Ms. Hill
Hello, Mr. Brown

Или вы можете отфильтровать результаты, например, только для Mr, а затем распечатать только имена.

1
2
3
scala> Name.findAllIn(sentence).matchData.filter(m=>m.group(1) == "Mr").foreach(m => println("Hello, " + m.group(2)))
Hello, John
Hello, Bill

Обратите внимание, что в приведенных выше строках я не конвертировал MatchIterator в список, так как был рад просто просмотреть список один раз и выполнить некоторые действия.

Выполнение замен

Другая вещь, которую вы получаете, — это возможность использовать регулярные выражения для замены одного класса выражений другим. Например, предположим, что (по какой-то странной причине) вы хотели бы изменить имя каждого, чтобы « Мистер Джон Смит » стал « Мистер Смит Джон ». Это достигается с помощью метода Regex replaceAllIn , который принимает два аргумента: первый — исходная строка, а второй — функция, которая принимает объект Match и возвращает строку.

1
2
scala> val swapped = Name.replaceAllIn(sentence, m => m.group(1) + ". " + m.group(3) + " " + m.group(2))
swapped: String = Mr. Smith John said hello to Ms. Hill Jane and then to Mr. Brown Bill.

Переменная m выше ссылается на каждый из идентифицированных объектов Match, по очереди. Это означает, что мы можем получить доступ к группам, как мы делали раньше. Сначала может показаться странным, что анонимная функция m => m.group (1) + «. ”+ M.group (3) +” ”+ m.group (2) является аргументом. Это не очень отличается от следующего, где мы сначала создаем именованную функцию, а затем передаем ее в качестве аргумента.

1
2
3
4
scala> def swapFirstLast = (m: scala.util.matching.Regex.Match) => m.group(1) + ". " + m.group(3) + " " + m.group(2)
swapFirstLast: (util.matching.Regex.Match) => java.lang.String
  
scala> val swapped = Name.replaceAllIn(sentence, swapFirstLast)swapped: String = Mr. Smith John said hello to Ms. Hill Jane and then to Mr. Brown Bill.

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

1
2
scala> val swappedNames = Name.findAllIn(sentence).matchData.map(swapFirstLast).toList
swappedNames: List[java.lang.String] = List(Mr. Smith John, Ms. Hill Jane, Mr. Brown Bill)

Разница в том, что использование findAllIn дает нам сами результаты Match, тогда как replaceAllIn заменяет их в String in situ. Нужно ли вам делать то или другое, зависит от ваших потребностей программирования.

Определение соответствия всей строки с помощью Regex API

Если вы просто хотите узнать, соответствует ли вся данная строка регулярному выражению, Scala, к сожалению, имеет несколько обходных путей для вас. Во-первых, вот синтаксис, проверяющий, соответствует ли Name переменным smith и предложению .

1
2
3
4
5
scala> Name.pattern.matcher(smith).matches
res21: Boolean = true
  
scala> Name.pattern.matcher(sentence).matches
res22: Boolean = false

Таким образом, предложение не совпадает (несмотря на наличие трех имен в нем), поскольку вся строка не совпадает с именем.

Здесь происходит то, что мы на самом деле используем классы, определенные в Java, для работы с регулярными выражениями. Сначала мы получаем объект java.util.regex.Pattern, связанный с нашим объектом scala.util.matching.Regex .

1
2
scala> Name.pattern
res16: java.util.regex.Pattern = (Mr|Mrs|Ms)\. ([A-Z][a-z]+) ([A-Z][a-z]+)

Затем мы используем этот шаблон, чтобы получить java.util.regex.Matcher для строки.

1
2
scala> Name.pattern.matcher(smith)
res17: java.util.regex.Matcher = java.util.regex.Matcher[pattern=(Mr|Mrs|Ms)\. ([A-Z][a-z]+) ([A-Z][a-z]+) region=0,14 lastmatch=]

Класс Matcher имеет метод match, который сообщает нам, было ли совпадение для этой строки.

1
2
scala> Name.pattern.matcher(smith).matches
res18: Boolean = true

Итак, многословно, но вы можете это сделать.

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

1
2
3
4
5
scala> smith match { case Name(_,_,_) => true; case _ => false }
res23: Boolean = true
  
scala> sentence match { case Name(_,_,_) => true; case _ => false }
res24: Boolean = false

Однако это требует дополнительной работы по определению групп захвата, которые все равно выбрасываются.

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

Есть другой метод replaceAllIn, который принимает строку, определяющую (довольно) стандартную подстановку регулярного выражения в качестве второго аргумента, а не функцию из соответствий в строки. Этот аргумент определяет регулярное выражение, аналогичное тому, которое используется в стандартных подстановках s /// из языка программирования Perl, например, в следующем, который превращает строки вроде « xyzaaaabbb123 » в « xyzbbbaaaa123 ».

1
s/(a+)(b+)/\2\1/

В отличие от Perl (который аналогичен синтаксису, обсуждаемому в книге Юрафски и Мартина), Scala использует $ 1 , $ 2 и т. Д. В качестве примера рассмотрим обмен фамилиями на фамилии, который мы делали ранее. Здесь это повторяется:

1
2
scala> val swapped = Name.replaceAllIn(sentence, m => m.group(1) + ". " + m.group(3) + " " + m.group(2))
swapped: String = Mr. Smith John said hello to Ms. Hill Jane and then to Mr. Brown Bill.

Вы можете получить тот же самый эффект несколько легче, создав строку замены с переменными $ n, которые ссылаются на группы.

1
2
scala> val swapped2 = Name.replaceAllIn(sentence, "$1. $3 $2")
swapped2: String = Mr. Smith John said hello to Ms. Hill Jane and then to Mr. Brown Bill.

Это гораздо более кратко и читабельно, чем стиль m.group (), описанный выше, поэтому он предпочтителен для подобных случаев. Однако иногда вам захочется сделать более интересную обработку значений в каждой группе, например, изменить названия на другой язык и вывести только первый инициал имени: например, « Мистер Джон Смит » станет « Sr». Дж. Смит »и« Миссис Джейн Хилл »станут« Сра. Дж. Хилл ». Мне не ясно, как можно это сделать с помощью подстановок $ n (если кто-то из читателей знает, пожалуйста, дайте мне знать). Чтобы сделать это с помощью функции Match => String , это просто. Сначала давайте определим метод, который отображает названия с английского на испанский.

1
2
3
4
5
def engTitle2Esp (title: String) = title match {
  case "Mr" => "Sr"
  case "Mrs" => "Sra"
  case "Ms" => "Srta"
}

Затем мы передаем m.group (1) через эту функцию, используя engTitle2Esp (m.group (1)) , и получаем только первый символ группы 2, индексируя в нем m.group (2) (0) .

1
2
scala> val spanishized = Name.replaceAllIn(sentence, m => engTitle2Esp(m.group(1)) + ". " + m.group(2)(0) + ". " + m.group(3))
spanishized: String = Sr. J. Smith said hello to Srta. J. Hill and then to Sr. B. Brown.

Это дает вам значительный контроль над тем, как обрабатывать замены.

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

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