Разделить строку различной длины с помощью регулярных выражений
Я не знаю, возможно ли это с помощью регулярных выражений. Я просто спрашиваю, если кто-то знает ответ.
у меня есть string ="hellohowareyou??"
, Мне нужно разделить это так
[h, el, loh, owar, eyou?, ?]
,
Разделение выполняется так, что первая строка будет иметь длину 1, вторую длину 2 и т. Д. Последняя строка будет содержать оставшиеся символы. Я могу сделать это легко без регулярных выражений, используя такую функцию.
public ArrayList<String> splitString(String s)
{
int cnt=0,i;
ArrayList<String> sList=new ArrayList<String>();
for(i=0;i+cnt<s.length();i=i+cnt)
{
cnt++;
sList.add(s.substring(i,i+cnt));
}
sList.add(s.substring(i,s.length()));
return sList;
}
Мне было просто любопытно, можно ли сделать это с помощью регулярных выражений.
3 ответа
Решение
Следующий фрагмент кода генерирует шаблон, который выполняет эту работу ( см. Его запуск на ideone.com):
// splits at indices that are triangular numbers
class TriangularSplitter {
// asserts that the prefix of the string matches pattern
static String assertPrefix(String pattern) {
return "(?<=(?=^pattern).*)".replace("pattern", pattern);
}
// asserts that the entirety of the string matches pattern
static String assertEntirety(String pattern) {
return "(?<=(?=^pattern$).*)".replace("pattern", pattern);
}
// repeats an assertion as many times as there are dots behind current position
static String forEachDotBehind(String assertion) {
return "(?<=^(?:.assertion)*?)".replace("assertion", assertion);
}
public static void main(String[] args) {
final String TRIANGULAR_SPLITTER =
"(?x) (?<=^.) | measure (?=(.*)) check"
.replace("measure", assertPrefix("(?: notGyet . +NBefore +1After)*"))
.replace("notGyet", assertPrefix("(?! \\1 \\G)"))
.replace("+NBefore", forEachDotBehind(assertPrefix("(\\1? .)")))
.replace("+1After", assertPrefix(".* \\G (\\2?+ .)"))
.replace("check", assertEntirety("\\1 \\G \\2 . \\3"))
;
String text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
System.out.println(
java.util.Arrays.toString(text.split(TRIANGULAR_SPLITTER))
);
// [a, bc, def, ghij, klmno, pqrstu, vwxyzAB, CDEFGHIJ, KLMNOPQRS, TUVWXYZ]
}
}
Обратите внимание, что в этом решении используются методы, уже описанные в моей серии статей о регулярных выражениях. Единственная новая вещь здесь \G
и жду ссылки.
Рекомендации
Это краткое описание основных конструкций регулярных выражений:
(?x)
является модификатором встроенного флага для включения режима свободного пробела, где неэкранированные пробелы игнорируются (и#
можно использовать для комментариев).^
а также$
являются якорями начала и конца строки.\G
является привязкой конца предыдущего совпадения.|
обозначает чередование (то есть "или").?
как спецификатор повторения обозначает необязательный (то есть ноль или один из). В качестве квантификатора повторения, например, в.*?
это означает, что*
(то есть ноль или более) повторение неохотно/ не жадный.(…)
используются для группировки.(?:…)
это группа без захвата. Группа захвата сохраняет строку, которой она соответствует; он позволяет, помимо прочего, сопоставлять ссылки назад / вперед / вложенные (например,\1
).(?=…)
позитивный взгляд; он смотрит вправо, чтобы утверждать, что есть совпадение данного образца.(?<=…)
это позитивный взгляд сзади; это смотрит налево.(?!…)
является негативным взглядом; он смотрит вправо, чтобы утверждать, что нет совпадения шаблона.
Смежные вопросы
- Статьи в
[nested-reference]
серии: - Как работает регулярное выражение
(?<=#)[^#]+(?=#)
Работа?
объяснение
Шаблон совпадает с утверждениями нулевой ширины. Довольно сложный алгоритм используется, чтобы утверждать, что текущая позиция является треугольным числом. Есть 2 основных варианта:
(?<=^.)
то есть мы можем посмотреть назад и увидеть начало строки на расстоянии одной точки- Это соответствует индексу 1 и является важной отправной точкой для остальной части процесса
- В противном случае мы
measure
восстановить, как был сделан последний матч (используя\G
в качестве контрольной точки), сохраняя результат измерения в "до"\G
и после"\G
захват групп. Мы тогдаcheck
если текущая позиция - та, которая предписана измерением, чтобы найти, где должно быть сделано следующее совпадение.
Таким образом, первая альтернатива является тривиальным "базовым случаем", а вторая альтернатива устанавливает, как сделать все последующие совпадения после этого. У Java нет пользовательских групп, но вот семантика для 3 групп захвата:
\1
захватывает строку "до"\G
\2
захватывает некоторую строку "после"\G
- Если длина
\1
например, 1 + 2 + 3 +... + k, то длина\2
должен быть к.- следовательно
\2 .
имеет длину к +1 и должен быть следующей частью в нашемsplit
!
- следовательно
\3
захватывает строку справа от нашей текущей позиции- Следовательно, когда мы можем
assertEntirety
на\1 \G \2 . \3
мы подходим и устанавливаем новый\G
- Следовательно, когда мы можем
Вы можете использовать математическую индукцию, чтобы строго доказать правильность этого алгоритма.
Чтобы проиллюстрировать, как это работает, давайте рассмотрим пример. Давайте принимать abcdefghijklm
в качестве входных данных, и сказать, что мы уже частично отделены [a, bc, def]
,
\G we now need to match here!
↓ ↓
a b c d e f g h i j k l m n
\____1____/ \_2_/ . \__3__/ <--- \1 G \2 . \3
L=1+2+3 L=3
Помни что \G
отмечает конец последнего совпадения, и это происходит по индексам треугольных чисел. Если \G
произошло в 1 + 2 + 3 +... + k, тогда следующий матч должен быть k+1 позиции после \G
быть треугольным индексом числа.
Таким образом, в нашем примере, где \G
где мы только что откололись def
, мы измерили, что k = 3, и следующий матч будет отделен ghij
как и ожидалось.
Иметь \1
а также \2
быть построены в соответствии с вышеуказанной спецификацией, мы в основном делаем while
"петля": до тех пор, пока это notGyet
мы считаем до k следующим образом:
+NBefore
т.е. расширяем\1
однимforEachDotBehind
+1After
т.е. расширяем\2
только одним
Обратите внимание, что notGyet
содержит прямую ссылку на группу 1, которая будет определена позже в шаблоне. По сути мы делаем цикл до \1
"хиты" \G
,
Заключение
Излишне говорить, что это конкретное решение имеет ужасную производительность. Движок регулярных выражений помнит только ГДЕ было сделано последнее совпадение (с \G
) и забывает, КАК (то есть все группы захвата сбрасываются при следующей попытке сопоставления). Наш шаблон должен затем восстановить HOW (ненужный шаг в традиционных решениях, где переменные не так "забывчивы"), кропотливо создавая строки, добавляя по одному символу за раз (что является O(N^2)
). Каждое простое измерение является линейным, а не постоянным временем (так как оно выполняется в виде совпадения строк, где длина является фактором), и, в дополнение к этому, мы делаем много измерений, которые являются избыточными (то есть, чтобы увеличить на единицу, нам нужно сначала повторно сопоставить что у нас уже есть).
Вероятно, есть много "лучших" решений регулярных выражений, чем это. Тем не менее, сложность и неэффективность этого конкретного решения должны справедливо предполагать, что регулярное выражение не предназначено для такого типа сопоставления с образцом.
Тем не менее, в учебных целях, это абсолютно замечательная проблема, потому что есть множество знаний в области исследования и формулирования ее решений. Надеемся, что это конкретное решение и его объяснение было поучительным.
Цель Regex - распознавать шаблоны. Здесь вы ищете не шаблоны, а разделение по длине. Так что регулярные выражения не подходят.
Это возможно, но не с одним регулярным выражением: чтобы найти первые n символов с помощью регулярного выражения, вы используете: "^(.{N}).*"
Таким образом, вы можете искать с помощью этого регулярного выражения для 1-го символа. Затем вы создаете подстроку и ищете следующие 2 символа. И т.п.
Как сказал @splash, это сделает код более сложным и неэффективным, так как вы используете регулярные выражения для чего-то вне их предназначения.
String a = "hellohowareyou??";
int i = 1;
while(true) {
if(i >= a.length()) {
System.out.println(a);
break;
}
else {
String b = a.substring(i++);
String[] out = a.split(Pattern.quote(b) + "$");
System.out.println(out[0]);
a = b;
if(b.isEmpty())
break;
}
}