콘텐츠로 건너뛰기
Home » 정규표현식의 개념과 기초 문법

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

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

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

정규표현식의 개념

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

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

정규표현식의 종류

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

유닉스 명령줄 도구들에서 사용하던 정규표현식은 후에 POSIX 표준에 편입되었다. 그러면서 이 시기의 표준으로 받아들여진 형식을 POSIX 정규식이라고 한다. 그리고 이후에 다시 POSIX 정규식은 POSIX BRE (POSIX 기본 정규식)와  POSIX ERE (POSIX 확장 정규식)으로 다른 버전이 나뉘게 된다. (grep에서 -e 스위치를 써서 확장 정규식을 쓰던 옵션이 바로 ERE를 쓴다는 의미이다.) 그 외에 BRE를 기본 골격으로 한 vim 정규식이 있다. 이 vim 정규식은 vim 편집기 내에서 찾기/바꾸기 등의 동작에서 범위를 지정하는데 사용된다. 하지만 오랜 역사를 따라 개선과 확장을 거듭하면서 이 vim 정규식 역시 POSIX 표준과는 좀 다른 규격으로 취급될 정도1심지어 vim 내에서도 magic 모드, very magic 모드라는 것이 있어서 이후의 확장 정규식의 일부 기능을 사용하거나, 일부 punctuation 문자를 매칭하는 방법이 다시 갈리게 되는 지경이다.이다.

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

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

정규표현식 기본 문법

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

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

정규표현식 메타 문자

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

  • ^ : 문자열의 시작을 표현한다. [ ... ] 내부에서 쓰이는 경우라면 뒤의 패턴에 일치하지 않는 것을 선택한다.
    • ^http : 문자열이 http로 시작하는 경우에만 매치하며, 중간에 나타난 http에는 매치하지 않는다.
    • ab[^0-9]: “ab” 뒤에 숫자가 아닌 것이 오는 것에만 매치한다. (“abc” – O, “ab1” – X)
  • $ : 문자열의 끝을 표현한다.
    • them$ : 문자열이 “them”으로 끝난 경우에만 해당 “them”에 매치한다.
  • \b : 단어의 경계. 문자열 시작과 끝, 공백, 개행, 탭, 콤마, 구두점, 대시문자 등이 올 수 있다.
    • \bplay\b : 는 단어 경계로 구분되는 “play”에는 매치한다. 하지만 “playground”의 “play”에는 매치하지 않는다.
  • \B : \b가 아닌것. 정규식 메타문자에서는 흔히 대문자로 표현한 것은 소문자로 표현한 문자의 반대를 의미한다.
    • \bplay\B : “play”뒤에 단어의 경계가 아닌 것이 올 때에만 매치한다. “playground”, “playball”의 “play”에 매치한다. 뒤에 오는 “g”, “b” 등의 문자는 포함하지 않는다.
  • \s : 공백 문자 및 탭 문자에 매치한다.
  • \S : 공백 문자가 아닌 한 글자에 매치한다.
  • \d : 숫자에 매치한다. [0-9]와 같다.
  • \D : 숫자가 아닌 문자에 매치한다. [^0-9]와 같다.
  • \w : 단어를 만들 수 있는 글자. 알파벳 대소문자, 숫자, 언더스코어를 포함한다. [A-Za-z0-9_] 와 같다.
  • \W : \w에 포함되지 않는 문자들
  • \n : 개행문자. \r은 캐리지 리턴이다.
  • \ : 이스케이프용 문자. 정규식 상의 특별한 의미가 있는 문자들을 문자 그대로 쓸 때 앞에 붙인다. \^ 라고 쓰면 “^” 문자 그대로를 가리킨다.
  • . : 아무 문자 1개에 대응된다. 공백 역시 문자 1개로 취급된다.

메타 문자의 패턴을 유심히 살펴보면 알겠지만, 소문자를 쓴 것과 대문자를 쓴 것이 서로 반대의 의미를 지닌다.

선택 패턴

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

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

또한 선택 패턴 내에서 ^ 이 쓰이면 not 의 의미가 되며, 이 문자 뒤에 오는 문자들은 제외하게 된다. [^cfh]all 은 앞서 나온 call, fall, hall에는 매치하지 않으며 mall에는 매치하게 된다.

그룹

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

  • (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 개 만큼 나오는 경우에 수량한정자를 뒤에 붙일 수 있다.

  • ? : 바로 앞의 글자 혹은 그룹이 1개 혹은 0개 이다.
    • apples? : s?는 “s”가 있을 수도 있고, 없을 수도 있다는 의미로, “apple”, “apples” 모두에 매치된다.
  • * : 0개 이상이다.
    • n\d* : n 뒤에 숫자가 0개 이상이라는 의미. “n”, “n1”, “n123” 에 모두 매치된다.
  • + : 1개 이상이다.
    • n\d+ : “n” 뒤에 숫자가 1개 이상이다. “n1”, “n123″에 매치되지만 “n” 에는 매치되지 않는다.
  • {n} : n 개가 있다.
    • n\d{2}$ : “n”뒤에 숫자가 2개 있다. “n12” 에 매치되지만, “n”, “n1”, “n123″에는 매치되지 않는다. n\d{2}라고 했을 때에 “n123″에서는 n12까지만 매치하고 3은 제외한다.
  • {n, m} : n개 이상, m개 이하가 있다.
    • n\d{2, 3}$ : “n12” , “n123″에는 매치되지만 “n1″이나 “n1234″에는 매치되지 않는다.
    • m은 생략가능하며, 생략되면 n개 이상이라는 의미가 된다.

수량한정자의 lazy 매치

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

i like apples and bananas

라는 문장에 대해서 ^.*s를 매치하면 . 문자(아무 글자)는 욕심을 부려서 다 먹어치우기 때문에 마지막으로 등작하는 s, 즉 “i like apples and bananas”가 모두 매치된다.

이 때, ^.*?s라고 쓰면, 첫글자 이후 최초 등장하는 s까지만 매치한다. 따라서 이 패턴에서는 i like apples 까지만 매치한다.

예제

몇 가지 예제를 통해 정규식의 동작에 대해 익혀보자. 해당 예제들은 hacker 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}(?:\.[^\n]{3}){3}
---------------------------------------------------
[^\n]               # 개행문자가 아닌 글자
     {3}            # 가 3개 있고
(?:                 # 캡쳐하지 않는 그룹이 시작
 \.                 # . 이 온 후
   [^\n]{3}         # . 다음에 다시 개행이 아닌 문자가 3개
){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]\D?\d{3,4}\D?\d{4}
-----------------------------------------------------
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}).*?\[\s(\d+).*?\[\s(\d+)
---------------------------------------------------
(                   # 첫번째 그룹을 캡쳐
(?:\d{2})           # 숫자 두 개가 연속으로 나오고
(?:                 # 캡쳐안되는 그룹으로 묶은,
\:\d{2}             # :45 같은 : 뒤에 숫자 두 개가 오는 패턴이
){2}                # 두개 더 붙어서 12:34:56 패턴을 완성
)                   # 이게 첫번째 그룹
.*?\[\s             # "[ " 을 만날 때까지 앞으로 나가서
(\d+)               # 연속된 숫자만 취한다. 이게 두 번째 그룹
.*?\[\s(\d+)        # 같은 패턴을 한 번 더 써서 세 번째 그룹

그리고 이 패턴에 매치한 결과를 \1,\2,\3 으로 치환하면 시간, heap, native 메모리 값만 남기고 나머지를 모두 제거할 수 있다. 정규식 동작 확인은 여기서 보면 좀 더 자세한 설명과 함께 볼 수 있을 것이다.

HTML 태그의 내용만 추출하기

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

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

<tr.*?>(.*?)</tr>
------------------
<tr                 # <tr 로 시작한다.
   .*?>             # tr 태그가 끝날 때까지
(.*?)               # 태그내 HTML 내용을 캡쳐
</tr>               # 닫는 태그

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

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