Regex разбивается на перекрывающиеся строки

Я изучаю возможности регулярных выражений, поэтому мне просто интересно, возможно ли что-то подобное:

public class StringSplit {
    public static void main(String args[]) {
        System.out.println(
            java.util.Arrays.deepToString(
                "12345".split(INSERT_REGEX_HERE)
            )
        ); // prints "[12, 23, 34, 45]"
    }
}

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

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

Если это невозможно, объясните, пожалуйста, почему.


БОНУСНЫЙ ВОПРОС

Тот же вопрос, но с find() цикл вместо split:

    Matcher m = Pattern.compile(BONUS_REGEX).matcher("12345");
    while (m.find()) {
        System.out.println(m.group());
    } // prints "12", "23", "34", "45"

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

И если их не существует, я хотел бы получить хорошее и четкое объяснение, почему.

6 ответов

Решение

Я не думаю, что это возможно с split(), но с find() это довольно просто Просто используйте заголовок с группой захвата внутри:

Matcher m = Pattern.compile("(?=(\\d\\d)).").matcher("12345");
while (m.find())
{
  System.out.println(m.group(1));
}

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

На самом деле, это работает, даже если регулярное выражение в целом ничего не соответствует. Удалить точку из регулярного выражения выше ("(?=(\\d\\d))") и вы получите тот же результат. Это связано с тем, что всякий раз, когда при успешном сопоставлении не используется никаких символов, механизм регулярных выражений автоматически поднимается на одну позицию перед попыткой повторного сопоставления, чтобы предотвратить бесконечные циклы.

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

Это несколько тяжелая реализация с использованием Matcher.find вместо split также будет работать, хотя к тому времени вы должны кодировать for В цикле для такой тривиальной задачи вы можете также вообще отбросить регулярные выражения и использовать подстроки (для аналогичной сложности кодирования за вычетом циклов ЦП):

import java.util.*;
import java.util.regex.*;

public class StringSplit { 
    public static void main(String args[]) { 
        ArrayList<String> result = new ArrayList<String>();
        for (Matcher m = Pattern.compile("..").matcher("12345"); m.find(result.isEmpty() ? 0 : m.start() + 1); result.add(m.group()));
        System.out.println( result.toString() ); // prints "[12, 23, 34, 45]" 
    } 
} 

EDIT1

match(): причина, по которой никто до сих пор не смог придумать регулярное выражение, подобное вашему BONUS_REGEX лежит в пределах Matcher, который возобновит поиск следующей группы, в которой предыдущая группа закончилась (т. е. не перекрывается), как было предложено после того, как началась предыдущая группа, то есть, если явно не указать заново начальную позицию поиска (см. выше). Хороший кандидат на BONUS_REGEX был бы "(.\\G.|^..)" но, к сожалению, \G трюк-якорь в середине не работает с Java Match (но прекрасно работает в Perl):

 perl -e 'while ("12345"=~/(^..|.\G.)/g) { print "$1\n" }'
 12
 23
 34
 45

split(): что касается INSERT_REGEX_HERE хороший кандидат был бы (?<=..)(?=..) (точка разделения - это позиция нулевой ширины, где у меня есть два символа справа и два слева), но опять же, потому что split кончает ничем из того, что вы перекрываете [12, 3, 45] (что близко, но без сигары.)

EDIT2

Ради интереса можно обмануть split() чтобы делать то, что вы хотите, сначала удваивая неограниченные символы (здесь вам нужно зарезервированное значение символа для разделения):

Pattern.compile("((?<=.).(?=.))").matcher("12345").replaceAll("$1#$1").split("#")

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

Pattern.compile("((?<=.).(?=.))").matcher("12345").replaceAll("$1$1").split("(?<=..)(?=(..)*$)")

Альтернативно обман match() аналогичным образом (но без необходимости в зарезервированном значении символа):

Matcher m = Pattern.compile("..").matcher(
  Pattern.compile("((?<=.).(?=.))").matcher("12345").replaceAll("$1$1")
);
while (m.find()) { 
    System.out.println(m.group()); 
} // prints "12", "23", "34", "45" 

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

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

В Perl это работает:

my $string = '12345';
my @array = ();
while ( $string =~ s/(\d(\d))/$2/ ) {
    push(@array, $1);
}
print join(" ", @array);
# prints: 12 23 34 45

Выражение поиска и замены гласит: сопоставьте первые две соседние цифры и замените их в строке только второй из двух цифр.

Альтернатива, используя простое сопоставление с Perl. Должен работать в любом месте, где работают заглядывающие. И нет необходимости в петлях здесь.

 $_ = '12345';
 @list = /(?=(..))./g;
 print "@list";

 # Output:
 # 12 23 34 45

Но этот, как было опубликовано ранее, лучше, если трюк \G работает:

 $_ = '12345';
 @list = /^..|.\G./g;
 print "@list";

 # Output:
 # 12 23 34 45

Изменить: Извините, не видел, что все это было опубликовано уже.

Создание перекрывающихся совпадений сString#splitневозможно, как уже говорилось в других ответах. Однако можно добавить regex-replace перед ним, чтобы подготовить строку, а затем использовать разделение для создания обычных пар:

      "12345".replaceAll(".(?=(.).)","$0$1")
       .split("(?<=\\G..)")

The .replaceAll(".(?=(.).)","$0$1")превратит "12345"в "12233445". Он в основном заменяет каждый 123подстрока в 1223, то каждый 234к 2334(обратите внимание, что они перекрываются) и т. д. Другими словами, он будет дублировать каждый символ, кроме первого и последнего.

      .(?=(.).)  # Replace-regex:
.          #  A single character
 (?=    )  #  followed by (using a positive lookahead):
     . .   #   two more characters
    ( )    #   of which the first is saved in capture group 1

$0$1       # Replacement-regex:
$0         #  The entire match, which is the character itself since everything
           #  else was inside a lookahead
  $1       #  followed by capture group 1

После этого разделит эту новую строку на пары:

      (?<=\G..) # Split-regex:
(?<=    ) #  A positive lookbehind:
    \G    #   Matching the end of the previous match
          #   (or the start of the string initially)
      ..  #   followed by two characters

Еще немного информации о .split("(?<=\\G..)")можно найти здесь .

Попробуйте онлайн.

Другие вопросы по тегам