7 дек. 2007 г.

Regular Expression Introduction

Some people, when confronted with a problem, think "I know, I’ll use regular expressions." Now they have two problems.

Jamie Zawinski in comp.lang.emacs.

Давно это было, честно сказать и забыл уже - но считаю необходимым поделиться с общественностью :
Введение в регулярные выражения на Java.

/*
* Created on 12.02.2005 0:10:43
*/
package test.regexp;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.TestCase;


/**
* Regular Expression Introduction
* @author Vladimir ``BoB'' Dolzhenko
* @see "Mastering Regular Expressions" by Jeffrey E.F. Friedl
* @see {@link java.util.regex.Pattern}
* @see {@link java.util.regex.Matcher}
*/
public class RegExpIntro extends TestCase {

public static void main(String[] args) {
junit.textui.TestRunner.run(RegExpIntro.class);
}

public void setUp(){
assertTrue("Regular Expression Introduction started", true);
}

public void tearDown(){
assertTrue("Regular Expression Introduction finished.", true);
}

public void testSymbolClass(){
//  символьный класс
//  Представляет собой набор допустимых символов для символа в тексте
//  задаётся при помощи конструкции [...]

String regexp = "[ea]";

// The matches method attempts to match the entire input sequence against the pattern.
assertTrue("e".matches(regexp)); // e is correspoding to regexp 
assertTrue("a".matches(regexp)); // a is corresponding to regexp too !  

assertFalse("u".matches(regexp)); // u (and other) is NOT corresponding to regexp  
}

public void testInvertSymbolClass(){
//  инвертированный символьный класс
//  класс совпадает с любыми символами, не входящими в список.
//  конструкция [^...], 

String regexp = "[^ea]";

assertFalse("e".matches(regexp)); // e is not correspoding to regexp 
assertFalse("a".matches(regexp)); // a is not corresponding to regexp too !  

assertTrue("u".matches(regexp)); // u (and other) IS corresponding to regexp
}

public void testSymbolClassIntervals(){
//  интервалы в символьном классе:
//  требуется найти цифры, очевидно, что символьный класс 
//  [0123456789], но такая запись слишком громоздка и неудобна.Для таких целей сущетсвуют
//  интервалы: [0123456789] == [0-9]

String regexp = "[0-9]";

assertTrue("0".matches(regexp)); // 0 is corresponding to regexp
assertTrue("2".matches(regexp)); // 2 is corresponding to regexp too

assertFalse("q".matches(regexp));
} 

public void testSymbolClassMetaSymbols(){
//  метасимволы, определяющие символьные классы:
//  . (точка) - определяет любой символ
//  \s == [ \t\n\r]
//   "пропуской" символ - пробел, табуляция, новая строка (space)
//  \S == [^ \t\n\r]
//   всё, что не относится к \s
//  \w == [a-zA-Z0-9_]
//   буквы, цифры, знак подчёркивания (word)
//  \W == [^a-zA-Z0-9_]
//   всё, что не относится к \w
//  \d == [0-9]
//   цифры (digits)
//  \D == [^0-9]
//   всё, что не относится к \d

String regexp = ".";

assertTrue("c".matches(regexp));
assertTrue("$".matches(regexp));

regexp = "\\w"; // due to java style '\' have to be escaped by itself  

assertTrue("c".matches(regexp)); // is corresponding to regexp
assertFalse("$".matches(regexp)); // is NOT corresponding to regexp


regexp = "\\W";
//   vice-versa
assertFalse("c".matches(regexp)); 
assertTrue("$".matches(regexp));

}


public void testSymbolClassHiddenDangers(){
// подводный камень #1

// символ - только внутри определения символьного класса
// интерпретируется как метасимвол, во всех других случаях это обычный символ
String regexp = "-";

assertTrue("-".matches(regexp));

// если только он стоит между символьными литералами определяющими начало и конец символьного интервала

regexp = "[-A-Z]"; // символьный класс будет состоять из A-Z и символа -

assertTrue("-".matches(regexp));
assertTrue("C".matches(regexp));

regexp = "[A-Z-]"; // символьный класс будет состоять из A-Z и символа -

assertTrue("-".matches(regexp));
assertTrue("C".matches(regexp));  

// Избыточно (для параноиков): в любом случае, для пущей безопасности, можно писать "\-" вместо "-"
// и "[\-A-Z]" или "[A-Z\-]" вместо "[-A-Z]".
regexp = "[A-Z\\-]"; // символьный класс будет состоять из A-Z и символа -

assertFalse("\\".matches(regexp));
assertTrue("-".matches(regexp));
assertTrue("C".matches(regexp));


// точка только вне определения символьного класса определяет любой символ
// т.е внутри символьного класса точка действует не как метасимвол, а как обычный символ точки
regexp = "[.]";  

assertTrue(".".matches(regexp));
assertFalse("a".matches(regexp));  

// но метасимволы определяющие символьные классы 
// действуют как внутри определения символьного класса, так и вне его
regexp = "[\\w]"; // т.е это будет == [a-zA-Z0-9_], а не (\|w)

assertFalse("\\".matches(regexp));
assertTrue("w".matches(regexp));
assertTrue("a".matches(regexp));


}

public void testSelect(){
// выбор
// позволяет объединить несколько рег.выражений в одно, совпадающее с любым из выражений-компонентов.

String regexp = "this|super";

assertTrue("this".matches(regexp)); // is corresponding to regexp
assertTrue("super".matches(regexp)); // is corresponding to regexp
assertFalse("thisuper".matches(regexp));
assertFalse("thissuper".matches(regexp));
assertFalse("something".matches(regexp)); // is NOT corresponding to regexp
}

public void testQuantificator(){
//  квантификаторы
//  требуется найти color или colour, при этом u может "выпадать". 
//  Для этого существует квантификатор необязательного символа ? и регулярное выражение выглядит
//  как colou?r
//  т.е кол-во минимальных воспадаений 0, максимальных 1
//  существуют другие метасимволы определяющие кол-во совпадений
//  * = min 0, max = infinity
//  + = min 1, max = infinity
//
//  так же существуют интервалы:
//  {min,max} соот-тует минимальному количеству совпадению min, максимальному max
//  {eq} соот-тует точному совпадению eq

String regexp = "colou?r";

assertTrue("color".matches(regexp)); // is corresponding to regexp
assertTrue("colour".matches(regexp)); // is corresponding to regexp

assertFalse("coloir".matches(regexp)); // is NOT corresponding to regexp  


regexp = "\\w+"; // ONE or more word-symbols occurence

assertTrue("regular_expression".matches(regexp));
assertTrue("125".matches(regexp));

assertFalse("".matches(regexp)); // there is no even one symbol
assertFalse("!@#$%".matches(regexp)); // there are not word-symbols
assertFalse("Hello!".matches(regexp)); // not of them are word-symbols


regexp = "\\w*"; // ZERO or more word-symbols occurence

assertTrue("regular_expression".matches(regexp));
assertTrue("125".matches(regexp));
assertTrue("".matches(regexp)); // empty string matches! 

assertFalse("!@#$%".matches(regexp)); // there are not word-symbols
assertFalse("Hello!".matches(regexp)); // not of them are word-symbols
}

public void testQuantificatorHiddenDangers(){
// подводный камень #2
// максимальное совпадение (жадность regexp'а)
// Квантификаторы действуют максимально, по принципу "кто раньше встал, того и тапки"  

String regexp = "(.*)(.*)";

Pattern pattern = Pattern.compile(regexp);

String str = "this.changed = true";
Matcher matcher = pattern.matcher(str);

assertTrue(matcher.matches());  
assertEquals(matcher.groupCount(), 2);

String firstGroup = str.substring(matcher.start(1), matcher.end(1));
// первый квантификатор отработал первым и забрал всю строку
assertEquals(firstGroup, str);

String secondGroup = str.substring(matcher.start(2), matcher.end(2));
// второму ничего не осталось
assertEquals(secondGroup, "");

// Сравните:

regexp = "(.*)(.+)";

pattern = Pattern.compile(regexp);

matcher = pattern.matcher(str);

assertTrue(matcher.matches());
assertEquals(matcher.groupCount(), 2);

firstGroup = str.substring(matcher.start(1), matcher.end(1));
// первый квантификатор отработал первым и забрал столько, 
// сколько позволил весь regexp
// т.е забрал максимальное количество символов при котором ещё возможно 
// совпадение regexp'а

assertEquals(firstGroup, "this.changed = tru");

secondGroup = str.substring(matcher.start(2), matcher.end(2));
assertEquals(secondGroup, "e");

// other example here
regexp = "('.*')";

pattern = Pattern.compile(regexp);
str = "'GNU' is 'GNU is Not Unix'";
matcher = pattern.matcher(str);

assertTrue(matcher.matches());
firstGroup = str.substring(matcher.start(1), matcher.end(1));
// ожидаемое значение будет не 'GNU', а вся исходная строка, т.к квантификатор очень жадный
assertEquals(firstGroup, str);
}

public void testEscapeMetasymbols(){
// Многие символы в regexp'ах используются как ключевые управляющие символы
// для того, чтобы задавать совпадение именно по этим символам их необходимо экранировать  

String regexp = "\\\\" 
+ "\\." 
+ "\\+" 
+ "\\*" 
+ "\\{" 
+ "\\}" 
+ "\\(" 
+ "\\)";  
// это далеко не полный список метасимволов
// см. и читайте Mastering Regular Expressions за более детальной информацией

assertTrue("\\.+*{}()".matches(regexp));
}

public void testGroups(){
// Группы позволяют запоминать найденные совпадения, к которым можно 
// в послетствии обращаться. 
// Конструкция: ()
// Группы начинают нумероваться с 1ой 
String regexp = "(\\w+)\\.(\\w+)\\s*=\\s*(true|false)";
Pattern pattern = Pattern.compile(regexp);

String str = "this.changed = true";
Matcher matcher = pattern.matcher(str);

// вызов matcher.matches() (либо matcher.lookingAt(), либо matcher.find() ) обязателен, 
// т.к. он запускает механизм нахождения соответствия
assertTrue(matcher.matches()); 

assertEquals(matcher.groupCount(), 3);

String firstGroup = str.substring(matcher.start(1), matcher.end(1));
assertEquals(firstGroup, "this");

String secondGroup = str.substring(matcher.start(2), matcher.end(2));
assertEquals(secondGroup, "changed");

String thirdGroup = str.substring(matcher.start(3), matcher.end(3));
assertEquals(thirdGroup, "true");

// Группы также позволяют отделить логические части выбора
regexp = "if \\((true|false)\\) \\{";
assertTrue("if (true) {".matches(regexp));
assertTrue("if (false) {".matches(regexp));

// следующий пример относится не столько к regexp'ам, сколько к java api
regexp = "(\\w+)";
pattern = Pattern.compile(regexp);

String string = " This is a table.";

matcher = pattern.matcher(string);

// в данном случае matcher.find() запускает механизм поиска соответсвий
for(int occurence=0;matcher.find();occurence++){
String substring = string.substring(matcher.start(1), matcher.end(1));
switch(occurence){
case 0:
assertEquals("This", substring);
break;
case 1:
assertEquals("is", substring);
break;
case 2:
assertEquals("a", substring);
break;
case 3:
assertEquals("table", substring);
break;
default:
//  нет больше строк удовлетворящих regexp'у
fail();
break;
}
} 

// меняем исходную строку, но не меняем regexp

string = " This is an apple ";

// нет необходимости вызывать pattern.matcher(string);
// достаточно сбросить matcher

matcher.reset(string);  

for(int occurence=0;matcher.find();occurence++){
String substring = string.substring(matcher.start(1), matcher.end(1));
switch(occurence){
case 0:
assertEquals("This", substring);
break;
case 1:
assertEquals("is", substring);
break;
case 2:
assertEquals("an", substring);
break;
case 3:
assertEquals("apple", substring);
break;
default:
fail();
break;
}
}  
}

public void testMatcherHiddenDangers(){
// всё тоже самое, только без запуска механизма поиска совпадений
String regexp = "(\\w+)\\.(\\w+)\\s*=\\s*(true|false)";
Pattern pattern = Pattern.compile(regexp);

String str = "this.changed = true";
Matcher matcher = pattern.matcher(str);

// assertTrue(matcher.matches()); // не выполнил matches() - лови exception 

try{
String firstGroup = str.substring(matcher.start(1), matcher.end(1)); // вот тут возникнет exception !
assertEquals(firstGroup, "this");   
fail(); // до сюда дойти не должен из-за возникшего exception'а

String secondGroup = str.substring(matcher.start(2), matcher.end(2));
assertEquals(secondGroup, "changed");
fail();

String thirdGroup = str.substring(matcher.start(3), matcher.end(3));
assertEquals(thirdGroup, "true");   
fail();
} catch(IllegalStateException illegalStateException){
assertTrue(true);
}
}

public void testBeginAndEndLine(){
// Метасимвол ^ означает начало строки
String regexp = "(^\\w+)";
Pattern pattern = Pattern.compile(regexp);

String string = "StartLine ^any words$ here endLine";
Matcher matcher = pattern.matcher(string);
for(int occurence=0;matcher.find();occurence++){
String substring = string.substring(matcher.start(1), matcher.end(1));
switch(occurence){
case 0:
assertEquals("StartLine", substring);
break;
default:
// нет больше строк удовлетворящих regexp'у
fail();
break;
}
}  

// Метасимвол $ означает конец строки
regexp = "(\\w+$)";
pattern = Pattern.compile(regexp);

matcher = pattern.matcher(string);
for(int occurence=0;matcher.find();occurence++){
String substring = string.substring(matcher.start(1), matcher.end(1));
switch(occurence){
case 0:
assertEquals("endLine", substring);
break;
default:     
// нет больше строк удовлетворящих regexp'у
fail();
break;
}
} 
}

public void testBeginAndEndLineHiddenDangers(){  
// подводный камень #3

// Символы $ ^ внутри симльвольного класс интерпретируются как обычные символы

// поиск символа $ или ^ можно производить и без введения символьного класса
// эти метасимволы, как и все другие метасимволы достаточно заэкранировать, 
// чтобы искать как обычные символы: \$ или \^
String regexp = "([$^]\\w+)";

Pattern pattern = Pattern.compile(regexp);

String string = "StartLine ^any words$ here endLine";
Matcher matcher = pattern.matcher(string);
for(int occurence=0;matcher.find();occurence++){
String substring = string.substring(matcher.start(1), matcher.end(1));
switch(occurence){
case 0:
assertEquals("^any", substring);
break;
default:
// нет больше строк удовлетворящих regexp'у
fail();
break;
}
}  
}

public void testGroupsOrder(){
// Группы нумеруются в порядке их открытия в regexp'е
// (т.е. в том же порядке, что и их открывающие скобки):
String str = "ABC";
String regexp = "(((\\w)\\w)\\w)";
Pattern pattern = Pattern.compile(regexp);
Matcher matcher = pattern.matcher(str);
assertTrue(matcher.matches());  
assertEquals(matcher.groupCount(), 3);

String firstGroup = str.substring(matcher.start(1), matcher.end(1));
assertEquals(firstGroup, "ABC");

String secondGroup = str.substring(matcher.start(2), matcher.end(2));
assertEquals(secondGroup, "AB");

String thirdGroup = str.substring(matcher.start(3), matcher.end(3));
assertEquals(thirdGroup, "A");  
}

public void testBackReferences(){
// Обратные ссылки позволяют требовать совпадение со значением ранее совпавшей группы
// конструкция: \N , где N - номер группы 
String regexp = "('|\")\\w+\\1";

assertTrue("'Test'".matches(regexp));
assertTrue("\"Test\"".matches(regexp));

assertFalse("'Test\"".matches(regexp));
assertFalse("\"Test'".matches(regexp));

}

public void testReferencesInReplacement(){
// Ссылки на группы можно использовать в выражении для замены
// конструкция: $N , где N - номер группы

// ВНИМАНИЕ !!! В Java \N используется как обратная ссылка на группу внутри regexp'а,
// а $N как ссылка в строке замены на группу из regexp'а

// удаление повторяющихся слов
String str = "the  the table of the theories and the duties duties";

String regexp = "(\\w+)\\s+(\\1(\\s+|$))";

String replacedString = str.replaceAll(regexp, "$2");
assertEquals(replacedString, "the table of the theories and the duties");
}
}

Комментариев нет: