부트캠프

정규표현식

noyyo 2023. 7. 24. 20:57

문자열을 처리할 때 string 클래스도 좋지만 특정한 패턴을 추출하고 싶을 때가 있다.

이럴 때는 정규표현식을 사용하면 좋다.

정규표현식은 특정한 형식의 문자열의 집합을 표현하는 형식 언어다. 쉽게 말하면 문자열의 패턴을 표현하는 방법이다.

 

 

. ^ $ * + ? {} [] \ | ()

다음과 같은 메타문자를 사용해서 여러 패턴을 표현한다.

. : 와일드카드다. 줄바꿈 문자 \n을 제외한 모든 문자를 나타낼 수 있다.

^ : 문자열의 시작을 뜻한다. ^s 는 시작이 s인 문자열이다. []나 () 안에서 사용된다면 반대의 의미인 NOT이라고 생각하면 된다. 그 문자가 아님을 나타낸다.

$ : 문자열의 끝을 뜻한다. e$는 e로 끝나는 문자열이다.

시작과 끝은 주어진 전체 문자열의 시작과 끝을 의미한다. "see see" 라는 문자열이 주어질 때 ^s는 첫 번째 see만 매치하고 e$는 두 번째 see만 매치한다.

* : 앞에 있는 문자가 0번 이상 반복된다. 없을 수도 있고 하나일 수도 있고 여러번 반복될 수도 있다. ab*c라면 ac도 abc도 abbc도 매칭된다.

+ : 1번 이상 반복된다. *에서 0번만 뺐다.

? : 없거나 있거나. *에서 2회 이상을 제외한 것과 같다.

{} : 반복 횟수 지정이다. {1}은 1번 반복. {3}은 3번 {1, 3}은 1번 이상 3번 이하. {1,} 은 1번 이상 {,3}은 3번 이하

[] : 문자클래스다. 하나의 문자인데 매칭할 글자가 여러개일 때 쓴다. [abc]는 abc를 매치하는게 아니라 a나 b나 c를 매칭한다. "a"도 "b"도 "c" 도 다 매칭된다. [0-9] 는 0부터 9까지를 매칭해본다는 뜻이다. [^0-9]는 0부터 9까지를 제외하고 매칭한다는 뜻이다. [가-힣] 이건 한글을 매칭한다는 뜻이다.

\ : 이스케이프 문자로 특수문자나 메타문자 등을 표현할 때 쓰이기도 하고 형식을 지정할 때도 쓴다.

\d : 숫자를 매치한다. [0-9]랑 같다.

\D : 숫자 아닌걸 매치한다[^0-9]랑 같다.

\s : 모든 공백문자와 매치한다.

\S : \s 반대다.

\w : 알파벳과 숫자 그리고 언더바(_) 까지 매치한다. [a-zA-Z0-9_]랑 같다.

\W : \w 반대다.

\b : 경계 지정이다. \w와 \W 사이의 경계를 뜻한다. "abc def" 가 있다면 c랑 공백 사이 또는 공백과 d 사이가 \b가 의미하는 경계의 지점이다.

\bdef\b 표현식이라면 "abc def"에서 "def" 를 매칭한다. \bde\b 표현식이라면 "abc def"에서 매칭되지 않는다.

\B : \b의 반대로 \w와 \w 사이 또는 \W와 \W의 사이의 경계를 뜻한다. "abc def" 에서 a와 b 사이, b와 c 사이, d와 e 사이, e와 f 사이가 되겠다.

 

() : 그룹 지정이다. 여기서부터 조금 복잡해진다. 그룹화를 하면 하나의 글자처럼 묶어서 취급한다고 생각하면 된다.

그리고 자동으로 매칭된 값이 캡처된다.

예시로 알아보자.

(abc)는 "a"는 매칭이 안된다 "abc"는 매칭된다.

(abc)+는 "abcbc"는 매칭되지 않는다. "abcabc"는 매칭이 된다.

 

캡처는 매칭이 됐을 때의 해당 값을 가지고 있다고 생각하면 된다. 그룹에 이름을 매겨줄 수도 있고 안매겨도 자동으로 1번부터 번호로 매겨서 참조할 수 있다.

(abc)(def) 이 경우 (abc) 그룹은 1 그룹이 되고 (def)는 2그룹이 된다.

(abc)가 매칭이 됐을 때 그 값을 캡처하고 \1을 통해서 참조할 수 있다. (def)는 \2가 된다.

(a+)\s\1 이 있다고 하면 "aaa aaa"는 매칭된다. "aaa a"는 매칭되지 않는다. (a+)에서 캡처된 값이 aaa기 때문에 \1은 aaa가 되기 때문이다. 표현식을 캡처하는 게 아니라 값을 캡처한다는 점.

 

그룹에 이름을 매기는 것은 

(?<name>)으로 할 수 있다. 참조는 \k<name> 으로 참조한다.

(?<number>[0-9])\s\k<number> 는 "1 1" 을 매치한다.

 

그룹에 순서를 매길 때 주의할 점으로 이름이 명명된 그룹은 뒤쪽으로 밀린다.

(?<name>)\s(a+) 라는 정규식에서 \1을 통해 참조하면 (?<name>) 그룹이 아니라 (a+) 그룹의 캡처를 참조한다. \2가 name 그룹이다.

헷갈리게 되니 안그래도 가독성 안좋은 정규식에서 캡처를 할 거라면 항상 이름을 매겨주자.

 

(?:식) : 해당 그룹은 캡처하지 않는다. 

 

●positive,negative / lookahead,lookbehind assertions

문자열의 앞이나 뒤에 특정한 패턴이 있는지 검사해서 매칭되게 하는 용도다. 매치 결과에 포함되지는 않지만 식을 만족하지 않으면 매칭이 되지 않는다.

(?=식) : 앞에 있는 식이 이 식을 만족해야 매칭된다.

(?!식) : 앞에 있는 식이 이 식을 만족하지 않아야 매칭된다. 

(?<=식) : 뒤에 있는 식이 이 식을 만족해야 매칭된다.

(?<!식) : 뒤에 있는 식이 이 식을 만족하지 않아야 매칭된다.

 

한 번에 알아듣기 쉽지 않다. 예시를 봐야 낫다.

\b\w+\b(?=.+and.+) 해당 정규식이 있을 때 "cats, dogs and some mice." 문자열에서 "cats", "dogs" 는 매칭된다.

차근차근 생각해서 \b를 통해 경계를 찾고 \w+를 통해 반복되는 문자를 찾았다. 뒤에 \b를 통해 경계가 있는지도 확인했다. 그리고 나서 .+and.+를 만족하는지 체크한다. 만족하면 매칭이고 안되면 매칭 안된다.

\b\w+\b를 통해 cats를 찾았다. 해당 지점에서 +and.+도 만족한다. cats는 매칭된다.

마찬가지로 dogs를 찾았다. 해당 지점은 dogs<여기에 멈춰있음> and 이기 때문에 공백이 있어 .+and.+를 만족한다. dogs는 매칭된다.

and를 찾았다. 해당 지점에서 .+and.+를 찾을 수 없다. and는 매칭되지 않는다.

some도 마찬가지.

mice도 마찬가지다.

 

또 하나를 보자

\b\w+(?=\sis\b 해당 정규식이 있을 때 "The dog is a Malamute." 문자열에서 "dog"가 매칭된다.

먼저 \b\w+로 The를 먼저 찾고 뒤에 \sis\b를 만족하는지 봐야한다. 공백은 있지만 is가 없다. 매칭되지 않는다.

dog를 찾았고 뒤에 공백과 is, 경계까지 만족한다. 매칭된다.

is를 찾았지만 뒤에 공백과 is가 없다. 매칭되지 않는다.

a 를 찾았지만 마찬가지로 매칭되지 않는다.

Malamute 도 마찬가지로 매칭되지 않는다.

 

2개의 예시를 들어서 설명했지만 여전히 헷갈릴 수 있다. 매칭을 위한 제한조건이라고 생각하면 훨씬 이해가 쉽다.

나머지 3가지의 assertion도 마찬가지의 원리로 생각하면 된다.

 

이제 C#에서 정규표현식을 사용하기 위해 어떻게 해야할까.

using System.Text.RegularExpressions; // using을 통해 해당 네임스페이스를 사용하자.

C#에서는 Regex 클래스로 정규표현식을 구현할 수 있다.

 

Regex(string) 이나 Regex(string, RegexOptions) 생성자로 정규식을 입력해 인스턴스를 생성한다.

RegexOptions는 9가지가 있는데 Compiled, Multiline, IgnoreCase 정도만 알면 될 것 같다. 컴파일은 MSIL 코드로 바꿔서 초기화 시간을 희생해서 런타임에 성능이 좋아진다고 하고 멀티라인은 줄바꿈이 있어도 매칭하는 옵션 Case무시는 대소문자 구분 없이 하는 옵션이다. 옵션은 비트플래그 형식이다. 동시에 사용하면 | 연산을 해주자.

Regex 객체를 만들었다면 메소드로 IsMatch(string input)나 Match(string input), Matches(string input), Replace, Split 등을 사용하면 된다. IsMatch는 말그대로 매치되는게 있는지 여부를 리턴. Match는 Match 객체를 리턴. Matches는 MatchCollections 객체를 리턴한다.

Match객체에선 Success, Value, Index, Length 등을 통해 여러 값을 가져올 수 있다.

 

사용할 때 주의할 점으로 string input을 인자로 넘겨줄 때 문자열 리터럴을 작성한다면 앞에 @를 꼭 붙여주자!

@"input" 과 같이 말이다. @를 붙이는 이유는 문자열에서 역슬래시를 이스케이프로 쓰지 않기 위함이다. 문자열 내부에 들어가는 역슬래시는 정규식의 표현이지 C#에서 문자열의 표현이 아니기 때문!

 

간단하게 "12ab34"에서 ab를 빼고 "1234"를 만들어보자

Regex.Replace("12ab34", "[^0-9]+", "");

해당 메소드는 정적 메소드라 인스턴스 없이도 바로 사용할 수 있다. 간단하게 input, pattern, replacement 를 넘겨주면 해당 패턴에 매치되는 것들을 바꿔준다.

 

이번엔 Regex 객체를 만들어서 휴대폰 번호를 추출해보자.

Regex rx = new Regex(@"\b01[0-9]-\d{4}-\d{4}\b",
    RegexOptions.Compiled | RegexOptions.IgnoreCase);
Match match = rx.Match("홍길동, 010-1234-5678");
Console.WriteLine(match.Value);
// output : 010-1234-5678

이제 정규표현식을 알고 C#에서 활용할 수 있게 되었다. 대부분의 언어에서 라이브러리를 통해 정규표현식을 지원하니 약간만 다를 뿐 다른 언어에서도 충분히 활용할 수 있을 것이다.

'부트캠프' 카테고리의 다른 글

싱글톤 패턴으로 게임 매니저 만들기  (0) 2023.07.26
Unity TextMeshPro  (0) 2023.07.25
C# ref, out, in, 박싱과 언박싱  (0) 2023.07.19
C# 문자열 보간($)  (0) 2023.07.18
C# Nullable Reference Type  (0) 2023.07.18