정규표현식

About 정규표현식

직장인B 2021. 4. 12. 00:22

 코딩 테스트를 보게 되었는데, 정규 표현식을 사용해야하는 문제가 나왔다. 정규 표현식이라는 말은 익숙했지만 그 사용법에 대해서는 지식이 일천하여 문제 앞에서 속수무책으로 무너졌다. 그게 그렇게 슬플 수가 없어서 글을 쓰게 되었다. 맘 잡고 공부하는 겸 내용을 정리해보고자 한다. 

 

정규표현식이란 Regular Expression

 직역하자면, 무언가에 대한 규칙적인 표현이다. 그 무언가는 '문자 Text'이다. 합쳐 말해, 문자 규칙의 표현식.

 이런 의문이 든다. 문자는 그 자체로 '정식적인 표현 규칙' 아닌감? 반박할 수 없는 팩트다. 실은 정규표현식은 문자 그 자체에 대한 표현 규칙이 아닌, 문자의 조합을 추상화하여 규칙화한 메타META표현식이다. 문자는 현실에 대한 1차 메타 표현이므로, 정규표현식은 말하자면 현실을 두 번 추상화한 2차 메타 표현이다. 무튼 그러므로 정규 표현식의 더 정확한 정의는 다음과 같다. 

 

"문자 조합의 추상화된 규칙에 따라 문자를 표현하는 정식"

 * 정규 표현식을 고안한 사람은 스티븐 클레이니라는 외국인 할아버지다. 프리스턴 대학교를 나온 멋쟁이다. 

 

정규표현식의 규칙 Rule of Regular Expression

 정규표현식이 가능하려면, 정규표현식으로 표현하려고 하는 '규칙'이 있어야 한다. 앞서 말했듯이 정규표현식이 표현하고자 하는 규칙은 문자 그 자체의 규칙이 아닌 문자의 '조합 규칙'이다. 문제는 문자의 조합 규칙이라는 걸 어떻게 형식화할 것인가이다. a 다음에는 b가 올수도 있고 c가 올수도 있다. 알파벳은 26개의 문자로 구성되어있다. 만약 알파벳으로 5 크기의 문자를 만든다면 가능한 조합은 26의 5제곱인 11881376의 가능수를 지닌다. 여기에 숫자나 특수기호가 포함되면 상황은 더욱 가관일테다. 

 정규표현식을 이미 아는 입장에서 보면 문제는 단순한 해결책을 지닌다. 문자의 조합을 '추상'한 형식을 구성한 것이다. 나의 피셜이지만 바로 이러한 이유로 정규표현식의 표현 문자는 문자들의 'SET'이라고 불리지 않고 문자들의 'PATTERN'이라고 명명된다. PATTERN은 문자들의 조합을 추상한 또 다른 문자다. 

 가령 BAAAA 라는 문자 조합과 ABBBB 라는 문자 조합이 있다고 하자. 일차적으로 둘은 서로 다른 조합 규칙을 지닌다. 앞의 문자는 B 문자 1개와 그 뒤의 A 문자 4개의 규칙을 지니고, 뒤의 문자는 A 문자 1개와 그 뒤의 B문자 4개의 규칙을 지닌다. 둘의 규칙은 특정한 방식으로 '추상'되어 합쳐질 수 있다. 둘 모두, 특정 문자가 특정 횟수로 반복되고 이 특정 문자의 반복 자체가 다시 반복된다. 이걸 정식화하여 문자 조합을 BAAAA -> B1A4, ABBBB -> A1B4 로 재표현해볼 수 있다. 재표현은 기존의 문자가 아닌, '추상된 규칙'을 따르는 새로운 문자 체계에 의해 구성된다. 이러한 새로운 문자 체계, 재표현 정식을 따라 CCBBAA를 C2B2A2로, QOPP 를 Q1O1P2로 재표현해볼 수 있다. 

 정규표현식의 원리는 이와 완전히 동일하다. 문자의 여러 조합들이 지닌 여러 규칙들을 추상화하고 이 추상화된 규칙을 따르는 새로운 문자 체계를 구성한다. 그리고 이 문자 체계로 기존의 표현을 재표현한다. 여기서의 새로운 문자 체계로 재구성된 재표현을 PATTREN이라고 한다. 

 

정규표현식의 추상 Abstraction of Regular Expression

 이어질 질문은 이러하다. 그렇다면, 정규표현식은 어떠한 '추상화'에 기반하는가. 실로 추상화에도 수많은 방법들이 있으니 말이다. 

 여기서 하나 주목할 점은 정규표현식이 문자에 대한 일대일 대응이 아니란 점이다. 그러니까 다양한 문자 조합들이 하나의 정규표현식을 지닐 수 있고 반대로 하나의 정규표현식은 수많은 문자 조합들을 일원하하여 표현한다. 

 이런 식이다.  AB1ab 라는 A 옆에 B가 있고 B 옆에 1이 있고 1 옆에 a가 있고 a 옆에 b가 있는 규칙을 지닌다. 정규표현식은 이 규칙을 아래처럼 추상화한다. 

 

 AB1ab : 대문자 2개가 있고, 그 옆에 숫자 1개가 있고, 그 옆에 소문자 2개가 있다. 

 

 아래의 문자들의 규칙을 같은 방식으로 추상화할 수 있다. 

 

 abcde : 소문자 5개가 있다. 

 11111a : 숫자 4개가 있고, 그 옆에 소문자 1개가 있다. 

 @aaa@ : 특수 문자 1개가 있고, 그 옆에 소문자 3개가 있고, 그 옆에 특수 문자 1개가 있다. 

 

 소문자가 5개 나열된 문자는 abcde 뿐만 아니라 abccc도 있고 bbccc도 있다. 그러므로 정규표현식은 문자에 대한 일대일 조합이 아니다. 자신 있게 말할 수는 없는 부분이지만, 정규표현식이 만들어진 이유 중 하나가 다양한 문자 조합을 단일한 규칙의 명명으로 묶어내기 위함이기도 하지 않을까 싶다. 

 위에서 문자 옆에 쓴 추상화된 규칙에 대한 기술은 그 자체로 문자에 대한 특정 방식의 '재표현'이다. 하지만 당연히 이처럼 줄글로 문자를 재표현하는 건 컴퓨터 공학적으로 아무 의미 없는 일이다. 그러므로 이렇게 줄글이 아니지만 비슷한 의미를 내포한 또다른 문자 체계를 만든 것이다.  통상 요 새로운 문자 체계 자체를 정규표현식이라고 부른다. 아래는 앞의 추상화된 규칙의 기술을 정규표현식으로 바꾸어본 것이다. 

 

 abcde : [a-z]{5}

 11111a : [0-1]{4}[a-z]

 @aaa@ : \W[a-z]{3}\W

 

 실로 난해하다. 나를 슬프게 한 난해함이다. 무튼, 이렇게 문자의 추상화된 규칙을 재표현한 식을 PATTREN이라고 한다. 자연히 다음 섹션은 PATTERN의 조합 규칙에 대한 설명이다. 

 

정규표현식의 패턴 PATTERN of Regular Expression

 

 정규표현식의 PATTERN(이하 패턴)을 깊게 파자면 한도 끝도 없었다. 그리하여 실무적으로 운용할 수 있는 정도만 파악을 했고, 아래의 내용도 그 정도 수준의 내용만을 다룰 것이다. 

 패턴을 이해하기 위해선 패턴을 구성하는 문자 체계를 먼저 숙지해야한다. 나만의 기준으로 나누자면 패턴 문자의 체계는 '문자 요소 지정 체계'와 '문자 나열 규칙 설정 체계'의 두 가지로 구성된다. 

 추가로, 기존 문자 조합(Raw Text)과 패턴의 호환 관계를 일치 MATCH 라고 부른다. 

 

1 . 문자 요소 지청 체계 

 - 문자 요소 지정 체계는 다시 개별 지정 체계와 집합 지정 체계로 나뉜다. 덧붙여 부정을 이용한 지정 표현이 있다. 

 

1-1) 개별 지정 체계 

 

 특정 문자를 일치시키는 기호이다. 소괄호를 사용한다. 

 a -> (a) 

 abc -> (a)(b)(c) -> (abc) // 연속된 경우  하나로 묶을 수 있다. 

 언뜻 보면 이와 같은 표현은 아무 의미 없다. 굳이 소괄호를 씌울 필요 없이 PATTERN 내에서 문자를 직접 입력하면 되기 때문이다. 소괄호의 의미가 생기는 건 다음과 같은 경우다. 

 수직선 | 은 문자의 or 을 표현한다. 가령 black | white 의 표현식은 black과 white 모두에 일치한다.

 

 black -> black | white

 white -> black | white

 

 문자 전체를 or 로 묶는 것이 아닌 문자의 부분을 or 로 묶는 경우 소괄호의 의미가 생긴다. Power 와 Tower는 첫 문자에만 or 의 조건을 줌으로써 둘을 묶을 수 있다.  이 경우는 다음처럼 표현된다. 

 

 Power -> (P|T)ower

 Tower -> (P|T)ower

 

 Blue 와 Grue 의 경우엔 다음처럼 묶을 수 있다. 

 

 Blue -> (Bl | Gr)ue

 Grue -> (Bl | Gr)ue

 

 공백, 탭, 줄바꿈 등의 기호를 조금 더 명시적으로 사용하고자 할 때도 소괄호가 유용하다. 

 ( ), (\t), (\n)

 

 실은 이보다 더 중요한 소괄호의 사용처가 있다. 이는 뒤이어 설명될 반복 횟수 지정 체계 묶어서 설명하도록 하겠다. 

 

1-2) 집합 지정 체계

 

 문자와 특정한 문자 집합을 일치시키는 기호다. 대괄호를 사용하는 방식이다. 

 a -> [a-z]

 Ag -> [A-Z][a-z]

 b9 -> [a-z][0-9]

 [a-z]는 모든 소문자와 일치되고, [A-Z]는 모든 대문자와 일치되고, [0-9]는 모든 숫자와 일치된다. 대괄호 내부의 표현은 [From-To] 의 규칙을 따르기에, 이를 [a-c] 나 [3-8] 처럼 바꾸어 응용할 수 있다. 각각의 표현식은 서로 합칠 수 있다. 가령 소문자와 대문자 모두를 일치시키고자 한다면 [A-Za-z] 로 합칠 수 있고, [A-Za-z0-9] 는 모든 알파벳, 숫자와 일치된다. 찾아보니 [A-Za-z0-9_] 라는 기호도 있던데 이건 알파벳 + 숫자에 '_' underbar 를 더한 집합이다. 변수를 선언할 때 '_'를 사용할 일이 많아서 이런 집합을 따로 만든 것 같다. 

 특수문자의 경우엔 '[][!"#$%&'()*+,./:;<=>?@\^_`{|}~-]' 요런 주먹구구식의 코드를 넣어주어야 한다. 앞의 []는 뒤의 집합의 요소 중 '아무거나' 지정하라는 표현이다. 즉, 이 표현은 뒷 괄호 안의 포함된 모든 특수 문자와 일치한다. 

 대괄호를 사용한 표현식은 상당히 직관적이다. 이것만 써도 정규표현식의 이점을 충분히 누릴 수 있겠는데, 실무를 위해 추가적으로 알아야할 것이 있다. 대괄호 표현과 대응되는 Perl, POSIX 기호이다. 대괄호 기호는 엄밀하게는 ASCII 기호다. 내 기억엔 ASCII 외의 기호를 이용한 소스가 종종 있었다. 무튼, 더 알아놓아서 나쁠 건 없겠다.  대응 관계는 이러하다. 

 

ASCII perl POSIX
[a-z] \l [:lower:]
[A-Z] \u [:upper:]
[A-Za-z] \a [:alpha:]
[0-9] \d [:digit:]
[A-Za-z0-9]   [:alnum:]
[A-Za-z0-9_] \w  
[][!"#$%&'()*+,./:;<=>?@\^_`{|}~-]   [:punct:]
[ \t\r\n\v\f] \s [:space:]

언뜻 장황해보이는 이 표현식은 모두 한 개의 특정 문자와 대응된다는 것을 주의하자. 

 

추가로 '.' Point 의 경우는 모든 문자와 일치한다.  

 

1-3) 부정 지정('^')

 

 부정을 통한 지정을 한다는 뜻이다. 엄밀하게 이는 집합 지정 체계의 한 부분이다. 말이 어렵지 내용은 단순하다. 그것만 아니면 되~ 라는 식. 가령 앞서 [a-z] 라는 집합 표현식이 있었다. 간혹 소문자가 아닌 다른 모든 문자와 일치시키고 싶은 경우가 있다. 그럴 땐 소문자가 아닌 다른 모든 문자를 대괄호 안에 합쳐서 표현할 필요 없이 '[^a-z]' 패턴을 사용하면 된다. 이는 소문자가 아닌 모든 문자와 일치된다. 

 물론 [^A-Z] , [^0-9] 같은 식도 유효하다. 

 

 한쪽 눈알 웃음 '^' 은 뒤이어 다른 규칙으로 또 소개되는데 해당 규칙과 부정 지정 규칙을 혼동해서는 안된다. 부정 지정은 언제나 대괄호와 쌍으로 운용되고, 꼭 대괄호 내부에 포함되어야 한다. 

 

 한편 이해할 수는 없지만 perl 기호의 경우 문자를 대문자로 표현하여 부정 지정을 행한다. 다음처럼 말이다. 

ASCII perl
[^0-9] \D
[^A-Za-z0-9_] \W
[^ \t\r\n\v\f] \S

 

 2. 문자 나열 규칙 설정 체계

 

- 문자 나열 규칙 설정 체계는 '반복 횟수 지정 체계'와 '패턴 위치 지정 체계'로 나뉜다. 

 

2-1) 반복 횟수 지정 체계 

 

 정규표현식을 어렵게 만드는 주범이 바로 이것이다. 복합적인 패턴인 경우 반복 지정이 엄청난 헷갈림을 유발한다. 하지만 각각의 단일한 표현 규칙은 퍽 단순하다. 반복 지정 기호는 언제나 요소 지정 기호의 뒤에 붙는다. <요소 지정><반복 지정> 이 기본 쌍이며, 이 표현은 앞의 요소에 정해진 반복 규칙을 적용하라는 뜻이다. 우선 아래의 개노답 삼형제를 알아야 한다. 

 

 ? : 0번 또는 1번 반복

 * : 0번 이상

 + : 1번 이상

 

 만약 bc 라는 문자가 있다고 하자. 이 bc 앞에 a 라는 문자가 없는 지 있는 지 있으면 몇 번 있는 지 모르는 경우에 다음의 패턴을 구성할 수 있다. 

 

 a?bc <- bc, abc

 a*bc <- bc, abc, aabc, aaabc, ...

 a+bc <- abc, aabc, aaabc, ...

 

 물론 앞서의 요소 지정 체계와 복합적으로 사용될 수 있다. 앞서의 경우에서 조금 변해, a 가 아닌 어느 숫자의 유무를 일치시키려는 경우 다음의 패턴을 구성할 수 있다. 통상 이 경우엔 ASCII 기호보다는 Perl 기호가 주로 사용된다. 

 

 \d?bc <- bc, 0bc, 1bc, ..., 9bc

 \d*bc <- bc, 0bc, 1bc,  ... 438bc, ....

 \d+bc <- 0bc, 1bc,  ... 438bc, ....

 

 개노답 삼형제는 함께 모일수록 더 강한 빌런이 된다. 그러므로 불필요한 다양한 반복 지정 기호의 사용은 지양할 필요가 있다. 

 이보다는 더욱 깔끔하게 반복의 횟수를 명시적으로 지정하는 기호가 있다. 여기에선 중괄호가 사용된다. 가령 a 를 4번 반복하는 경우엔 다음의 패턴을 구성할 수 있다. 

 

 aaaa -> a{4}

 

 새삼 말할 것 없겠지만, 하나의 문자는 다양한 패턴과 일치될 수 있다. 위에서 언급된 예제를 다시 불러와보자. 1111a 라는 문자가 있고 이것과 일치되는 여러 패턴들이 있다. 

 

1111a -> 1{4}a{1}

1111a -> [0-1]{4}[a-z]{1}

1111a -> [a-z0-9]{5} 

 

 세 개의 표현들은 1111a 기준으로 보았을 때는 모두 1111a와 일치한다는 공통점을 지니지만, 실제로 서로 전혀 다른 규칙 범위를 지닌다. 그러므로 상황에 맞춘 적절한 PATTERN을 구현하는 것이 중요한데, 내 생각에 바로 이 점이 정규표현식을 얼마나 숙달하였는 지를 나누는 기준이 되겠다. 

 반복 회수의 범위를 지정할 수도 있다. {min, max} 로 표현된다. 문자열 반복 횟수의 최솟값과 최댓값을 지정한다. 

 

 a{1,3} <- a, aa, aaa

 

 max 값을 명시하지 않은 경우, 최솟값만 적용되어 무한한 반복 횟수와 일치시킬 수 있다. 

 

 a{3, } <- aaa, aaaa, aaaaa, ...

 

 단 min값을 지우는 건 불가능하다는 점을 주의해야한다. 컴파일 에러가 발생한다. 

 한편 앞서 언급한 소괄호의 유용은 바로 여기서 설명될 수 있다. 만약 단일 문자의 반복이 아니라, 다수의 문자 조합의 반복을 패턴으로 만들고자 한다면 반복이 적용되는 조합을 소괄호로 묶어주어야 한다. 

 

a(bc){2} <- abcbc

([a-b][0-1]){2} <- a0a0, a0a1, a0,b1, ...

 

 요 소괄호와 반복 횟수 지정 기호의 조합도 만만찮은 빌런이다. <요소 지정><반복 지정> 의 쌍을 꾸준히 의식하고 있어야겠다. 

 

2-2) 패턴 위치 지정 체계

 

 이 부분은 고맙게도 아주 단순하다. 두 가지만 기억하면 된다. 한쪽 눈 웃음 '^' 과 달라 $ 가 그것들이다. 두 기호는 패턴의 앞과 뒤에 붙어, 해당 패턴이 문자열의 맨 앞에 있는 지, 맨 뒤에 있는 지를 일치시켜준다. 일종의 돛같은 것.

 

 ^ab <- ab, abc, abab, ...

 ab& <- ab, aab, cab, ...

 

 한 쪽 눈웃음은 반드시 패턴의 앞에 위치해야하고, 달라는 뒤에 위치해야 한다. 

 

정규표현식의 유용 Utility of Regular Expression 

 정규표현식의 의미와 사용법을 알아보았다. 남은 건, 실질적으로 이를 어디에 쓸지, 어느 분야, 어느 업무에서 이것이 유용한 지를 고민해보는 일이다. 이에 대해 콕 집어 말할 내용이 없다. 사용처가 애매해서가 아니라, 반대로 어느 곳에서든 사용될 수 있기 때문이다. 텍스트 처리 분야에서는 물론이고, 데이터 검색, 사용자 정보 입력 등등의 넓고 작은 분야에서 두루 사용될 수 있다. 

 이 중요한 걸 미리 꼼꼼히 공부 안해놓고 코딩 테스트 후에야 살펴보았다는 사실에 좌절감이 든다. 

 

 어쨌든, 이제 잘 사용하면 되는 일이다. 정규표현식은 컴퓨터 공학의 산물이다. 그러므로 정규표현식을 사용한다는 건 정규표현식을 운용하는 소스를 작성하고 활용한다는 말에 다름이 아니다. 그러므로 정규표현식의 개념을 파악한 건 실은 그 출발점에 선 것과 마찬가지이다. 정규표현식을 소스화하는 일이 본격적인 작업이다. 

 다음 장에서 관련 내용을 다루어보고자 한다. java, python, javascipt에서 정규표현식이 어떻게 지원되고 있는 지를 살펴보고 참고 코드를 작성해보겠다.