C# 6.0 완벽 가이드/ 정규 표현식

  • 정규 표현식(regular expression) 줄여서 정규식(regex)은 문자 패턴을 식별하는 수단이다.
    • 정규식을 지원하는 .NET 형식들은 Perl 5의 정규 표현식 문법을 따르며, 패턴을 찾는 기능뿐만 아니라 찾아 바꾸는 기능도 지원한다.
  • 정규 표현식은 이를테면 다음과 같은 과제에 쓰인다.
    • 패스워드나 전화번호 같은 텍스트 입력의 유효성 점검(ASP.NET은 이 용도만을 위해 ReularExpressionValidator라는 컨트롤을 제공한다)
    • 텍스트 자료를 좀 더 구조화된 형태로 파싱(이를테면 HTML 페이지에서 자료를 추출해서 데이터베이스에 저장하는 등)
    • 문서에 있는 특정 패턴의 텍스트를 치환

정규 표현식의 기초

  • 정규 표현식에는 여러 연산자가 있는데, 그중 한정사(quantifier; 양화사)라고 부르는 연산자들이 특히나 많이 쓰인다.
    • 한정사 중 하나인 ?는 그 앞의 항목이 0회 또는 1회 나와야 한다는 뜻이다. 다른 말로 하면 ?는 그 앞의 항목이 선택적(optional)임을 뜻한다.
    • 예컨대 “colou?r”라는 정규 표현식은 color와도 부합하고 colour와도 부합하지만, colouur와는 부합하지 않는다.
Console.WriteLine(Regex.Match("color", @"colou?r").Success);  // True
Console.WriteLine(Regex.Match("colour", @"colou?r").Success);  // True
Console.WriteLine(Regex.Match("colouur", @"colou?r").Success);  // False
  • Regex.Match는 주어진 문자열에서 주어진 패턴과 부합하는 부분 문자열을 찾는다.
    • 이 메서드가 돌려주는 객체에는 패턴과 부합하는 부분 문자열의 시작 색인을 담은 Index 속성과 길이를 담은 Length 속성, 그리고 부함 문자열 자체를 담은 Value 속성이 있다.
Match m = Regex.Match("any colour you like", @"colou?r");

Console.WriteLine(m.Success);  // True
Console.WriteLine(m.Index);  // 4
Console.WriteLine(m.Length);  // 6
Console.WriteLine(m.Value);  // colour
Console.WriteLine(m.ToString());  // colour
  • Regex.Match를 string의 IndexOf 메서드의 좀 더 강력한 버전이라고 생각해도 될 것이다. 차이점은 Regex.Match는 주어진 문자열을 곧이곧대로 검색하는 것이 아니라 패턴을 검색한다는 것이다.
  • IsMatch 메서드는 Match 호출 후 Success 속성을 판정하는 과정을 하나로 엮은 단축 메서드이다.
  • 정규 표현식 엔진은 기본적으로 왼쪽에서 오른쪽으로 패턴을 점검하므로, Match는 가장 왼쪽의 부함만을 돌려준다. 더 많은 부합을 얻으려면 NextMatch 메서드를 사용해야 한다.
Match m1 = Regex.Match("One color? Threre are two colours in my head!", @"colou?rs?");
Match m2 = n1.NextMatch();
Console.WriteLine(m1);  // color
Console.WriteLine(m2);  // colour
  • Matches 메서드는 모든 부합을 배열에 담아 돌려준다.
foreach (Match m in Regex.Match("One color? Threre are two colours in my head!", @"colou?rs?"))
  Console.WriteLine(m);
  • 흔히 쓰이는 또 다른 정규 표현식 연산자로 대안 선택자(alternator)가 있다. 대안 선택자는 수직선 기호 |로 표시한다. 대안 선택자는 말 그대로 선택할 수 있는 대안들을 표현한다.
    • 예컨대 다음은 “Jen”이나 “Jenny”, “Jennifer”와 부합한다.
Console.WriteLine(Regex.IsMatch("Jenny", "Jen(ny|nifer)?"));  // True
  • 대안 선택자를 감싸는 괄호는 대안들을 정규식의 나머지 부분과 구분하는 역할을 한다.
  • .NET Framework 4.5 부터는 정규 표현식 부합 메서드 호출 시 만료 시간을 지정할 수 있다.
    • TimeSpan 객체로 주어진 시간이 다 지나도 부합 연산이 완료되지 않으면 RegexMatchTimeoutException 예외가 발생한다.
    • 임의의 정규 표현식(이를테면 고급 검색 대화상자에 사용자가 입력한 정규식)을 처리하는 프로그램이라면 잘못된 또는 악의적인 정규 표현식 때문에 프로그램이 무한히 멈추는 일을 방지하기 위해 이러한 시간 만료 기능을 활용하는 것이 바람직하다.

컴파일된 정규 표현식

  • 앞의 예제에서는 같은 패턴으로 정적 RegEx 메서드를 여러 번 호출했다. 그렇게 하는 대신, 원하는 패턴과 함께 RegexOptions.Compiled를 지정해서 Regex 객체를 생성하고 그 객체에 대해 인스턴스 메서드들을 호출하는 방법도 있다.
Regex r = new Regex(@"sausages?", RegexOptions.Compiled);
Console.WriteLine(r.Match("sausage"));  // sausage
Console.WriteLine(r.Match("sausages"));  // sausages
  • RegexOptions.Compiled를 지정하면 RegEx 인스턴스는 가벼운 코드 생성 기능(Reflection.Emit의 DynamicMethod)을 이용해서 해당 정규 표현식을 위한 코드를 동적으로 구축하고 컴파일한다. 초기화시 컴파일 비용이 있긴 하지만, 그 후에는 정규 표현식 부합 연산을 좀 더 빠르게 수행할 수 있다.
  • Regex 인스턴스는 불변이 객체이다.
  • .NET Framework의 정규 표현식 엔진은 빠르다. 초기 컴파일 과정을 거치지 않더라도 간단한 패턴 부합에 걸리는 시간은 1마이크로초 미만이다.

RegexOptions 열거형

  • RegexOptions 플래그 열거형은 부합 연산의 작동 방식을 조율하는 옵션들을 나타낸다. RegexOptions의 흔한 용도 하나는 다음처럼 대소문자를 구분하지 않고 패턴을 찾는 것이다.
Console.WriteLine(Regex.Match("a", "A", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant));
  • RegexOptions의 플래그들 대부분을 정규 표현식 자체에서 한 글자 부호를 이용해서 활성화 하는 것도 가능하다. 다음이 그러한 예이다.
Console.WriteLine(Regex.Match("a", @"(?i)A"));  // a
  • 특정 플래그를 비활성화하려면 다음처럼 해당 문자열 앞에 -를 붙이면 된다.
Console.WriteLine(Regex.Match("AAAa", @"(?i)a(?-i)a"));  // Aa
  • 또 다른 유용한 옵션으로 IgnorePatternWhitespace 또는 (?x)가 있다. 이 옵션을 활성화하면 정규 표현식 패턴의 공백 문자들이 무시된다.
    • 이 옵션은 정규 표현식의 가독성을 높이기 위해 공백 문자들을 적절히 추가하고자 할 때 유용하다.
  • 아래 표에는 RegExOptions 열거형의 모든 값과 그에 해당하는 한 글자 부호가 나와 있다.
열거형 값 정규 표현식 부호 설명
None
IgnoreCase i 대소문자를 구분하지 않는다(기본적으로 정규 표현식은 대소문자를 구분한다)
Multiline m ^와 $가 문자열의 시작/끝이 아니라 한 행의 시작/끝과 부합하게 한다.
ExplicitCapture n 명명된 또는 번호가 명시적으로 지정된 그룹들만 갈무리한다
Complied IL로의 컴파일을 강제한다.
Singleline s .가 \n을 포함한 모든 문자와 부합하게 한다(기본은 \n을 제외한 모든 문자와 부합하는 것이다)
IgnorePatternWhitespace x 패턴에서 탈출되지 않은 공백 문자들을 제거한다
RightToLeft r 오른쪽에서 왼쪽으로 검색한다. 정규 표현식 도중에 이 옵션을 지정할 수는 없다.
ECMAScript ECMA 준수를 강제한다(기본적으로 .NET Framework의 정규 표현식 구현은 ECMA를 준수하지 않는다)
CultureInvariant 문자열 비교 시 문화권 고유의 규칙을 따르지 않도록 한다.

 

문자 탈출

  • 정규 표현식에는 다음과 같은 메타문자(metacharacter)들이 있다. 이들은 해당 문자 그대로 쓰이는 것이 아니라 특별한 의미로 쓰인다.
\ * + ? | { [ ( ) ^ $ . #
  • 이러한 메타문자들을 그대로 쓰려면 그 앞에 역슬래시를 붙여서 ‘탈출’ 시켜야 한다. 다음은 문자열 ‘what?”와의 부합을 위해 메타문자 ? 를 탈출시키는 예이다.
Console.WriteLine(Regex.Match("what?", @"what\?"));  // what? (정확한 답)
Console.WriteLine(Regex.Match("what?", @"what?"));  // what (부정확한 답)
  • 대괄호로 감싸인 문자집합 안에 있는 문자들에는 이러한 규칙이 적용되지 않는다. 문자 집합 안에서는 메타문자들이 해당 문자 그대로 처리된다.
  • Regex의 Escape 메서드는 주어진 문자열에 담긴 정규 표현식 메타문자들을 해당 탈출 표현으로 치환한 문자열을 돌려주고, Unescape 메서드는 그 반대의 일을 한다. 예컨대 다음과 같다.
Console.WriteLine(Regex.Escape(@"?"));  // \?
Console.WriteLine(Regex.Unescape(@"\?"));  // ?>
  • (?x) 옵션을 지정하지 않는 한, 정규 표현식의 빈칸은 문자 그대로 빈칸으로 취급된다.
Console.WriteLine(Regex.IsMatch("hello world", @"hello world"));  // True

문자 집합

  • 문자 집합(character set)은 대상 문자열의 한 문자에 부합할 수 있는 일단의 문자들을 지정하는 수단이다.
정규식 의미 역(부정)
[abcdef] 나열된 문자 중 하나와 부합한다. [^abcdef]
[a-f] 주어진 범위에 있는 문자 중 하나와 부합한다. [^a-f]
\d 십진 숫자 하나와 부합한다. [0-9]와 같다. \D
\w 단어(word) 문자 하나와 부합한다(기본적으로 ‘단어 문자’의 의미는 CultureInfo.CurrentCulture의 설정을 따른다.
예컨대 영어에서 단어 문자는 [a-zA-Z_0-9]와 같다)
\W
\s 공백 문자 하나와 부합한다. [\n\r\t\f\v ]와 같다. \S
\p{범주} 지정된 범주의 한 문자와 부합한다. \P
. (기본 모드) \n를 제외한 모든 문자와 부합한다. \n
. (SingleLine 모드) 모든 문자와 부합한다. \n

 

  • 패턴 안에서 여러 문자를 대괄호로 감싸면, 대상 문자열의 한 문자가 그 문자 중 딱 하나와 부합한다.
Console.WriteLine(Regex.Matches("That is that", [Tt]hat).Count); // 2
  • 부합하지 않아야 할 문자들을 지정하려면, 다른 말로 해서 주어진 문자들을 제외한 문자들하고만 부합하게 하려면, 대괄호 안의 첫 문자 앞에 ^ 기호를 붙이면 된다.
Console.WriteLine(Regex.Match("quiz qwerty", "q[^aeiuo]").Index);  // 5
  • 문자들을 일일이 나열하는 대신 하이픈(-)을 이용해서 문자들의 범위를 지정할 수도 있다. 다음의 정규 표현식은 하나의 체스 수(move)와 부합한다.
Console.WriteLine(Regex.Match("b1-c4", @"[a-h]\d-[a-h]\d").Success);  // True
  • \d는 십진 숫자를 나타낸다. 즉, \d는 임의의 십진 숫자와 부합한다. \D는 십진 숫자가 아닌 모든 문자와 부합한다.
  • \w는 단어 문자를 나타내는데, 단어 문자는 글자(letter)와 숫자, 밑줄로 구성된다.
    • \W는 단어 문자가 아닌 모든 문자와 부합한다. 이는 키릴 문자 등 영문 알파벳 이외의 문자 체계에서도 같은 방식으로 작동한다.
  • .(마침표)는 \n을 제외한 모든 문자(\r 포함)와 부합한다.
  • \p는 지정된 범주(category)에 속한느 문자와 부합한다. 예컨대 {Lu}는 대문자들, {P}는 문장부호들을 뜻한다.
Console.WriteLine(Regex.IsMatch("Yes, please", @"\p{P}"));  // True

한정사

  • 한정사들은 주어진 항목이 몇 번이나 부합할 수 있는지를 결정한다.
한정사 의미
* 0회 이상 부합
+ 1회 이상 부합
? 0회 또는 1회 부합
{n} 정확히 n회 부합
{n,} 적어도 n회 부합
{n,m} n에서 m회 사이 부합

 

  • * 한정사는 그 앞의 문자 또는 그룹이 0회 이상 부합할 수 있음을 뜻한다. 예컨대 다음 정규식은 cv.doc 뿐만 아니라 cv 다음에 버전 번호가 붙은 모든 파일 이름(예컨대 cv2.doc, cv15.doc 등)과도 부합한다.
Console.WriteLine(Regex.Match("cv15.doc", @"cv\d*\.doc").Success);  // True
  • 파일 확장자 앞의 마침표를 역슬래시로 탈출시켜야 했음을 주목하기 바란다.
  • 다음 정규식은 cv와 .doc 사이에 그 어떤 문자들이 있어도 부합한다. 이는 dir cv*.doc에 해당한다.
Console.WriteLine(Regex.Match("cvjoint.doc", @"cv.*\.doc").Success);  // True
  • + 한정사는 그 앞의 문자 또는 그룹이 1회 이상 부합함을 뜻한다.
Console.WriteLine(Regex.Matches("slow! yeah slooow!", "slo+w").Count);  // 2
  • { } 한정사로는 구체적인 횟수 또는 횟수의 범위를 지정할 수 있다. 다음 정규식은 최고혈압과 최저혈압의 쌍과 부합한다.
Regex bp = new Regex(@"\d{2,3}/\d{2,3}");
Console.WriteLine(bp.Match("예전에는 160/110이었는데 "));  // 160/110
Console.WriteLine(bp.Match("지금은 115/75 밖에 안되네요"));  //115/75

탐욕스런 한정사 대 게으른 한정사

  • 기본적으로 한정사들은 게으르게(lazy) 작용하는 것이 아니라 탐욕스럽게(greedy) 작용한다. 탐욕스러운 한정사는 허용되는 한에서 최대한 많은 문자 또는 그룹들과 부합하려 한다.
    • 반대로 게으른 한정사는 허용되는 한에서 최소한으로만 부합하려 한다. 한정사를 게으르게 만들려면 한정사 뒤에 ? 기호를 붙이면 된다.
    • 두 한정사의 차이를 보여주는 예로 다음과 같은 HTML 조각이 있다고 하자.
string html = "<i>기본적으로</i> 한정사들은 <i>탐욕스러운</i> 녀석들이다";
  • 여기서 이탤릭체로 표현되는 두 문구만 추출하려면 어떻게 해야 할까? 그냥 다음처럼 하면
foreach (Match m in Regex.Matches(html, @"<i>.*</i>"))
  Console.WriteLine(m);
  • 부합이 두 개 나오는 것이 아니라, 다음과 같은 부합 하나만 나온다.
<i>기본적으로</i> 한정사들은 <i>탐욕스러운</i>
  • 이는 * 한정사가 탐욕스러워서 </i>를 만날 때까지 최대한 많은 문자와 부합하기 때문에 생긴 문제이다. 즉, 이 * 한정사는 첫 </i>를 지나쳐서 마지막 </i>에 도달해서야(즉, 패턴의 나머지 부분이 여전히 부합할 수 있는 마지막 지점까지 가서) 멈춘다.
  • 원하는 결과를 얻으려면 한정사를 게으르게 만들어야 한다.
foreach (Match m in Regex.Matches(html, @"<i>.*?</i>"))
  Console.WriteLine(m);
  • 이제 *는 패턴의 나머지가 부합할 수 있는 첫 지점(첫 번째 </i>)까지만 간다. 결과는 다음과 같다.
<i>기본적으로</i> 
<i>탐욕스러운</i>

너비 0 단언

  • 정규 표현식 문법에는 하나의 부합 이전이나 이후에 만족해야 하는 조건을 지정하는 패턴 구축 요소들이 있다. 전방탐색(lookahead; 또는 미리보기), 후방탐색(lookbehind; 또는 돌아보기), 앵커(anchor), 단어 경계(word boundary)가 바로 그것이다.
    • 이들을 너비 0 (zero-width) 단언이라고 부르는데, 이는 이들이 부합 자체의 너비(길이)를 늘리지 않기 때문이다.

전방탐색과 후방탐색

  • (?=정규식) 패턴은 그 다음의 텍스트가 정규식과 부합하는지 점검하되,  정규식은 결과에 포함하지 않는다. 이를 양성 전방탐색(positive lookahead)이라고 부른다.
    • 다음은 “miles”라는 단어 앞에 있는 수치를 추출하는 예이다.
Console.WriteLine(Regex.Match("say 25 miles more", @"\d+\s(?=miles)"));  // 25
  • 패턴 부합을 만족하려면 반드시 텍스트에 “miles”가 있어야 하지만, 단어 miles 자체는 결과에 포함되지 않았음을 주목하기 바란다.
  • 일단 전방탐색 조건이 만족되면, 마치 전방탐색이 없었던 것처럼 패턴 부합이 진행된다.
    • 예컨대 앞의 전방탐색 정규식에 다음처럼 .*를 추가하면 결과는 25 miles more가 된다.
Console.WriteLine(Regex.Match("say 25 miles more", @"\d+\s(?=miles).*"));  // 25 miles more
  • 전방탐색은 이를테면 강한 패스워드 규칙을 강제할 때 유용하다. 패스워드가 적어도 여섯 자이고 숫자가 적어도 하나는 있어야 한다고 하자. 전방탐색을 이용하면 그러한 규칙을 다음과 같이 구현할 수 있다.
string password = "...";
bool ok = Regex.IsMatch(password, @"(?=.*\d).{6,}");
  • 이 예제의 정규식은 우선 문자열 어딘가에 숫자가 하나 있는지를 전방탐색을 이용해서 점검한다. 그것이 만족되면 마치 전방탐색이 일어나지 않았던 것처럼 원래 위치로 돌아가서, 문자열이 여섯 자 이상인지 판정한다.
  • 음성 전방탐색(negative lookahead)은 양성 전방탐색과 의미가 반대이다. (?!정규식) 형태로 표기하는 음성 전방탐색은 이후의 텍스트가 정규식과 부합하지 않아야 만족된다.
    • 예컨대 다음 정규식은 텍스트에 “good”이 있어야, 그러나 그 다음에 “however”나 “but”이 없어야 부합한다.
string regex = "(?!)good(?!.*(however|but))";
Console.WriteLine(Regex.IsMatch("Good work! But...", regex));  // False
Console.WriteLine(Regex.IsMatch("Good work! Thanks!", regex));  // True
  • 다음으로 (?<=정규식)은 양성 후방탐색을 뜻한다. 양성 후방탐색은 현재 부합 위치 이전(문자열의 시작 방향)의 텍스트가 정규식과 부합해야 만족된다.
    • 그 반대인 음성 후방탐색은 (?<!정규식) 형태로 표기하며, 현재 부합위치 이전(문자열의 시작 방향)의 텍스트가 정귯힉과 부합하지 않아야 만족된다.
    • 예컨대 다음 정규식은 텍스트에 “good”이 있어야, 그러나 그 이전에 “however”가 없어야 텍스트와 부합한다.
string regex = "(?!)(?<!however.*)good";
Console.WriteLine(Regex.IsMatch("However good, we..., regex));  // False
Console.WriteLine(Regex.IsMatch("Very good, thanks!", regex));  // True

앵커

  • 앵커는 ^와 $ 두 가지이다. 이들은 문자가 아니라 특정 위치와 부합한다. 기본적으로 이들의 의미는 다음과 같다.
    • ^
      • 문자열의 시작과 부합한다.
    • $
      • 문자열의 끝과 부합한다.
  • 문맥에 따라서는 ^가 앵커가 아니라 문자 부류 부정 기호를 뜻하기도 한다. 비슷하게 문맥에 따라서 $는 앵커가 아니라 치환 그룹 표식을 뜻하기도 한다.
  • 다음은 이들을 사용하는 예이다.
Console.WriteLine(Regex.Match("Not now", "^[Nn]o"));  // No
Console.WriteLine(Regex.Match("f = 0.2F", "[Ff]$"));  // F
  • 그런데 부합 메서드 호출시 RegexOptions.Multiline을 지정하거나 정규식에 (?m)을 포함하면 이들의 의미가 약간 달라진다. 그런 경우
    • ^는 문자열의 시작 또는 한 행(line)의 시작(\n 바로 뒤)과 부합한다.
    • $는 문자열의 끝 또는 한 행의 끝(\n 바로 앞)과 부합한다.
  • 이러한 다중 행 모드에서 $를 사용할 떄 주의할 점이 있다. Windows에서는 줄바꿈(새 줄)을 거의 항상 \r\n으로 표시한다(그냥 \n이 아니라). 따라서 일반적인 경우 $를 제대로 사용하려면 다음과 같이 양성전방탐색을 이용해서 \r도 부합할 필요가 있다.
(?=\r?$)
  • \r을 양성 전방탐색으로 점검하므로 \r 자체는 결과에 포함되지 않는다. 다음은 이를 이용해서 “.txt”로 끝나는 모든 행과 부합하는 정규식이다.
string fileNames = "a.txt" + "\r\n" + "b.doc" + "\r\n" + "c.txt";
string r = @".+\.txt(?=\r?$)";
foreach (Math m in Regex.Matches(fileNames, r, RegexOptions.Multilines))
  Console.Write(m + " ");

// 출력
// a.txt c.txt
  • 다음 예제는 문자열 s에 있는 모든 빈 줄과 부합한다.
MatchCollection emptyLines = Regex.Matches(s, "^(?=\r?$)", RegexOptions.Multiline);
  • 다음은 모든 빈 줄 또는 공백만 있는 줄과 부합한다.
MatchCollection blankLines = Regex.Matches(s, "^[ \t]*(?=\r?$)", RegexOptions.Multiline);
  • 앵커는 문자가 아니라 위치와 부합하므로, 앵커 하나로 이루어진 패턴의 부합 결과는 빈 문자열이다.
Console.WriteLine(Regex.Match("x", "$").Length);  // 0

단어 경계

  • 단어 경계 단언 \b는 단어 문자(\w)와 단어 문자가 아닌 어떤 것, 즉
    • 비단어 문자(\W) 또는
    • 문자열의 시작/끝(^와 $)
  • 사이의 위치에 부합한다. \b는 흔히 하나의 온전한 단어와 부합하는 용도로 쓰인다. 다음이 그러한 예이다.
foreach (Math m in Regex.Matches("Wedding in Sarajevo", @"\b\w+\b"))
  Console.WriteLine(m);

// 출력
// Wedding
// in
// Sarajevo
  • 다음은 단어 경계의 효과를 잘 보여주는 예이다.
int one = Regex.Matches("Wedding in Sarajevo", @"\bin\b").Count; // 1
int two = Regex.Matches("Wedding in Sarajevo", @"in").Count; // 2
  • 다음 정규식은 양성 전방탐색을 이용해서 “(sic)” 앞의 단어를 돌려준다.
string text = "Don't loose (sic) your cool";
Console.WriteLine(Regex.Match(text, @"\b\w+\b\s(?=\(sic/))"));  // loose

그룹

  • 하나의 정규 표현식을 일련의 부분 정규식 또는 그룹들로 분할하는 것이 유용할 때가 있다.
    • 예컨대 206-465-1918 같은 전화번호를 나타내는 다음 정규식을 생각해 보자.
\d{3}-\d{3}-\d{4}
  • 그런데 이를 지역번호와 나머지 전화번호라는 두 그릅으로 나누고 싶다고 하자. 그러면 다음처럼 각 그룹을 괄호로 감싸면 된다.
(\d{3})-(\d{3}-\d{4})
  • 이런 형태의 정규식을 적용하면 각 그룹의 부합 결과가 개별적으로 갈무리(capture)된다.
Match m = Regex.Match("206-465-1918", @"(\d{3})-(\d{3}-\d{4})");
Console.WriteLine(m.Groups[1]); // 206
Console.WriteLine(m.Groups[2]); // 465-1918
  • 0번 그룹은 부합 전체에 해당한다. 다른 말로 하면 0번 그룹은 부합의 Value 속성과 같다.
  • 그룹은 정규 표현식 문법 자체가 지원하는 기능이다. 이 덕분에 정규 표현식 안에서 그룹을 지칭할 수도 있다.
    • 정규식 안에서 n번 그룹을 지칭하려면 \n이라는 표기를 사용하면 된다. 예컨대 (\w)ee\1은 deed나 peep 같은 문자열들과 부합한다.
    • 다음 예제는 시작 글자와 끝 글자가 같은 모든 행을 찾는다.
foreach (Math m in Regex.Matches("pop, pope peep", @"\b(\w)\w+\1\b"))
  Console.Write(m + " ");  // pop peep
  • \w를 감싸는 괄호쌍은 정규 표현식 엔진에게 부분 부합(이 경우 글자(단어 문자) 하나)을 나중에 사용할 수 있도록 그룹에 갈무리하라고 지시한다.
    • 그 그룹을 나중에 \1로 지칭한다. \1은 이 정규식의 첫(1번) 그룹을 뜻한다.

명명된 그룹

  • 길거나 복잡한 정규식의 경우 그룹을 번호가 아니라 이름으로 지칭하면 정규식을 다루기가 좀 더 수월해 진다. 다음은 앞의 예제를 ‘letter’라는 이름의 그룹을 이용해서 다시 작성한 것이다.
string regEx = 
  @"\b" +  // 단어 경계
  @"(?'letter'\w)" +  // 첫 글자와 부합; 그 문자에 letter라는 이름을 부여
  @"\w+" +  // 중간 글자들과 부합
  @"\k'letter'" +  // 'letter'dp goekdgksms akwlakr rmfwkdhk qngkq
  @"\b"; // 단어 경계

foreach (Math m in Regex.Matches("pop, pope peep", regEx))
  Console.Write(m + " ");  // pop peep
  • 갈무리된 그룹에 이름을 부여하는 구문은 다음과 같다.
(?'그룹이름'그룹정규식) 또는 (?<그룹이름>그룹정규식)
  • 그리고 명명된 그룹을 지칭하는 구문은 다음과 같다.
\k'그룹이름' 또는 \k<그룹이름>
  • 다음 예제는 요소 이름이 같은 시작/종료 태그를 찾는 방식으로 단순한(중첩되지 않은) XML/HTML 요소와 부합한다.
string regFind = 
  @"<(?'tag'\w+?).*>" + // 시작 태그와 부합; 요소 이름에 'tag' 라는 이름을 부여
  @"(?'text'.*?)" +  // 텍스트 내용과 부합; 'text'라는 이름을 부여
  @"</\k'tag'>";  // 'tag'에 해당하는 종료 태그와 부합

Match m = Regex.Match("<h1>hello</h1>", regFind);
Console.WriteLine(m.Groups["tag"]);  // h1
Console.WriteLine(m.Groups["text"]);  // hello
  • 중첩된 요소 등 모든 가능한 형태의 XML 구조와 부합하려면 이보다 훨씬 복잡한 정규 표현식이 필요하다. .NET의 정규 표현식 엔진에는 중첩된 태그들과의 부합에 도움이 되는 ‘부합 대칭(matched balanced)’ 패턴이라는 정교한 확장 기능이 있다.

텍스트 치환 및 분할

  • RegEx.Replace는 string.Replace와 비슷하되, 정규 표현식을 이용한다는 점이 다르다.
  • 다음 예제는 “cat”을 “dog”으로 치환한다. 이를 string.Replace로 구현했다면 “catapult”가 “dogapult”로 바뀌었겠지만, 이 예제는 단어 경계가 포함된 패턴을 사용하므로 그런 일이 발생하지 않는다.
string find = @"\bcat\b";
string replace = "dog";
Console.WriteLine(Regex.Replace("catapult the cat", find, replace));  // catapult the dog
  • 치환 문자열 안에서 부합 문자열 전체를 치환 그룹 표식 $0으로 지칭할 수 있다.
    • 다음은 문자열 안에 있는 수치들을 화살괄호(<>)로 감싸는 예이다.
string text = "10 더하기 20은 30"
Console.WriteLine(Regex.Replace(text, @"\d+", @"<$0>"));  // <10> 더하기 <20>은 <30>
  • 정규식에 그룹이 있는 경우, 치환 문자열에서 그 그룹들을 $1, $2, $3 등으로 지칭할 수 있다. 명명된 그룹은 ${그룹-이름}으로 지칭하면 된다.
    • 이러한 기능의 용도를 보여주는 예로 이전에 나온 간단한 XML 요소와 부합하는 정규식을 생각해 보자.
    • 치환 문자열에서 치환 그룹 표식들의 위치를 적절히 조정함으로써 요소의 내용을 XML 특성으로 이동할 수 있다.
string regFind = 
  @"<(?'tag'\w+?>" +  // 시작 태그와 부합; 요소 이름에 'tag'라는 이름을 부여
  @"(?'text'.*?)" +  // 텍스트 내용과 부합; 'text'라는 이름을 부여
  @"</\k'tag'>";  // 'tag'에 해당하는 종료 태그와 부합

string regReplace = 
  @"<${tag}" +  // <tag
  @"value=""" +  // value="
  @"${text}" +  // text
  @"""/>";  // "/>

Console.Write(Regex.Replace("<msg>hello</msg>", regFind, regReplace));

// 결과
// <msg value="hello"/>

MatchEvaluator 대리자

  • Replace에는 MatchEvaluator 대리자를 받는 중복적재 버전이 있다. 이 대리자는 부합마다 호출된다. 이를 이용하면 정규 표현식 문법만으로는 표현할 수 없는 복잡한 치환을 C# 코드로 표현할 수 있다. 예컨대 다음과 같다.
Console.WriteLine(Regex.Replace("6은 10보다 작다", @"\d+", m => (int.Parse(m.Value) * 10).ToString()));

// 출력
// 60은 100보다 작다

텍스트 분할

  • 정적 Regex.Split 메서드는 string.Split 메서드의 좀 더 강력한 버전으로, 정규식 패턴을 분리자(separator)로 사용한다.
    • 다음은 임의의 숫자를 분리자로 사용해서 하나의 문자열을 여러 부분 문자열로 분할하는 예이다.
foreach (string s in Regex.Split("a5b7c", @"\d"))
  Console.Write(s + " ");  // a b c
  • 분할된 부분 문자열들에 분리자는 포함되지 않음을 주목하기 바란다. 분리자 정규식을 양성 전방탐색으로 감싸면 분리자도 포함시킬 수 있다.
    • 다음은 단어들이 낙타 등 방식(camel-case)으로 연결된 무낮열을 대문자를 분리자로 이용해서 개별 단어로 분할하는 예이다.
foreach (string s in Regex.Split("oneTwoThree", @"(?=[A-Z])"))
  Console.Write(s + " ");  // one Two Three

유용한 정규 표현식 예제 모음

미국 사회보장번호(SSN)와 전화번호 부합

string ssNum = @"\d{3}-\d{2}-\d{4}"
Console.WriteLine(Regex.IsMatch("123-45-6789", ssNum));  // True

string phone = @"(?x)( \d{3}[-\s] | \(\d{3}\)\s? )(\d{3}[-\s]?\d{4}";
Console.WriteLine(Regex.IsMatch("123-45-6789", phone));  // True
Console.WriteLine(Regex.IsMatch("(123) 45-6789", phone));  // True

“이름=값” 쌍 추출(한 줄당 하나)

  • 정규 표현식이 다중 행 지시자(?m)로 시작함을 주의하기 바란다.
string r = @"(?m)^\s*(?'name'\w+)\s*=\s*(?'value'.*)\s*(?=\r?$);
string text = @"id = 3 secure = true timeout = 30";

foreach (Match m in Regex.Matches(text, r))
  Console.WriteLine(m.Groups["name"] + " is " + m.Groups["value"]);

// 출력
// id is 3 secure is true timeout is 30

강한 패스워드 규칙 점검

  • 다음 예제는 주어진 패스워드가 적어도 여섯 자이고 숫자나 기호, 문장부호가 포함되어 있는지 점검한다.
string r = @"(?x)^(?=.*(\d|\p{P}|\p{S})).{6,};

적어도 문자 80개로 이루어진 행들

string r = @"(?m)^.{80,}(?=\r?$)";

날짜 및 시간 파싱(N/N/N H:M:S AM/PM)

  • 다음 정규 표현식은 다양한 수치 날짜 서식들과 부합한다. 연도가 처음에 오는 날짜 서식은 물론 마지막에 오는 날짜 서식도 문제없이 처리한다.
    • (?x) 지시자를 사용했으므로 가독성을 위해 정규표현식에 공백들을 임의로 집어넣을 수 있다.
    • (?i)는 부합시 대소문자를 구분하지 않게 하는 스위치이다(AM/PM과 am/pm을 위한 것이다)
    • Groups 컬렉션을 통해서 각 부분 부합에 접근할 수 있다.
    • 물론 이 정규 표현식이 주어진 날짜/시간의 유효성까지 점검하지는 않는다.
string r = @"(?x)(?i)(\d{1,4})[./-](\d{1,2})[./-](\d{1,4})[\sT](\d+):(\d+):(\d+)\s?(A\.?M\.?|P\.?M\.?)?";

string text = "01/02/2008 5:20:50 PM";

foreach (Group g in Regex.Match(text, r).Groups)
  Console.WriteLine(g.Value + " ");

// 출력
// 01/02/2008 5:20:50 PM 01 02 2008 5 20 50 PM

로마 숫자 부합

string r = 
  @"(?i)\bm*" +
  @"(d?c{0,3}|c[dm])" + 
  @"(l?x{0,3}|x[lc])" +
  @"(v?i{0,3}|i[vx])" + 
  @"\b";

Console.WriteLine(Regex.IsMatch("MCMLXXXIV", r));  // True

중복 단어 제거

string r = @"(?'dupe'\w+)\W\k'dupe'";

string text = "In the the beginning...";
Console.WriteLine(Regex.Replace(text, r, "${dupe}"));  // In the beginning...

단어 개수

string r = @"\b(\w|[-'])+\b";

string text = "It's all mumbo-jumbo to me";
Console.WriteLine(Regex.Matches(text, r).Count);  // 5

GUID 부합

string r =
  @"(?i)\b" +
  @"[0-9a-fA-F]{8}\-" + 
  @"[0-9a-fA-F]{4}\-" +  
  @"[0-9a-fA-F]{4}\-" + 
  @"[0-9a-fA-F]{4}\-" +
  @"[0-9a-fA-F]{12}\-" + 
  @"\b";

XML/HTML 요소 파싱

  • 정규식은 HTML 코드를 파싱할 때 특히 형태가 불완전할 수도 있는 문서를 파싱할 때 유용하다.
string r =
  @"<(?'tag'\w+?).*>" +  // 시작 태그와 부합; 요소 이름에 'tag'라는 이름을 부여,
  @"(?'text'.*?)" +  // 텍스트 내용과 부합; 'text'라는 이름을 부여
  @"</\k'tag'>";  // 'tag'에 해당하는 종료 태그와 부합

string text = "<h1>hello</h1>";

Match m = Regex.Match(text, r);

Console.WriteLine(m.Groups["tag"]);  // h1
Console.WriteLine(m.Groups["text"]);  // hello

낙타 등 방식 단어 분할

string r =@"(?=[A-Z])";

foreach (string s in Regex.Split("oneTwoThree", r))
  Console.Write(s + " ");  // one Two Three

유효한 파일 이름 얻기

string input = "My \"good\"<recipes>.txt";

char[] invalidChars = System.IO.Path.GetInvalidPathChars();
string invalidString = Regex.Escape(new string(invalidChars));

string valid = Regex.Replace(input, "[" + invalidString + "]", "");
Console.WriteLine(valid);  // My good Recipes.txt

HTML에 사용할 유니코드 문자 탈출

string htmlFragment = "ⓒ 2007";

string result = Regex.Replace(htmlFragment, @"[\u0080-\uFFFF]", m => @"&#" + ((int)m.Value[0]).ToString() + ";");

Console.WriteLine(result);  // &#169; 2007

HTTP 질의 문자열 안의 탈출 문자열 복원

string sample = "C%23 rocks";

string result = Regex.Replace(
  sample,
  @"%[0-9a-f][0-9a-f]",
  m => ((char)Convert.ToByte(m.Value.SubString(1), 16)).ToString(),
  RegexOptions.IgnoreCase
);

Console.WriteLine(result);  // C# rocks

웹 접속 로그에서 Google 검색어 추출

  • 이 정규 표현식은 앞에 나온 질의 문자열의 탈출 문자 복원용 정규식과 함꼐 사용해야 할 것이다.
string sample = "http://google.com/search?hl=en&q=greedy+quantifiers+regex&btnG=Search";

Match m = Regex.Match(sample, @"(<?=google\..+search\?.*q=).+?(?=(&|$))");

string[] keywords = m.Value.Split(new[] { '+' }, StringSplitOptions.RemoveEmptyEntries);

foreach (string keyword in keywords)
  Console.Write(keyword + " ");  // greedy quantifiers regex

정규 표현식 언어 일람

  • 아래 표는 .NET 구현이 지원하는 정규 표현식 문법과 구문을 요약한 것이다.

문자 탈출

탈출 문자열 의미 해당 16진 문자 리터럴
\a \u0007
\b 백스페이스 \u0008
\t \u0009
\r 캐리지 리턴 \u000A
\v 수직 탭 \u000B
\f 폼 피드 \u000C
\n 새 줄 \u000D
\e ESC \u001B
\nnn 8진수 ASCII 부호 nnn에 해당하는 문자 (예: \n052는 *)
\xnn 16진수 ASCII 부호 nn에 해당하는 문자 (예: \x3F는 ?)
\cl ASCII 제어문자 l (예: \cG는 Ctrl-G)
\unnnn 16진수 유니코드 부호 nnnn에 해당하는 문자 (예: \u263A는 ☺)
\기호 기호 자체(위에 나온 탈출 문자들에 해당하지 않는 기호일 때

 

  • 특수 사례: 정규 표현식 안에서 \b는 기본적으로 단어 경계를 뜻하지만, [ ] 문자 집합 안의 \b는 백스페이스 문자를 뜻한다.

문자 집합

정규식 의미 역(부정)
[abcdef] 나열된 문자 중 하나와 부합한다. [^abcdef]
[a-f] 주어진 범위에 있는 문자 중 하나와 부합한다. [^a-f]
\d 십진 숫자 하나와 부합한다. [0-9]와 같다. \D
\w 단어(word) 문자 하나와 부합한다(기본적으로 ‘단어 문자’의 의미는 CultureInfo.CurrentCulture의 설정을 따른다.
예컨대 영어에서 단어 문자는 [a-zA-Z_0-9]와 같다)
\W
\s 공백 문자 하나와 부합한다. [\n\r\t\f\v ]와 같다. \S
\p{범주} 지정된 범주의 한 문자와 부합한다. \P
. (기본 모드) \n를 제외한 모든 문자와 부합한다. \n
. (SingleLine 모드) 모든 문자와 부합한다. \n

 

문자 범주

구문 의미
\p{L} 글자(단어 문자)
\p{Lu} 대문자
\p{Ll} 소문자
\p{N} 숫자
\p{P} 문장 부호
\p{M} 분음 부호
\p{S} 기호(symbol)
\p{Z} 분리 문자
\p{C} 제어 문자

 

한정사

한정사 의미
* 0회 이상 부합
+ 1회 이상 부합
? 0회 또는 1회 부합
{n} 정확히 n회 부합
{n,} 적어도 n회 부합
{n,m} n에서 m회 사이 부합

 

  • 임의의 한정사에 접미사 ?를 붙이면 게으른 한정사가 된다.

치환 그룹 표식

표식 의미
$0 부합된 텍스트 전체로 치환된다.
$그룹-번호 해당 번호의 그룹과 부합한 부분 부합 텍스트로 치환된다.
${그룹-이름} 해당 이름의 그룹과 부합한 부분 부합 텍스트로 치환된다.

 

  • 이 표식들은 치환 문자열 안에서만(검색 패턴이 아니라) 유효하다.

너비 0 단언

구문 의미
^ 문자열의 시작(다중 행 모드에서는 한 행의 시작)
$ 문자열의 끝(다중 행 모드에서는 한 행의 끝)
\A 문자열의 시작(다중 행 모드는 무시함)
\z 문자열의 끝(다중 행 모드는 무시함)
\Z 행 또는 문자열의 끝
\G 검색이 시작된 위치
\b 단어 경계
\B 단어 경계가 아닌 위치
(?=정규식) 정규식이 현재 검색 위치의 오른쪽 부분과 부합할 때만 검색을 계속함(양성 전방탐색)
(?!정규식) 정규식이 현재 검색 위치의 오른쪽 부분과 부합하지 않을 때만 검색을 계속함(음성 전방탐색)
(?<=정규식) 정규식이 현재 검색 위치의 왼쪽 부분과 부합할 때만 검색을 계속함(양성 후방탐색)
(?=<!정규식) 정규식이 현재 검색 위치의 왼쪽 부분과 부합하지 않을 때만 검색을 계속함(음성 후방탐색)
(?>정규식) 역추적(backtracking) 없이 정규식과 한 번만 부합

 

그룹 지정

구문 의미
(정규식) 정규식과 부합하는 부분 문자열을 현재 그룹에 갈무리한다.
(?번호) 정규식과 부합하는 부분 문자열을 지정된 번호의 그룹에 갈무리한다.
(?’이름’) 정규식과 부합하는 부분 문자열을 지정된 이름의 그룹에 갈무리한다.
(?’이름1-이름2′) 이름2의 정의를 해제하고, 구간과 현재 그룹을 이름1에 저장한다.
이름2가 정의되어 있지 않으면 역추적으로 부합을 적용한다. 이 경우 이름1은 생략할 수 있다.
(?:정규식 그룹만 지정하고 갈무리는 하지 않는다.

 

역 참조

매개변수 구문 의미
\번호 이전에 갈무리한, 해당 번호의 그룹을 참조한다.
\k<이름> 이전에 갈무리한, 해당 이름의 그룹을 참조한다.

 

대안

구문 의미
| 논리합(OR)
(?(정규식)yes|no) 만일 정규식이 부합하면 yes에 해당하는 문자열이, 그렇지 않으면 no에 해당하는 문자열이 갈무리된다(|no는 생략 가능)
(?(이름)yes|no) 만일 지정된 이름의 그룹이 부합하면 yes에 해당하는 문자열이, 그렇지 않으면 no에 해당하는 문자열이 갈무리된다(|no는 생략 가능)

 

기타 구문

구문 의미
(?#주석) 인라인 주석
#주석 줄의 끝까지 주석(IgnorePatternWhitesapce 모드에서만 유효함)

 

정규 표현식 스위치

스위치 의미
(?i) 부합 시 대소문자를 구분하지 않는다(case-insensitive 또는 ‘ignore’ case)
(?m) 다중 행(multiline) 모드. ^와 $가 문자열 전체가 아니라 문자열 안의 임의의 줄의 시작과 끝과 부합한다.
(?n) 이름 또는 번호가 명시적으로 주어진 그룹만 갈무리한다.
(?c) IL로 컴파일한다.
(?s) 단일 행 모드. 이 모드에서는 “.”가 모든 문자(특히, 새 줄 문자 포함)와 부합한다.
(?x) 탈출되지 않은 공백들을 패턴에서 제거한다.
(?r) 오른쪽에서 왼쪽으로 검색한다 .정규 표현식 중간에 이 스위치를 지정할 수는 없다.

 

[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.