Как эта замена регулярного выражения перевернуть строку? - PullRequest
16 голосов
/ 12 сентября 2010

Это четвертая часть в серии образовательных регулярных выражений.Он показывает, как комбинация вложенных ссылок (см .: Как это регулярное выражение находит треугольные числа? ) для "подсчета" в утверждениях (см .: Как мы можем сопоставить ^ nb ^ n с регулярным выражением Java? ) может использоваться для обращения строки.Программно сгенерированный шаблон использует абстракции мета-шаблона (см .: Как этот регулярный код Java обнаруживает палиндромы? ).Впервые в серии эти методы используются для замены вместо сопоставления всей строки.

Предоставляются полные рабочие реализации на Java и C #.Вдохновляющие цитаты включены.

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

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

C # ( также на ideone.com )

using System;
using System.Text.RegularExpressions;

public class TwoDollarReversal {    
public static void Main() {
   string REVERSE = 
      @"(?sx) . grab$2"
         .Replace("grab$2",
            ForEachDotBehind(
               AssertSuffix(@"((.) \1?)")
            )
         );
   Console.WriteLine(
      Regex.Replace(
         @"nietsniE treblA --
         hguone llew ti dnatsrednu t'nod uoy ,ylpmis ti nialpxe t'nac uoy fI",

         REVERSE, "$2"
      )
   );
   // If you can't explain it simply, you don't understand it well enough
   // -- Albert Einstein
}      
// performs an assertion for each dot behind current position
static string ForEachDotBehind(string assertion) {
   return "(?<=(?:.assertion)*)".Replace("assertion", assertion);
}
// asserts that the suffix of the string matches a given pattern
static string AssertSuffix(string pattern) {
   return "(?=.*$(?<=pattern))".Replace("pattern", pattern);
}

}

Java ( также на ideone.com )

class TwoDollarReversal {

public static void main(String[] args) {
   String REVERSE =
      "(?sx) . grab$2"
         .replace("grab$2",
            forEachDotBehind(
               assertSuffix("((.) \\1?)")
            )
         );

   System.out.println(
      "taerG eht rednaxelA --\nyrt lliw ohw mih ot elbissopmi gnihton si erehT"
         .replaceAll(REVERSE, "$2")
   );
   // There is nothing impossible to him who will try
   // -- Alexander the Great"
}

static String forEachDotBehind(String assertion) {
   return "(?<=^(?:.assertion)*?)".replace("assertion", assertion);
}
static String assertSuffix(String pattern) {
   return "(?<=(?=^.*?pattern$).*)".replace("pattern", pattern);
}

}

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

Очевидно, что это не лучший, самый простой и эффективный способ перевернуть строку .Тем не менее, в интересах изучения регулярных выражений;как осмыслить шаблоны;как двигатель работает, чтобы соответствовать им;как соединить разные части, чтобы построить то, что мы хотим;как сделать это так, чтобы его можно было читать и обслуживать;и просто ради радости узнать что-то новое, можем ли мы объяснить, как это работает?


Приложение: шпаргалка!

Это краткое описание основного регулярного выраженияиспользуемые конструкции:

  • (?sx) - встроенный флаг модификаторы .s включает «однострочный» режим, позволяя dot совпадать с ANY символом ( включая переводы строки).x включает режим свободный интервал , где неэкранированные пробелы игнорируются (и # можно использовать для комментариев).
  • ^ и $ - начало иконец строки якоря .
  • ? в качестве спецификатора повторения обозначает необязательно (т. е. ноль или один из).В качестве квантификатора повторения, например, .*?, это означает, что * (т. Е. Ноль или более) повторения неохотно / не жадный.
  • (…) используются для группировки .(?:…) - группа без захвата.Группа захвата сохраняет строку, которой она соответствует;он допускает возврат / пересылку / вложенные ссылки (например, \1), замену замены (например, $2) и т. д.
  • (?=…) является положительным прогнозирование ;он смотрит вправо, чтобы утверждать, что есть совпадение данного образца.(?<=…) является положительным взглядом за спиной ;он смотрит налево.

Ссылки на языки / дополнительные ресурсы

1 Ответ

7 голосов
/ 12 сентября 2010

Обзор

На высоком уровне, шаблон соответствует любому одному символу ., но дополнительно выполняет действие grab$2, которое фиксирует «мат» обращения персонажа, который был сопоставлен с группой 2. Этот захват выполняется построением суффикс входной строки, длина которой соответствует длине префикса до текущей позиции. Мы делаем это, применяя assertSuffix к шаблону, который увеличивает суффикс на один символ, повторяя это один раз forEachDotBehind. Группа 1 фиксирует этот суффикс. Первым символом этого суффикса, захваченным в группе 2, является обратный «помощник» для сопоставляемого символа.

Таким образом, замена каждого совпавшего символа его "сопряжением" имеет эффект обращения строки.


Как это работает: более простой пример

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

System.out.println(
    "123456789"
        .replaceAll(REVERSE, "[$0; $1; $2]\n")
);

Приведенные выше отпечатки ( как видно на ideone.com ):

[1; 9; 9]
[2; 89; 8]
[3; 789; 7]
[4; 6789; 6]
[5; 56789; 5]
[6; 456789; 4]
[7; 3456789; 3]
[8; 23456789; 2]
[9; 123456789; 1]

Таким образом, например, [3; 789; 7] означает, что точка соответствует 3 (захвачено в группе 0), соответствующий суффикс - 789 (группа 1), первый символ которого - 7 (группа 2). Обратите внимание, что 7 является 3 "помощником".

                   current position after
                      the dot matched 3
                              ↓        ________
                      1  2 [3] 4  5  6 (7) 8  9
                      \______/         \______/
                       3 dots        corresponding
                       behind      suffix of length 3

Обратите внимание, что "помощник" персонажа может быть справа или слева. Персонаж может даже быть его собственным "помощником".


Как создается суффикс: вложенная ссылка

Шаблон, отвечающий за сопоставление и построение растущего суффикса, выглядит следующим образом:

    ((.) \1?)
    |\_/    |
    | 2     |       "suffix := (.) + suffix
    |_______|                    or just (.) if there's no suffix"
        1

Обратите внимание, что в определении группы 1 есть ссылка на себя (с \1), хотя это необязательно (с ?). Необязательная часть предоставляет «базовый случай», способ сопоставления группы без ссылки на себя. Это необходимо, потому что попытка сопоставить ссылку на группу всегда терпит неудачу, когда группа еще ничего не захватила.

Как только группа 1 что-то захватывает, необязательная часть никогда не выполняется в нашей настройке, поскольку суффикс, который мы только что записали в прошлый раз, все еще будет там на этот раз, и мы всегда можем добавить другой символ к началу этого суффикса, используя 1043 *. Этот предваряющий персонаж попадает в группу 2.

Таким образом, этот шаблон пытается увеличить суффикс на одну точку. Повторяя это один раз forEachDotBehind, вы получите суффикс, длина которого равна длине префикса до нашей текущей позиции.


Как работают assertSuffix и forEachDotBehind: абстракции мета-паттернов

Обратите внимание, что до сих пор мы рассматривали assertSuffix и forEachDotBehind как черные ящики. Фактически, оставить это обсуждение насовсем - это преднамеренный акт: имена и краткая документация предполагают ЧТО они делают, и этой информации было достаточно, чтобы мы могли написать и прочитать наш шаблон REVERSE!

При ближайшем рассмотрении мы видим, что реализации этих абстракций в Java и C # немного различаются. Это связано с различиями между двумя двигателями регулярных выражений.

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

  • AssertSuffix(pattern) := (?=.*$(?<=pattern)), т. Е. Мы используем упреждающий просмотр, чтобы пройти до конца строки, а затем используем вложенный вид сзади, чтобы сопоставить шаблон с суффиксом.
  • ForEachDotBehind(assertion) := (?<=(?:.assertion)*), т. Е. Мы просто сопоставляем .* во взгляде, помечая утверждение вместе с точкой внутри группы без захвата.

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

  • assertSuffix(pattern) := (?<=(?=^.*?pattern$).*), т. Е. Мы используем взгляд назад, чтобы пройти весь путь до начала строки, а затем используем вложенный взгляд, чтобы сопоставить всю строку , добавляя суффикс шаблон с .*?, чтобы неохотно соответствовать некоторому неуместному префиксу.
  • forEachDotBehind(assertion) := (?<=^(?:.assertion)*?), т. Е. Мы используем закрепленный вид сзади с неохотным повторением, т. Е. ^.*? (и аналогично помечаем утверждение вместе с точкой внутри группы без захвата).

Следует отметить, что хотя реализация C # этих мета-шаблонов не работает в Java, реализация Java работает в C # ( см. На ideone.com ) , Таким образом, в действительности нет необходимости иметь разные реализации для C # и Java, но реализация C # сознательно использовала преимущества более мощной поддержки .NET regex engine для поддержки более естественного выражения шаблонов.

Таким образом, мы показали преимущества использования абстракций мета-паттернов:

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

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

Смотри также


Заключительные мысли

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...