정규표현식의 개념과 기초 문법

작성한지 10년이 지났는데도 notepad++ 관련한 키워드로 꾸준히 유입이 발생하고 있고,  그게 정규 표현식 관련한 글인데 별로 내용이 좋은 글이 아니다. (몇 가지 기초적인 문법만 요리책 식으로 나열해 놓은 거라…) 그래서 정규 표현식의 개념과 기초 문법 그리고 몇 가지 이 블로그에서 다루는 언어들에서 정규 표현식을 어떻게 쓰는지, 어떤 것들을 할 수 있는지 등에 대해서 알아볼 계획이다.

오늘은 그 첫 번째 순서로, 정규 표현식의 개념과 가장 기초가 되는 패턴 문법에 대해서 살펴보려고 한다.

정규 표현식의 개념

정규표현식은 줄여서 정규식(영어로는 Regular Expression이고 줄여서 Regex, Regexp 등으로 불린다.) 이라고도 하는데, 컴퓨터 과학의 정규언어로부터 유래한 것으로 특정한 규칙을 가진 문자열의 집합을 표현하기 위해 쓰이는 형식언어이다.

일반적인 어휘를 써서 풀어보자면 어떤 텍스트 내에서 특정한 형태나 규칙을 가진 문자열을 찾기 위해 그 형태나 규칙을 나타내는 패턴을 정의하는 것이 정규 표현식이라고 이해하면 된다.

 

정규 표현식의 종류

정규 표현식은 다양한 분야에서 쓰이기 시작했는데 각 분야의 정규식은 서로 영향을 주고 받으면서 발전해서 지금에 이르렀다. 아니 왜 아름다운 역사 이야기를 갑자기 들먹이는 거냐면, 바로 정규식이 이렇게 쓰이는 곳이 다양한데, 하나의 통일된 표준이 아니라는 것이다.

유닉스 명령줄 도구들에서 사용하던 정규 표현식은 POSIX 표준에 편입되었다. 그리고 이후에 다시 POSIX 정규식은 POSIX BRE (POSIX 기본 정규식)와  POSIX ERE (POSIX 확장 정규식)으로 다른 버전이 나뉘게 된다. (grep에서 -e 스위치를 써서 확장 정규식을 쓰던 옵션이 바로 ERE를 쓴다는 의미이다.) 그외에 BRE를 기본 골격으로 한 vim 정규식이 있다. 역시 오랜 역사를 따라 개선과 확장을 거듭하면서 이는 POSIX 표준과는 좀 다른 규격으로 취급될 정도이다. 1:

그리고 문자열을 다루는데 특화된 스크립트 언어인 펄(perl)이 등장했다. 펄의 정규식 체계는 역시나 기본은 POSIX와 비슷한데, 엄청나게 많은 확장이 들어갔다. 이후 펄의 정규식은 PCRE라는 규격으로 정리되었으며, 이후 많은 프로그래밍 언어들이 이 규격을 차용하거나, 계승한다. 여기서 중요한 것은 “일부 차용”이다. 이 규격은 워낙 방대해서 PCRE를 그대로 가져다 쓰지 않는 이상, 구현할게 너무 많기 때문이다.

참고로 Cocoa에서는 ICU 표준의 정규식을 따른다. 이 라이브러리 역시 PCRE를 기반으로 하고 있으며, 유니코드 문자열에 대한 정규식 패턴 매칭을 수행하는 알고리듬이 구현되어 있다.

정규식 기본 문법

정규식 기분 문법은 크게 세 가지 정도로 나눌 수 있다.

  1. 패턴 그대로를 매칭하는 경우 : 편집기에서 “찾기” 기능을 통해서 특정 단어를 찾는 것 처럼, 단어 그대로를 패턴으로 사용하여 매치되는 영역을 찾는다.
  2. 메타문자 및 수량 한정자를 적용하는 경우 : 정규식 패턴에 쓰이는 문자중에는 특별한 의미를 가지는 메타 문자들이 있는데, 이들을 사용하여 보다 폭넓은 패턴에 매치할 수 있다.
  3. 그룹 및 look around 기능을 사용하는 경우 : 제법 고급 정규식이라 할 수 있는 부분으로, 패턴의 일부를 그룹으로 묶거나, 특정 패턴의 앞 뒤로 다른 패턴이 오는 조건을 더하는 경우이다.

정규식 메타 문자

메타 문자는 특정한 문자 혹은 문자 계열을 대신하여 표시하는 문자이다. 메타문자를 이용하면 특정한 규칙을 가진 여러 단어를 하나의 패턴으로 함축할 수 있다.

메타 문자 의미
^ 문자열의 시작. [...] 내에서 쓰이면 “일치하지 않는“의 의미가 된다. ^http는 문자열의 맨 처음에 http가 온 경우에 매치한다. (중간에 http가 나타난 경우는 매치하지 않음)
$ 문자열의 끝 them$은 문자열이 them으로 끝난 경우에 them에매치한다.
\b 단어의 경계. 공백, 탭, 컴마, 대시 등이 올 수 있다. \bplay\b는 play 의 양 끝에 단어 경계가 오는 경우에만 play에 매치한다. 따라서 “playground”의 play에는 매치하지 않는다.
\B \b가 아닌 것. 정규식 메타문자에서는 대소문자가 바뀌면 반대의 의미를 지니는 것들이 있다. \bplay\B는 play뒤에 단어 경계가 아닌 것이 왔을 때 play에 매치한다. 따라서 play에는 매치하지 않지만 playground, playball의 play에는 매치한다.
\s 공백문자 공백, 탭에 매치한다.
\S 공백문자가 아닌것 공백 문자가 아닌 모든 문자에 매치한다.
\d 숫자. [0-9]와 같다.
\D 숫자가 아닌 것. [^0-9]와 같다.
\w word. 알파벳로마자에 매치한다.
\W not word. 알파벳로마자가 아닌 것에 매치한다.
\n, \r 개행 문자 및 캐리지 리턴에 매치한다.
\ 이스케이프 문자. 여러 메타 문자들을 이스케이프하여 그대로 사용할 수 있게 한다. *, ?, +, [, { ,(, ), }, ], ^, $, |, \, .
. 임의의 문자 하나에 대응한다.

선택 패턴

| 문자를 이용하면 A | B 의 패턴으로 A 혹은 B에 매칭할 수 있다. 예를 들어 tomato와 potato에 모두 매칭하고 싶다면 tomato|potato 라고 쓸 수 있다.  선택 패턴은 이후에 등장하는 그룹 패턴과 관련하여 보다 강력하게 쓰일 수 있다.

그외의 선택패턴으로는 [ ... ]이 있다. 대괄호속에 넣은 문자 중에서 하나에 매칭하는 것이다. [cfh]all 이라는 패턴은 call, fall, hall에 모두 매치될 수 있다. 특히 선택 패턴은 A-B를 통해서 특정 범위를 표현할 수도 있는데, 숫자의 경우 [0-9],  알파벳 소문자의 경우 [a-z], 알파벳대문자의 경우 [A-Z] 와 같은 식으로 한 글자에 매칭하는 것이 가능하다. 유니코드를 지원하는 정규식에서는 [ㄱ-힣]을 이용해서 한글 한 글자에 매칭하는 것도 가능하다.

또한 선택 패턴 내에서 ^ 이 쓰이면 not 의 의미가 되며, 이 문자 뒤에 오는 문자들은 제외하게 된다.

그룹

괄호는 그룹을 나타낸다. 그룹은 전체 패턴 내에서 다시 하나로 묶여지는 패턴 조각을 나타낸다. 특히 | 나 뒤에 나오는 수량 한정자를 그룹에 붙이는 형태로 많이 사용되며, 한 번 매치한 그룹이 다시 반복되어 나타나는 경우에도 사용할 수 있다.

  • (tom|pot)ato : tomato, potato에 모두 매치되는 패턴을 그룹을 써서 좀 더 줄였다.
  • (a|i){3}bc : a 혹은 i가 3개 온 후에 bc가 오는 패턴. aaabc, iiibc, aiabc, aaibc, iiabc 등에 매치된다.

괄호를 써서 묶은 부분은 1번부터 시작하는 그룹으로 참조할 수 있다. 앞서 매치한 그룹을 패턴 내에서 재사용하려면 \1과 같이 그룹번호를 역슬래시로 이스케이프하여 표현한다. tomato에서 to가 두 번 반복되는데 이는 다음과 같이 표현할 수 있다.

(to)ma\1
----
(to)       # to 에 매치하는 첫번째 그룹을 캡쳐한다.
    ma     # ma에 매치
      \1   # 1번 그룹인 to가 다시 나온다.

이를 좀 더 응용하면 아래와 같은 패턴도 만들 수 있다.

(a|b|c){2}ma\1

이 패턴은 a 혹은 b 혹은 c 중에서 매치되는 두 글자를 그룹으로 캡쳐하고 ma  뒤에 동일한 글자가 반복되는 패턴이다. 따라서 aamaaa, bcmabc, abmaab 등에 매치된다. 캡쳐된 그룹을 재사용하는 패턴은 그룹의 패턴이 아닌 캡쳐된 내용에 매치하므로 aamabb에는 매치되지 않는다.

비캡쳐링 그룹

(?: ) 을 사용하면 그룹으로 묶어는 주지만 캡쳐는 하지 않는 비 캡쳐링 그룹이 된다. 이는 특정한 수량 한정자등을 적용은 하려 하지만 최종 결과에서 따로 구분하여 사용할 필요가 없는 경우에 적용한다. (사실 캡쳐만 해놓고 사용하지 않아도 무방하다.)

수량 한정자

동일한 글자 혹은 동일한 족(family)이 n 개 만큼 나오는 경우에 수량한정자를 뒤에 붙일 수 있다.

표현 의미
? 앞에 온 표현이 없을 수 있다는 의미. apples?에서 s?는 s가 있거나 없을 수 있다는 의미이다. 따라서 이 패턴은 apple, apples에 모두 매칭된다.
* 앞의 표현이 0개 이상이다. n\d*는 숫자가 0개 이상이라는 의미로 n, n0, n12, n42435453 등에 매치될 수 있다.
+ 앞의 표현이 1개 이상이다. n\d+는 숫자가 1개 이상 따라온다는 의미. n은 매치하지 않으며 n0, n12, n3435345 등이 매치된다.
{n} 앞의 표현이 n개이다. n\d{3}은 n 뒤에 숫자가 3개 온다는 의미로 n, n1, n35245 는 매치하지 않으며 n123은 매치한다.
{n,} 앞의 표현이 n개 이상이다.
{n,m} 앞의 표현이 n개 이상, m개 이하이다. 수량이 n~m 사이에 있는 조건

수량 한정자와 관련하여 *, + 는 기본적으로 greedy 하게 동작한다. 즉 가능한한 많은 글자를 먹고 다음 패턴을 찾는다는 것이다. 예를 들어

i like apples and bananas

라는 문장에 대해서 ^.*s를 매치하면 . 문자(아무 글자)는 욕심을 부려서 다 먹어치우기 때문에 bananas의 s까지, 전체 문장이 다 매치된다.

이 때, *? 를 조합하는 경우에는 반대로 동작한다. 즉 ^.*?s로 패턴을 주면 i like apples 까지만 매치한다.

예제

몇 가지 예제를 통해 정규식의 동작에 대해 익혀보자. 해당 예제들은 hackker rank의 정규식 세션의 기본 문제에서 가져왔다.

개행이 아닌 문자에 매치하기

원글 주소 : https://www.hackerrank.com/challenges/matching-anything-but-new-line

abc.def.ghi.jkx의 형태에 매치하는 패턴을 찾는다. 각 변수 a,b,c…,x 는 개행문자가 아닌 한 글자의 문자에 해당한다.

이 문제는 다음과 같이 해석할 수 있다.

  1. 개행 문자가 아닌 글자 3개가 있고
  2. “.” 문자에 이어서 다시 개행 문자가 아닌 글자 3개가 온다.
  3. 그리고 2의 패턴은 3회 반복된다.

따라서 이를 나타내는 정규식 패턴은 다음과 같이 쓸 수 있다.

/[^\n]               # 개행문자가 아닌 글자
      {3}            # 가 3개 있고
      (\.            # 그 뒤에 . 이 온 후
         [^\n]       # . 다음에 다시 개행이 아닌 문자가
              {3}    # 3개 온다.
                 ){3}# (.[^\n]{3}) 패턴 자체가 다시 3개가 온다.

핸드폰 번호 매치하기

핸드폰 번호는 010-1234-5677 와 같은 식으로 쓰는데 구분자는 없을 수도 있고, 공백일 수도 있다.  따라서 다음과 같은 표현 중 어느 것이어도 유효한 핸드폰 번호라 할 수 있다.

  • 010-1234-5678
  • 01012345678
  • 010.1234.5678

그런데 예전 핸드폰 번호의 경우에는 011, 016, 017, 018, 019로 시작하는 것이 있을 수 있으며, 가운데 자리가 3자리만 있는 번호도 있다.

사실 010으로 시작하는 번호는 항상 가운데 번호가 4자리 이므로 010-123-4567과 같은 번호는 유효하지 않다. 하지만 지금까지 소개한 내용으로는 이를 판별하기 어렵기 때문에, 다음 글에서 보다 세밀하게 매치하는 패턴을 소개하겠다.

따라서 패턴은 다음과 같이 정리할 수 있다.

^01[016789]                   # 문자열은 010, 011 등의 식별번호로 시작하며
          \D?\d{3,4}          # 숫자가 아닌 구분기호는 있을 수도 없을 수도
                   \D?\d{4}$  # 구분기호(옵션)뒤에 4자리 숫자

로그에서 값 추출하기

이전 글에서 사용했던 로그 추출하는 부분에 대해서 다시 검토해보자. 로그의 각 줄은 다음과 같이 생겼다.

Line 394 : [21:44:07 Oct 12 Fri] @0x3924004E|JVM| Free Memory: Heap [ 2368340/ 6291456], Native[ 7299824/32505856]

여기서 필요한 것은 로그가 찍힌 시간과, 남은 힙, 네이티브 메모리의 양이다.  이전 글에서는 이를 다음과 같이 완전 무식한 패턴을 사용했었다.

^L.+\[(\d\d:\d\d:\d\d).+\[(\d+)/(\d+).+\[(\d+)/(\d+).*$

아마 이글을 쓰던 시점에는 Notepad++에서 정규식으로 치환하는 방법을 찾아서 기록만 해두던 시점이어서 그랬던 것 같다. 이는 다음과 같은 패턴으로 정리할 수 있다.

^.*?\[                                              # 앞에 쓸데없는 것들
      (\d{2}                                        # 시간을 캡쳐하는 그룹 시작
            (?::\d{2}){2}                           # :00:00 형태 반복
                         )                          # 시간 캡쳐 그룹 끝
                          .*?\[                     # [ 이 나타나는 곳까지 패스
                                (\d+)               # 연속된 숫자값 캡쳐
                                     .*?\[          # [ 이 나타나는 곳까지 패스
                                           (\d+)    # 연속된 숫자값 캡쳐
                                                .*$ # 라인끝까지 매치

그리고 이 패턴에 매치한 결과를 \1,\2,\3 으로 치환하면 시간, heap, native 메모리 값만 남기고 나머지를 모두 제거할 수 있다.

HTML 태그의 내용만 추출하기

특정 HTML 태그의 내용만 추출하는 것은 웹 페이지를 스크래핑하여 특정 정보만 빼내는데 특히 많이 사용된다. 적절하게 구분할 수 있는 힌트만 있다면 파이썬에서는 beautifulsoup 같은 HTML 파서 없이도 정규식을 통해서 내용을 추출할 수 있다.

웹 페이지 내 테이블 내의 특정 정보들을 추출하는 경우를 생각해보자. 테이블의 각 셀은 td 태그로 이루어져 있으므로 td 태그 내의 내용은 다음 패턴으로 얻을 수 있다.

<td.*?>               # <td 속성="속성값"... > 등 별도의 attribute를 무시한 td 태그
       (.*?)          # 모든 텍스트 내용을
            </td>     # </td> 앞까지 캡쳐한다. 

해당 내용에 매치된 부분은 <td>, </td>를 포함하며, 캡쳐링 그룹으로 해당 내용을 선택할 수 있다.

다음 글에서는 Look Around라고 하는 백 레퍼런스 참고 기법과, IF 조건절을 정규식내에서 사용하는 방법 등 고급 정규식 기법에 대해서 살펴보도록 하겠다.


  1. 심지어 vim 정규식은 그 자체에서 다시 매직 모드라는 걸 적용하여 오락가락하는 정규식 문법을 사용한다.