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

174

Я пытаюсь сопоставить многострочный текст с помощью Java. Когда я использую Patternкласс с Pattern.MULTILINEмодификатором, я могу соответствовать, но я не могу сделать это с(?m).

Тот же шаблон с (?m)использованием и использованием String.matches, похоже, не работает.

Я уверен, что что-то упустил, но понятия не имею, что. Я не очень хорош в регулярных выражениях.

Это то что я пробовал

String test = "User Comments: This is \t a\ta \n test \n\n message \n";

String pattern1 = "User Comments: (\\W)*(\\S)*";
Pattern p = Pattern.compile(pattern1, Pattern.MULTILINE);
System.out.println(p.matcher(test).find());  //true

String pattern2 = "(?m)User Comments: (\\W)*(\\S)*";
System.out.println(test.matches(pattern2));  //false - why?
Нивы
источник

Ответы:

299

Во-первых, вы используете модификаторы в неверном предположении.

Pattern.MULTILINEили (?m)говорит Java принять якоря ^и $совпадать в начале и конце каждой строки (в противном случае они совпадают только в начале / конце всей строки).

Pattern.DOTALLили (?s)говорит Java, что точка должна соответствовать символам новой строки.

Во-вторых, в вашем случае регулярное выражение дает сбой, потому что вы используете matches()метод, который ожидает, что регулярное выражение будет соответствовать всей строке - что, конечно, не работает, поскольку после (\\W)*(\\S)*сопоставления осталось несколько символов .

Так что, если вы просто ищете строку, которая начинается с User Comments:, используйте регулярное выражение

^\s*User Comments:\s*(.*)

с Pattern.DOTALLвозможностью:

Pattern regex = Pattern.compile("^\\s*User Comments:\\s+(.*)", Pattern.DOTALL);
Matcher regexMatcher = regex.matcher(subjectString);
if (regexMatcher.find()) {
    ResultString = regexMatcher.group(1);
} 

ResultString будет содержать текст после User Comments:

Тим Питцкер
источник
Я пытаюсь найти шаблон, который соответствует любой строке, которая начинается с «Комментарии пользователей:». После этого «Комментарии пользователей:» - это то, что пользователь вводит в текстовую область, и поэтому может содержать что угодно - даже новые строки. Похоже, мне нужно многому научиться в регулярных выражениях ...
Nivas
2
Это работает (спасибо!) Я попробовал шаблон (?s)User Comments:\s*(.*). Из ответа @Amarghosh я получил образец User Comments: [\\s\\S]*. Среди них есть лучший или рекомендуемый способ или это просто два разных способа сделать то же самое?
Нивас
3
Они оба означают одно и то же; [\s\S]является немного более явным («соответствует любому символу, который является пробелом или не пробелом»), .его легче читать, но вам нужно искать модификатор (?s)или DOTALL, чтобы узнать, включены ли новые строки или нет. Я бы предпочел .с установленным Pattern.DOTALLфлагом (это легче читать и запоминать, чем (?s)на мой взгляд. Вы должны использовать то, что вам удобнее всего.
Тим Питцкер,
.*с DOTALLболее читабельным. Я использовал другой, чтобы показать, что проблема заключается в различиях между str.matches и matcher.find, а не в флагах. +1
Амаргош
Я предпочитаю .*с Pattern.DOTALL, но придется идти с (? S), потому что я должен использовать String.matches.
Нивас
42

Это не имеет ничего общего с флагом MULTILINE; что вы видите , разница между find()и matches()методами. find()успешен, если совпадение может быть найдено где-либо в целевой строке , в то время matches()как регулярное выражение соответствует всей строке .

Pattern p = Pattern.compile("xyz");

Matcher m = p.matcher("123xyzabc");
System.out.println(m.find());    // true
System.out.println(m.matches()); // false

Matcher m = p.matcher("xyz");
System.out.println(m.matches()); // true

Кроме того, MULTILINEэто не значит, что вы думаете. Многие люди приходят к выводу, что вы должны использовать этот флаг, если ваша целевая строка содержит новые строки, то есть если она содержит несколько логических строк. Я видел несколько ответов на SO на этот счет, но на самом деле все, что делает флаг, это изменяет поведение якорей, ^и $.

Обычно ^соответствует самому началу целевой строки и $самому концу (или перед новой строкой в конце, но мы пока оставим это в стороне). Но если строка содержит символы новой строки, вы можете выбрать ^и $сопоставлять начало и конец любой логической строки, а не только начало и конец всей строки, установив флаг MULTILINE.

Так что забудьте о том, что MULTILINE значит, и просто вспомните, что он делает : меняет поведение ^и $якоря. DOTALLРежим первоначально назывался «однострочным» (и до сих пор присутствует в некоторых вариантах, включая Perl и .NET), и он всегда вызывал подобную путаницу. Нам повезло, что в этом случае разработчики Java получили более описательное имя, но разумной альтернативы для «многострочного» режима не было.

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

Алан Мур
источник
5
Трудно поверить, что они использовали имя метода "#matches" для обозначения "соответствует всем" yikes
rogerdpack
@ alan-moore Извините, я опроверг это, хотя это правильно [нужно больше спать :)]
Рэймонд Насиф
22

str.matches(regex) ведет себя так, как будто Pattern.matches(regex, str) пытается сопоставить всю входную последовательность с шаблоном и возвращает

trueтогда и только тогда, когда вся входная последовательность соответствует шаблону этого сопоставителя

Принимая во внимание, что matcher.find() пытается найти следующую подпоследовательность входной последовательности, которая соответствует шаблону, и возвращает

trueтогда и только тогда, когда подпоследовательность входной последовательности соответствует шаблону этого сопоставителя

Таким образом, проблема с регулярным выражением. Попробуйте следующее.

String test = "User Comments: This is \t a\ta \ntest\n\n message \n";

String pattern1 = "User Comments: [\\s\\S]*^test$[\\s\\S]*";
Pattern p = Pattern.compile(pattern1, Pattern.MULTILINE);
System.out.println(p.matcher(test).find());  //true

String pattern2 = "(?m)User Comments: [\\s\\S]*^test$[\\s\\S]*";
System.out.println(test.matches(pattern2));  //true

Таким образом, короче говоря, (\\W)*(\\S)*часть в вашем первом регулярном выражении соответствует пустой строке, что *означает ноль или более вхождений, а реальная совпавшая строка - это User Comments:не вся строка, как вы ожидаете. Второй сбой, поскольку он пытается сопоставить всю строку, но не может, поскольку \\Wсовпадает с несловесным символом, т.е. [^a-zA-Z0-9_]и первый символ является Tсимволом слова.

Amarghosh
источник
Я хочу сопоставить любую строку, которая начинается с «Комментарии пользователей», и строка может также содержать символы новой строки. Поэтому я использовал шаблон, User Comments: [\\s\\S]*и это сработало. (спасибо!) Из ответа @Tim я получил образец User Comments:(.*), это тоже нормально. Теперь, есть ли рекомендуемый или лучший способ среди них, или это всего лишь два способа сделать то же самое?
Нивас
@Nivas Я не думаю, что будет какая-то разница в производительности; но я думаю, что (.*)вместе с DOTALLфлагом это более очевидно / читабельно, чем([\\s\\S]*)
Amarghosh
Это лучший ответ .... обеспечивает как доступ к Java-коду, так и опции Pattern String для возможности MultiLine.
GoldBishop
0

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

Иегуда Шварц
источник