Atomic Habits

정규 표현식 (좀 더) 깊이 알아보기 본문

IT/Regular Expressions

정규 표현식 (좀 더) 깊이 알아보기

체계성 2021. 11. 24. 22:02

출처 : https://medium.com/@originerd/%EC%A0%95%EA%B7%9C%ED%91%9C%ED%98%84%EC%8B%9D-%EC%A2%80-%EB%8D%94-%EA%B9%8A%EC%9D%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-5bd16027e1e0

글을 읽으실 분들은 대부분 정규 표현식을 사용하거나, 최소한 한 번이라도 들어 보셨을 것으로 생각하지만, 정의부터 알아보면 좋을 것 같습니다.

정규 표현식(正規表現式, 영어: regular expression, 간단히 regexp 또는 regex) 또는 정규식(正規式)은 특정한 규칙을 가진 문자열의 집합을 표현하는 데 사용하는 형식 언어이다.

 

개발하면서 문자를 다룰 때, 가장 유용한 것은 바로 정규 표현식이라고 할 수 있습니다. 복잡한 패턴의 문자열을 정규 표현식을 사용하지 않고 찾아내기는 정말 상상할 수 없을 만큼 힘겨울 것 같습니다.

저 역시 정규 표현식을 사용해 다양한 문제를 해결하고 있습니다. 페이지 내의 전화번호 혹은 이메일, 링크 등을 찾거나, 에디터에서 개발하다가 특정 부분을 원하는 형태로 재가공을 하는 것 처럼 말이죠. 정규 표현식은 대부분의 프로그래밍 언어에서는 물론이고, 에디터나 터미널 유틸리티 등 폭넓게 지원하고 있습니다.

하지만, 정규 표현식 자주, 그리고 잘 사용하고 있다고 생각하지만, 좀 더 알아보면 활용도가 더 높아지지 않을까 생각을 하게 되었습니다. 책장에 고이 잠들어있던 정규 표현식 책과 인터넷의 문서 읽으며, 다시 한번 정규 표현식의 재미를 느끼게 되었습니다.

 

한 단계씩 정규 표현식을 알아가는 방식이 좋을 것 같네요.
(*참고: 정규 표현식은 일반적으로 슬래쉬(/)문자로 감싸진 형태로 표현됩니다. 검색된 결과는 굵은 글씨로 표시하는 방식으로 표현하겠습니다. 또한 바로 확인 및 적용할 수 있도록 결과 옆에 링크를 달았습니다)

# 예문
"안녕하세요, 만나서 반갑습니다."
"그래, 안녕?"# 정규 표현식
/안녕/# 결과 http://rubular.com/r/i6kXOgYO9f
"안녕하세요, 만나서 반갑습니다."
"그래, 안녕?"

먼저, 가장 간단한 형태의 정규 표현식입니다. 단순하게 입력한대로 일치하는 문자를 찾습니다.

 

다음으로는, 정규 표현식에는 다양한 메타 문자가 존재합니다. 그 중에서도 가장 기본적인 메타 문자들을 알아보도록 하겠습니다.

  • . : 모든 문자와 일치합니다.
  • [] : 대괄호 사이에 존재하는 문자들 중 하나에 일치합니다.
  • [^] : 대괄호 사이의 가장 첫 번째 문자로 ^ 문자가 있을 때, 그 문자 이후에 존재하는 문자들을 제외한 모든 문자와 일치합니다.
  • [a-z] : 대괄호 사이에서 특정문자1-특정문자2가 존재할 때, 특정문자1과 특정문자2사이의 모든 문자와 일치합니다. [a-z]의 경우, a 부터 z까지 모든 영문자 소문자와 일치합니다.
  • ^ : 대괄호 사이에 존재할 때는 부정을 나타내지만, 대괄호 밖에서는 문자열의 시작과 일치합니다.
  • $ : ^와 반대로, 문자열의 끝과 일치합니다.
  • * : 앞에 존재하는 문자가 0번 혹은 그 이상 반복되는 문자를 찾을 때 사용합니다.
  • + : 앞에 존재하는 문자가 1번 혹은 그 이상 반복되는 문자를 찾을 때 사용합니다.
  • ? : 앞에 존재하는 문자가 있을 수도, 없을 수도 있을 때 사용합니다.
  • \ : . 혹은 [] 등 특수한 목적으로 사용되는 메타 문자를 문자열에서 찾고 싶을 때, 메타 문자를 문자 그대로 사용할 수 있도록 변환해주는 기호입니다. .은 모든 문자와 일치하지만, \.의 경우, .문자와 일치합니다.
# 예문
"안녕하세요, 만나서 반갑습니다."
"그래, 안녕?"# 정규 표현식
/[가-힣]+/# 결과 http://rubular.com/r/iwUTQOIMuN
"안녕하세요, 만나서 반갑습니다."
"그래, 안녕?"

이것은, 한글 가부터 힣, 즉, 모든 완성된 한글 문자와 한 개 혹은 그 이상 일치하는 문자를 찾는 정규 표현식입니다.

 

다음으로는, 정의되어 있는 패턴을 사용할 수 있는 방식을 알아보겠습니다.

  • \d : 숫자와 일치합니다.
  • \w : 영문자 및 _ 문자와 일치합니다.
  • \s : 여러 가지 공백 문자와 일치합니다. (* 스페이스, 탭, 기타… 공백 문자)

그리고 앞서 언급한 것들과 정반대의 기능을 하는 표현이 있습니다.

  • \D : 숫자를 제외한 문자와 일치합니다.
  • \S : 공백 문자를 제외한 문자와 일치합니다.
  • \W : 영문자 및 _ 문자를 제외한 문자와 일치합니다.
# 예문
Lorem ipsum dolor sit amet, consectetur adipiscing elit.# 정규 표현식
\w\w\w# 결과 http://rubular.com/r/UfgMxPRDdi
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

영문자(혹은 _) 세 개가 연달아 붙어있는 문자와 일치합니다.

 

앞서 본 예제에서 세 개가 연달아 붙어있는 문자를 표현했습니다. 더욱 쉬운 방법으로, 특정 문자수를 지정할 수 있습니다.

  • {n} : 앞에 존재하는 문자가 n번 반복되는 문자와 일치합니다.
  • {n, m} : 앞에 존재하는 문자가 n번 이상 m번 이하 반복되는 문자와 일치합니다.
  • {n,} : 앞에 존재하는 문자가 n번 이상 반복되는 문자와 일치합니다.
# 예문
Lorem ipsum dolor sit amet, consectetur adipiscing elit.# 정규 표현식
\w{3}# 결과 http://rubular.com/r/x8IiwvgbG8
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

바로 앞의 예제와 정확하게 일치하는 예제입니다. 더 가독성이 향상된 느낌입니다 :)

 

여러 문자와 반복하여 일치하는 표현식에 대해서 알아보았습니다. 특정 수가 지정되지 않고, n개 이상의 반복되는 문자와 일치하는 문자를 찾을 때, 의도치 않게 너무 많이 일치하는 경우가 발생할 수 있습니다. 기본적으로 탐욕적(Greedy) 방식으로 동작하기 때문입니다. ?문자를 해당 메타 문자 뒤에 붙이면 게으른(Lazy) 방식으로 동작합니다.

  • *?
  • +?
  • {n,}?

예를 들어보면 더 좋을 것 같네요. i로 시작하고 n으로 끝나는 모든 문자를 찾고 싶다고 가정합시다. 다음과 같이 작성할 수 있을 것 같습니다.

# 예문
internationalization# 정규 표현식
i\w+n# 결과 http://rubular.com/r/em8RaHlnq9
internationalization

결과에 나온 대로, internationalization 모두가 일치했습니다. i로 시작하고 n으로 끝나는 모든 문자를 찾고 싶었지만, 일치하는 건 internationalization 통째로 하나뿐입니다.

게으른 방식으로 바꿔보겠습니다.

# 예문
internationalization# 정규 표현식
i\w+?n# 결과 http://rubular.com/r/fr6Ejzal0N
internationalization

앞의 예제와의 차이점은 단 하나 ?문자입니다. ?를 붙임으로 인해서 게으른 방식으로 동작하고 있습니다. internationalization은 i로 시작하고 n으로 끝나는 문자긴 하지만, 그 사이 사이에도 i로 시작하고 n으로 끝나는 문자가 여럿 존재합니다. 이렇게 요구에 따라 탐욕적 혹은 게으른 방식의 찾기를 할 수 있습니다.

 

다음으로는, 빼놓을 수 없는 그룹을 설명하도록 하겠습니다. 그룹은 () 문자로 지정할 수 있으며, 괄호 사이에 존재하는 표현식을 통해 찾은 결과를 묶음으로 처리할 수 있도록 해줍니다. 그룹을 통해 하나의 결과에서도 여러 가지 그룹으로 나눌 수 있고, 같은 문자가 반복되는 것을 찾거나, 원하는 방식으로 사용할 수 있습니다.

그룹은 일반적으로 \1과 같이 \ 문자와 그룹의 번호로 구성됩니다. 일반적으로 전체 결과가 \0이며, 앞에서 부터 나타나는 그룹의 순서에 따라 숫자가 하나씩 증가하는 방식입니다.

# 예문
<h1>이것은 첫 번째 제목</h1>
<h2>이것은 두 번째 제목</h2>
<h3>이것은 세 번째 제목</h3>
<h4>It's also right heading</h4>
<h5>이것은 올바르지 않은 제목</h6># 정규 표현식
<(h[1-6])>[가-힣\w\s']+<\/\1># 결과 http://rubular.com/r/ZF58XxDYVw
<h1>이것은 첫 번째 제목</h1>
<h2>이것은 두 번째 제목</h2>
<h3>이것은 세 번째 제목</h3>
<h4>It's also right heading</h4>
<h5>이것은 올바르지 않은 제목</h6>

HTML에서 제목을 나타내는 h 태그를 찾는 정규 표현식입니다. h 태그는 h1부터 h6까지 있으므로, h[1-6]로 표현하였습니다. 태그의 경우, 여는 태그 <h1> 와 닫는 태그 </h1>가 일치하므로, (h[1-6])로 그룹을 만들고, 만들어진 그룹을 이용해 닫는 태그를 지정해놓고, 그 사이에 한글 및 영문자, 공백, ' 문자 한 개 이상 존재하는 문자를 찾도록 하였습니다. 따라서, h1에서 h4까지는 제대로 열고 닫혀서 정상적으로 찾아졌습니다. 하지만 마지막 문자열의 경우, 여는 태그는 <h5>로 시작했지만, 닫는 태그의 경우 </h6>로, 제대로 여닫아지지 않았기 때문에 찾아지지 않았습니다. 이처럼, 그룹을 잘 활용하면 다양한 문제를 해결할 수 있습니다.

 

앞서 설명한 부분은 정규 표현식을 어느 정도 접해보신 분들이라면, 이미 아는 부분이어서 지루하셨거나, 훑어보며 내려오셨을 것 같습니다. 저도 주로 사용하는 부분은 이 정도였습니다. 제목처럼 (좀 더) 깊이 들어가 보도록 하겠습니다. (“좀 더”입니다. 역시나 싱거울 수 있습니다)

특정한 문자 앞에 일치하는 문자만 찾는 전방탐색과 특정한 문자 뒤에 일치하는 문자만 찾는 후방탐색이 있습니다. 전방탐색과 후방탐색으로 일치된 문자는 그룹처럼 보이지만 결과에는 포함되지 않습니다.

  • (?=) : 전방탐색. 찾고자 하는 표현식 뒤에 전방탐색 표현식을 넣으며(?=와 ) 사이에 표현식을 넣습니다. 전방탐색 표현식을 통해 문자가 존재하고, 그 앞에 찾고자 하는 문자가 존재할 때 일치합니다. 특정 문자가 포함된 문자를 찾고 싶지만 결과에 포함하고 싶지는 않을 때 사용합니다.
  • (?<=) : 후방탐색. 후방탐색 표현식 (?<=와 ) 사이에 표현식을 넣고, 찾고자 하는 표현식을 작성합니다. 후방탐색 표현식을 통해 문자가 존재하고, 그 뒤에 찾고자 하는 문자가 존재할 때 일치합니다. 전방탐색과 유사하죠.

이와 반대인 탐색이 있습니다.

  • (?!) : 부정형 전방탐색. 전방탐색과 반대로, 부정형 전방탐색 내의 표현식이 일치하지 않고, 찾고자 하는 문자가 존재할 때 일치합니다.
  • (?<!) : 부정형 후방탐색. 후방탐색과 반대로, 부정형 후방탐색 내의 표현식이 일치하지 않고, 찾고자 하는 문자가 존재할 때 일치합니다.

설명이 굉장히 어렵네요. 예제를 통해서 살펴보면 더 이해하기 쉬울 것 같습니다.

# 예문
2400원
3600원
28392830원
238493엔
원2839283# 정규 표현식
\d+(?=원)# 결과 http://rubular.com/r/XrZGoLjUbm
2400원
3600원
28392830원
238493엔
원2839283

원화 표기로 작성된 문자를 찾는 예제입니다. 전방탐색을 사용했고, 숫자와 원이라는 문자가 들어가야 일치하지만, 결과에는 원이 포함되지 않은 걸 확인할 수 있습니다.

 

깊이 들어가려면 끝이 없는 게 정규 표현식인 것 같습니다. 이 글에서의 마지막은 역참조(Backreference)입니다. ‘어떤 그룹이 있으면, 이런 문자를 찾아라.’ 같은 개념이라고 볼 수 있습니다. 수를 찾는데, 괄호로 묶여있는 수의 경우 괄호까지 찾고 싶을 때 사용할 수 있습니다. 일종의 분기문이라고 할 수 있습니다.

  • (?(n)) : ?(n)의 n에 그룹의 번호를 넣습니다. 이 뒤에 나타나는 것은 n번 그룹이 존재할 때 일치해야 하는 표현식입니다.
# 예문
12345
12.3456
(120.293)
(18729.28
2839283)# 정규 표현식
(\()?\d+(\.\d+)?(?(1)\))# 결과 http://rubular.com/r/H2pt3TCCqF
12345
12.3456
(120.293)
(18729.28
2839283)

위의 설명에서 언급한 내용을 예제로 만들어 보았습니다. (\()?  ( 문자를 그룹으로 만들었고, ? 메타 문자를 통해 있을 수도 없을 수도 있다는 것을 표현한 것입니다. 그리고, 숫자(소수점 포함)를 표현하였고, (?(1)\))를 통해 앞서 설명한 역참조를 사용하였습니다. (1)의 경우, 가장 첫 그룹인 ( 문자 유무를 판단하고, 이 그룹이 있으면 ) 문자가 있는지 찾습니다. 따라서, 결과에 나온 것처럼, 숫자들 혹은 괄호로 둘러싸인 숫자를 찾습니다. 괄호가 제대로 여닫아지지 않는 숫자들은 그냥 숫자에만 일치하는 것을 확인할 수 있습니다.

 

'IT > Regular Expressions' 카테고리의 다른 글

[정규식 수량자]  (0) 2021.12.04
정규식 간단 강좌 1-8편  (0) 2021.11.24
풀어 쓴 정규식  (0) 2021.11.24
[JAVA] 정규식/정규표현식  (0) 2021.11.22
정규식 전후방탐색(vim 포함)  (0) 2021.11.20
Comments