0. Introduction

팽당숙이라는 청나라 문인이 집안의 하인에게 말하기를..."하면 어려운 것도 쉬워지지만, 하지 않으면 쉬운 것도 어려워진다."고 했데요. 하인이 아마 '어쩌라고?' 그랬을것 같아요.

'정규 표현식' 자체가 어렵다기 보다는 아마도, 하지 않아서가 아닌가 해요. 사실, 참 친해지기 어려운 녀석이 아닌가 합니다. 단어 자체가 너무 어려워서인지, 무슨 장갑차같은 느낌이랄까... 이름부터 살짝 피곤하기도 해요. 그래서, 원래 이름인 regular expression, 줄여서 regex에 대해 이야기 해 보겠습니다.

1. What is?

regex는 text가 어떻게 구성되었냐에 대한 규칙입니다. 말이 너무 어렵죠. 간단히 말해서, 다음과 같은 글이 있습니다.

Kiyong +82-10-3500-1985
Steve +44-72-3944-1924

이런 글이 있을 때, 이름만 추려내고 싶다고 가정하겠습니다. 이름에는 어떤 규칙이 있을까요? 눈에 보이시는 것처럼, 영문자로 구성되었다고 할 수 있습니다. 반대로, 전화 번호는 숫자로만 이루어졌다고 볼 수 있습니다. 이러한 규칙, 즉 search pattern을 정의하는 것이 바로 regex입니다.

규칙이라는 것은, 지켰다와 어기는 것이 분명하게 나누어집니다. 개발자라면, 다음과 같은 예를 쉽게 이해하실 수 있을 것 같네요.

Console.WriteLine("Hello World!");

가장 유명한 프로그램인 'Hello World'의 C# 버전입니다. 위의 프로그램을 만약에 다음과 같이 수정한다면 어떻게 될까요?

Console.WriteLine "Hello World!"

당연히, 컴파일이 되지 않습니다. method 이름 다음에는 괄호가 올 것을 규칙으로 정해두었기 때문입니다. 컴파일러와 정의된 규칙을 어겼으니, 컴파일러는 코드가 잘못된 것으로 판단합니다.

우리는 규칙을 만들어서, 규칙을 따르는 문자열을 검색하는 것 입니다.
대충, 감이 오셨으니, 좀 더 제대로된 정의를 살펴보겠습니다. 위키피디아에 따르면,

In computing, a regular expression (abbreviated regex or regexp) is a sequence of characters that forms a search pattern, mainly for use in pattern matching with strings, or string matching

regex는 연속된 글자가 어떤 search pattern을 가지는 정의하여,  pattern matching, 즉 searching을 위해 사용합니다. 여기서 우리가 해야할 일은, 목적에 맞는 searching pattern을 작성하는 일입니다. 이미, 훌륭한 선배들에 의해, pattern matching은 이미 구현되어 있습니다.(대부분의 언어는 물론, 개발 환경 및 문서 작성 도구에서도 확인할 수 있습니다.)

regex는 1950년대 만들어진 컨셉입니다. 미국 수학자 Stephen Kleene 확립되었으며, 이후 Unix의 ed, grep에 적용되었습니다. Formal Language Theory 에 따르면, RL(Regular Language)를 충족하는 구조입니다.(컴파일러 수업 시간에 들었는데, 기억이 안나요. ㅠㅠ나중에 언제 정리할께요)

2. Basic Syntax

제일 먼저 배우는 것이, 가장 기본적이지만 가장 많이 쓰인다는 거 아시죠? 이것만 아셔도, 막막했던 Regex를 이해하시게 될 거예요. Regex는 제법 많은 token(atomic parse element)으로 이루어져 있습니다. 잘 안외워 지니까, 간단히 분류해서 보도록 할께요. 크게 4가지로 분류해 봤어요. 언어 별로, 구현된 regex가 조금씩 틀리지만, 아래의 내용은 대부분의 언어에서 지원하거나 보편적인 수준의 내용입니다.

In computing, a regular expression (abbreviated regex or
regexp) is a sequence of characters that forms a search pat
-tern, mainly for use in pattern matching with strings,or
string matching, i.e.

공부할 겸, 앞의 정의를 예제 text로 삼겠습니다. ^^

1) Character Set(글자 집합)

찾으려는 글자를 지정하기 위한 token입니다.

  • . → Any character (아무개 글자)
    구두점(.)을 사용하면 아무 글자를 일컫습니다. 영문자, 숫자 그리고 특수 기호까지를 뜻합니다.(Newlines를 뜻하는, LF CR NL은 대부분 포함되지 않습니다.)
    만약, 'i.'이라는 regex로 검색하면, 결과는 'ln', 'is', 'in'이 나오게 됩니다. 이를 다음과 같이 표현하겠습니다.
    /i./ » 'In', 'is', 'in'
  • [ ] → Contained Character set (포함되는 문자 집합)
    대괄호를 사용하면, 글자(character) 한 자에 대해 허용되는 문자 혹은 숫자를 뜻합니다.
    /i[ns]/ » 'In', 'is', 'in'
  • [^ ] → Not Contained Excluded Character set (제외한 문자 집합)
    대괄호 안의 글자가 없어야 한다는 것을 의미합니다.
    /i[^s]/ » 'In', 'in'

2) Quantification(수량자, 번역이 좀...;)

앞서 선언한 글자가 생략 혹은 여러번 반복되어질 때 사용하는 token입니다.

  • ? → zero or one(없거나, 하나만)
    한 번만 허용되거나 생략 가능한 경우에 사용할 수 있습니다. 즉, 여러 번 나타나면 안되는 경우 입니다.

    /strings?/ » 'string, strings'
  • * → zero or more(없거나, 많거나)
    대게, *(Asterisk)의 경우 생략되거나 혹은 그 길이를 한정짓지 않을 때 사용됩니다.
    /ab[^ ]*/ » 'abbreviated'
  • + → one or more(적어도, 하나 이상)
    반복되어질 수 있지만, 적어도 한번은 나타나야 하는 경우에 사용합니다. 다시 말하면, 반드시 포함되어야 하는 부분에 사용합니다.
    /strings+/ » 'strings'
  • {m, n} → between m and n (주어진 횟 수만큼 반복)
    횟수를 지정할 수도 있습니다. m=최소, n=최대가 됩니다. 생략하는 경우에는, 한정짓지 않는다는 의미입니다.
    /pa.{1,5}/ » 'pat', 'pattern'

3) Location Pointer

앞서 선언한 글자가 특정 위치에서 필요한 경우, 사용되는 token입니다.

  • ^ → the starting position of any lines(각 줄의 첫 글자)
    특정 pattern으로 시작되는 문장을 찾는 경우에 사용합니다.
    /^i./ » 'In'
  • $ → the ending position of the string or before a newline(각 줄의 마지막)
    특정 pattern으로 끝나는 문장을 찾는 경우에 사용됩니다.
    /.*$/ » 'string matching, i.e.'

4) ETC...

  • | → Set Union (합집합)
    word나 pattern의 일부가 OR 관계일 때 사용합니다.
    /main|match/ » 'main', 'match', 'match'
  • ( ) → Capturing groups (그룹 만들기)
    하나의 matching 결과에서, 여러 개의 sub set을 가질 수 있습니다. 이를 사용하기 위한 token입니다.
    /ma(in|tch)/ » 'main', 'in', 'match', 'tch', 'match', 'tch'
    검색 결과 외에도, capture된 문자열도 같이 보여지게 됩니다.
  • n → The reference of capturing groups (그룹 호출하기)
    앞서 capture된 group은, 1, 2와 같은 형태로 참조할 수 있습니다. group간의 위치를 바꾸거나, 복사할 때 사용합니다.
    replace에 사욛되므로, 다음 포스트에서 다룰께요;;

3. How to make a pattern (그래서, 어쩌라고?)

위의 element를 숙지했더라도, 막상 작성하기가 쉽지 않은데요. 저같은 경우에는 다음과 같이 진행합니다.

  1. Start a small part
    줄의 맨 처음에 나오는 문자에 대해 작성하신다면, '^'부터 시작합니다. 정말 작은 조각부터 맞춰가는 것입니다.
  2. Extend a existing part to complete the whole pattern
    거기에 조금씩 진행해 갑니다. '^ab'와 같이 작성해 보는 것입니다. 모든 작성은, TEST 기반이어야 합니다. IDE에서는 지속적으로 Search를 수행보시고, Code상에서는 Test Case를 돌리시거나 REPL을 이용합니다.
  3. Split the answer
    가끔, regex가 길어지면서 panic에 빠지는 상황이 연출되곤 합니다. 이럴 때는, 분리해서 처리하셔야 합니다. 이미 작성된 작동되는 pattern은 한 쪽으로 치워놓고, 나머지 부분에 대한 pattern을 새롭게 작성하여 붙입니다. 네, 맞습니다. Divide and Conquer죠.

4. Exercise

  1. "abcd" ▷ "bc"
  2. "abcdabdef" ▷ "abc", "abd"
  3. "ababacac" ▷ "abab", "acac"
  4. "www.watch.com, ww.net, ne" ▷ "www.watch.com", "ww.net"
  5. "+82-10-3500-1999, 011-052-369, 010-9600-2568, 016-2800-4528"
    ▷ "+82-10-3500-1999", "010-9600-2568"

5. Closing

언어를 익히는 일도, 무언가에 숙련된다는 뜻도, 자주 함에 있습니다.