8 분 소요

다양한 정규표현식 패턴 표현

스크린샷 2023-06-16 오후 5 32 40

1. 수량자

특정 문자열에서 “hey” 도 찾고 “heeeeeeeeeey” 도 찾고 싶으면 어떻게 하면 될까요?
특정 문자 하나에 수량자를 적용하여 문자의 개수를 유연하게 지정해줄 수 있습니다.

예를 들어 /he+y/ 는 e가 1개 이상 포함되어야 한다는 뜻입니다.
/he*y/ 는 e가 0개이상, 즉 없어도 되고 많아도 된다는 뜻입니다.
/he?y/ 는 e가 0개 혹은 1개를 뜻합니다. 이렇게 패턴을 잡으면 “heeeeeeeeeey”는 찾지 못할 것입니다.

수량자는 사용자 정의 하에 해당 문자의 개수의 범위도 지정해줄 수 있습니다.
“heeeeeeeey”의 e가 10개 이상 20개 이하인 것만 찾고 싶다면 /he{10, 20}y/ 패턴을 사용하면 될 것입니다.

스크린샷 2023-06-16 오후 3 30 07

2️. [], -, ^

[]

[]는 “한” 글자를 표현합니다. 그리고 대괄호 안에는 가능한 문자를 작성하면 됩니다.
예를 들어 주어진 문자열에서 cap, pap, tap 을 찾고 싶다고 가정하면 /[cpt]ap/gm 패턴을 사용하면 됩니다.

-

-는 범위를 나타낼 수 있습니다. 흔히 “0-9”, “a-z”, “A-Z”를 표기하곤 합니다.
예를 들어 “아무 소문자 + ap” 를 찾고 싶다면 /[a-z]ap/gm 패턴을 통해 찾을 수 있을 겁니다.
이 경우 “aap”, “bap”, …, “zap” 모두 찾을 수 있을 겁니다.

^

^(caret)에 대해 설명하기 전에 하나만 당부의 말씀을 드리겠습니다.
^ ? 이 두 녀석은 다재다능한 녀석입니다.
반드시 그걸 염두해두시고 쓰임새를 구분하셔야 정규표현식 패턴이 헷갈리지 않으실 겁니다.

자 다시 ^에 대해 설명하면,
[] 대괄호 안에 ^를 어미에 두게 되면 이 녀석들은 빼고 한 글자를 의미합니다.

예를 들어 /[^cpt]ap/gm 패턴은 “cpt”를 제외한 문자와 연속된 “ap” 문자를 찾습니다.
“kap”, “zap” 뿐만 아니라 “가ap”, “*ap” 모두 찾을 수 있습니다.

QUIZ

영어 문장을 찾는 정규표현식 패턴은 어떻게 구성될까요?

  1. 맨 앞 문자는 대문자이여야 합니다.
  2. 맨 끝 문자는 . ? ! 이어야 합니다.
  3. 그리고 그 사이에는 . ? ! 이 들어가 있으면 안 됩니다.
  • 정답
    • /[A-Z][^\.?!]+[\.?!]/gm
      • 위 표현식 패턴만이 정답은 아닙니다. 다른 패턴도 정답일 수 있습니다!
      • 또한 깊게 들어가면 더 많은 예외처리가 필요해입니다.

3️. backslash

escape

특수문자의 경우 \를 통해 escaping할 수 있습니다.
예를 들어, 패턴에서 . 는 모든 문자(줄바꿈 제외)를 뜻합니다.
만약 문자로서 마침표를 찾고 싶다면 \. 를 사용해야합니다.

character class

이스케이핑 용도 외에도 문자 집단을 나타내는 용도로도 사용 가능합니다.
스크린샷 2023-06-16 오후 3 40 29

만약 숫자 0이상 9999이하임을 확인하려면 어떤 정규표현식 패턴을 만들어야 할까요?
/\d{0,4}/ 이면 9999를 초과하거나 숫자가 아닌 경우 패턴에 매치되지 않습니다.

참고로 \w 에서 말하는 문자는 “a-z”, “A-Z”, “0-9”, “_” 를 뜻합니다.
_(underscore) 도 포함된다는 것을 명심해주세요.

4️. anchor, boundary

anchor

처음과 끝을 지정해줄 수 있습니다.

  • ^ : 처음
  • $ : 마지막

아까 [] 설명할 때 ^는 다재다능하다고 그랬습니다. 여기에서 ^[]안에 있는 녀석이 아님을 명심하세요!

위에 퀴즈에서 설명드렸던 영어 문장 찾는 패턴에서
/[A-Z][^\.?!]+[\.?!]/gm이 아니라 /^[A-Z][^\.?!]+[\.?!]$/gm 를 사용하면 어떻게 될까요?
“I love cat. I love pig.” 이 문자열에서 어느 것 하나도 매치되는 것이 없다는 걸 알 수 있습니다.

  • 패턴: [A-Z][^\.?!]+[\.?!]
    스크린샷 2023-06-16 오후 4 03 03
  • 패턴: ^[A-Z][^\.?!]+[\.?!]$
    스크린샷 2023-06-16 오후 4 03 29
    • 문자열의 처음과 끝을 한 덩어리로 보고 매칭되는게 있는지 찾는다.
    • “I love cat. I love pig.”의 경우 중간에 “.”이 있기 때문에 매칭되지 않는다.

이번에는 문자열 중간에 .을 빼보겠습니다.

  • 패턴: [A-Z][^\.?!]+[\.?!]
    스크린샷 2023-06-16 오후 4 05 15
  • 패턴: ^[A-Z][^\.?!]+[\.?!]$
    스크린샷 2023-06-16 오후 4 04 51

boundary

바운더리는 단어의 경계를 뜻합니다. (문장의 경계가 아님을 주의하세요!)
그리고 경계의 기준은 \W인 것(= 문자가 아닌 것)입니다.

경계를 나타내는 패턴 문자는 \b입니다.
\b는 찾고 싶은 단어의 양 옆에 붙일 수도 있으며 한 쪽에만 붙일 수도 있습니다.

예를 들어, /\bpokemon\b/gm 패턴을 사용하면
“pokemon pokemon pokemon!pokemon pokemon_” 에서 몇 개의 pokemon 을 찾을 수 있을까요?

스크린샷 2023-06-16 오후 4 07 58
마지막 pokemon을 제외하면 모든 pokemon이 매치될 것입니다.
마지막 “pokemon_” 에서 _\W가 아니기 때문에 경계로서 작용하지 않게 됩니다. (_\w 입니다.)

boundary에 대한 의문

\b(boundary)가 단어의 경계를 뜻하고, 그 기준이 문자가 아닌 것(\W)이라면
굳이 \b를 사용하지 않고 \W를 사용하면 되는 것 아닌가요?”
라고 생각하실 수 있을 것 같습니다.

백문이 불여일견입니다.
스크린샷 2023-06-16 오후 4 01 10

스크린샷 2023-06-16 오후 4 01 22
이렇게 나온 이유에 대해서는 충분히 고민하시면 좋겠습니다! 👍🏻

5️. flag

자 이제 패턴 말미( / /gm ) 에 붙였던 gm에 대해 이야기할 시간이 왔습니다.
gm은 하나가 아니라 사실 g, m을 뜻하는 플래그입니다.
정규표현식의 룰을 옵션으로 선택한다고 보면 됩니다.

플래그마다 어떠한 룰을 뜻하는 것인지 하나씩 알아보도록 하겠습니다.
(그 전에 주의할 점은 플래그는 서로 배타적인 옵션이 아닙니다.
서로 특성이 다르고 모두 선택할수도 모두 선택 제외할 수도 있습니다.)

g (global)

글로벌(g)로 선택하지 않으면 해당 정규표현식에 match되는 단 하나의 구문만 찾습니다.
하나를 찾은 후에는 더 이상 match를 찾지 않습니다.
글로벌(g)을 선택하면 최대한 많은 match를 찾습니다.

m (multiline)

멀티라인을 선택하면 \n(개행)을 기준으로 각각의 문장이 개별적인 문자열로 취급됩니다.
선택하지 않는다면 아무리 개행을 해도 모든 내용이 하나의 문자열로 취급됩니다.

i (case insentive)

i를 선택 시 모든 대문자와 소문자의 구분을 없애줍니다.

s (single line)

위에서 . 이 개행(\n)을 제외한 모든 글자를 뜻한다고 했습니다.
하지만 이는 singe line을 선택하지 않았을 때에만 적용됩니다.
즉, s를 선택하게 되면 . 는 개행(\n)까지 포함한 모든 글자를 뜻하게 됩니다.

6️. Quantifier(수량자): Greedy vs Lazy

2번 수량자의 심화 개념입니다.
수량자를 체크할 때 최대한 많이 할 수도, 최대한 적게 할 수도 있습니다.
말 그대로 하면 탐욕적인 녀석과 게으른 녀석을 선택할 수 있다는 것이겠죠?

  • greedy
    • as much as. 최대한 많은 표현을 찾습니다.
    • 자바스크립트는 이미 greedy가 기본. (언어/플랫폼마다 상이할 수 있습니다.)
    • Lazy를 사용할 것인지 아닐 것인지 표기해주면 됩니다.
  • lazy
    • as less as입니다.
    • lazy로 표현된 수량자는 최대한 나중에 평가됩니다.
      따라서 그 외의 것이 먼저 평가되기 때문에 최대한 작은 것이 선택됩니다.
    • 수량자 뒤에 ? 를 붙여 표현 가능합니다.
      • 분명히 말씀 드렸습니다! ^와 ?는 하는 일이 많습니다! 위치에 따라 의미가 달라지니 반드시 구분할 수 있어야 합니다.

쓸모없는 LAZY

앞서 설명했듯 우리가 이제껏 사용한 수량자는 기본적으로 greedy입니다.

/la*?/, /la+?/
이런 패턴이 대표적으로 쓸모없는 lazy 수량자 패턴입니다.
왜냐하면 순서대로 /l/, /la/ 패턴과 동일한 매치가 되기 때문입니다.

왜냐하면 a*?에서 *는 0개 이상을 뜻하지만 lazy(?)이니 a가 최소인 0개인 것이 선택되는 것입니다.
그리고 a+?에서 +는 1개 이상을 뜻하지만 lazy(?)이니 a가 최소인 1개인 것이 선택되는 것입니다.

useful LAZY

앞서 다루었던 영어 문장을 찾는 정규표현식 패턴을 다시 가져와보겠습니다.
[A-Z][^\.!?]*[\.!?] 이 표현을 lazy 수량자를 사용하여 다르게 표현할 수 있습니다.
[A-Z].*?[\.!?] 이런식으로 표현하면 lazy 수량자를 통해 같은 구문을 찾을 수 있습니다.

패턴 설명은 이렇습니다.

  1. .: 모든 문자(개행 제외)
  2. *?: 0개 이상이지만 최소한으로
  3. [\.!?]: ., !, ?가 나오기 전까지

만약 lazy에 대해 아직 감이 안 오신다면 ? 를 빼고 실습을 해보세요.
그러면 .* 가 greedy이기 때문에 최대한 많은 문자를 포함한 구문이 매치된다는 것을 알 수 있을 겁니다.
실습 : https://regex101.com/r/gwqfrX/1

7️. group

match와 group의 차이

매치는 말 그대로 정규표현식 패턴에 맞게 찾은 구문을 뜻합니다.
그리고 그룹은 그 매치 안에서 구분되어지는 또 다른 구문을 의미합니다.
그리고 그룹은 소괄호 () 를 통해 지정할 수 있습니다.

multiple

단순히 매치 안에서 그룹을 지정하기 위해 사용되는 것은 아닙니다.
or 연산자처럼 다수의 단어 중 하나만 충족되어도 찾고 싶을 때 그룹을 사용합니다.

“I love cat. I love pig.”이라는 문자열에 /I love (cat|dog|pig|butterfly)./gm 를 적용시켜보면 두 문자를 모두 매치시킬 수 있습니다.

대신에 그룹 () 을 사용했기 때문에 match된 정보에 group 정보도 포함됩니다.
만약 group 지정이 필요없을 경우 (?: )로 표현하면 됩니다.
(마지막으로 한번 더 강조하겠습니다! ?가 또 나왔습니다… 구분하셔야 합니다!)

8️. Capturing Group

/([^\s]+)\.(?:png|jpe?g|pdf)/gm (https://regex101.com/r/P7T4wK/1)
그룹까지 공부를 했으니 이번엔 패턴부터 보여드리겠습니다. 대충 예상이 가시나요?

png, jpg, jpeg, pdf의 확장자를 갖고 있는 파일 이름(확장자명 제외)을 찾고 싶습니다.
그러기 위해서는 해당 확장자 이름을 찾은 다음 그 앞에 있는 파일명을 찾아야할 것입니다.
때문에 .앞에 있는 부분을 그룹으로 지정해줌으로써 확장자 이름을 제외한 파일 이름만 추출할 수 있습니다.

여기에서 매치된 구문에서 그룹에 접근하는 것은 언어/플랫폼마다 다릅니다. 이 부분은 해당 언어의 공식문서를 참고하시면 됩니다.

group number

캡처링한 문자를 바로! 곧바로! 정규표현식에 적용할 수도 있습니다.
다음은 html 태그의 열린태그와 닫는태그를 한번에 찾을 수 있는 정규표현식입니다.
캡처링한 그룹은 패턴에서 /1, /2 처럼 사용할 수 있습니다.

  • /<(\w+)>.*?</\/1>/g
    • <(\w+)>: opening tag를 캡처링을 통해 태그 이름(그룹1)을 저장합니다.
    • .*?: lazy 수량자를 통해 최소한의 문자만 해당하게 합니다.
    • </\/1> : \와 함께 opening tag에서 캡처링한 태그 이름(그룹1)이 패턴에 적용됩니다. 이것이 closing tag 입니다.
    • flag : html 문서는 개행에 상관없이 하나의 문자열로 취급되어야 하기 때문에 multiline(m)이 제외되어야 합니다.

group name

/(?<filename>[^\s]+)\.(?<extension>png|jpe?g|pdf)/gm
(?<그룹명> )을 통해 그룹 이름을 지정할 수도 있습니다.

파이썬에서 정규표현식 사용

파이썬에서 정규표현식을 사용하기 위해서는 re 라이브러리를 import 해야한다.

import re

패턴 객체의 메서드

p = re.compile('[a-z]+') # 정규식 객체를 리턴

# 💥 match: 시작부터 일치하는 패턴 찾기
print(p.match('aaaaa')) # <re.Match object; span=(0, 5), match='aaaaa'>
print(p.match('bbbbbbbbb')) # <re.Match object; span=(0, 9), match='bbbbbbbbb'>
print(p.match('1aaaa')) # None
print(p.match('aaa1aaa')) # <re.Match object; span=(0, 3), match='aaa'>

# 💥 search: 전체 문자열에서 첫 번째 매치 찾기
print(p.search('aaaaa')) # <re.Match object; span=(0, 5), match='aaaaa'>
print(p.search('11aaaa')) # <re.Match object; span=(2, 6), match='aaaa'>
print(p.search('aaa11aaa')) # <re.Match object; span=(0, 3), match='aaa'>
print(p.search('1aaa11aaa1')) # <re.Match object; span=(1, 4), match='aaa'>

# 💥 findall: 모든 매치를 찾아 리스트로 반환
print(p.findall('aaa')) # ['aaa']
print(p.findall('11aaa')) # ['aaa']
print(p.findall('1a1a1a1a1a')) # ['a', 'a', 'a', 'a', 'a']
print(p.findall('1aa1aaa1a1aa1aaa')) # ['aa', 'aaa', 'a', 'aa', 'aaa']

# 💥 finditer: 모든 매치를 찾아 반복가능 객체로 반환
f_iter = p.finditer('a1bb1ccc')
print(f_iter) # <callable_iterator object at 0x106375f90>
for x in f_iter:
    print(x)
# <re.Match object; span=(0, 1), match='a'>
# <re.Match object; span=(2, 4), match='bb'>
# <re.Match object; span=(5, 8), match='ccc'>


참고로 아래는 두 코드는 동일한 결과를 출력한다.

p = re.compile('[a-z]+') # re.compile('패턴') 은 정규식 객체를 리턴
print(p.match('aaaaa')) # <re.Match object; span=(0, 5), match='aaaaa'>
# re.compile('패턴', '검사하려는 문자열')
print(re.match('[a-z]+', 'aaaaa')) # <re.Match object; span=(0, 5), match='aaaaa'>

예제

strr = '...!@bat#*..y.abcdefghijklm'

# 1️⃣ strr에서 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거합니다.
strr = re.sub('[^a-z0-9\-_\.]', '', strr) # ...bat..y.abcdefghijklm

# 2️⃣ 마침표(.)가 2번 이상 연속된 부분을 하나의 마침표(.)로 치환합니다.
strr = re.sub('\.+', '.', strr) # .bat.y.abcdefghijklm

# 3️⃣ strr에서 마침표(.)가 처음이나 끝에 위치한다면 제거합니다.
strr = re.sub('^[.]|[.]$', '', strr) # bat.y.abcdefghijklm
# ^: 처음, $: 끝

매치 객체의 메서드

패턴 객체의 메서드를 통해 리턴된 매치 객체는 아래와 같은 형태이다.

<re.Match object; span=(매치 시작지점 인덱스, 매치 끝지점 인덱스), match='매치된 문자열'>
# ex. <re.Match object; span=(5, 8), match='ccc'>


매치 객체는 내부 정보에 접근할 수 있는 네 가지 메서드를 제공한다.
스크린샷 2023-06-16 오후 1 18 10

p = re.compile('[a-z]+')

result = p.search('1aaa11aaa1') # search: 전체 문자열에서 첫 번째 매치 찾기
print(result) # <re.Match object; span=(1, 4), match='aaa'>

# 💥 매치 객체의 메서드 실행
print(result.group()) # aaa
print(result.start()) # 1
print(result.end()) # 4
print(result.span()) # (1, 4)

group

정규표현식을 () 안에 넣으면 그 부분만 그룹화된다.
groups 메서드를 통해 그룹들을 튜플 형태로 리턴 할 수 있다.

p = re.search('(hello)(world)', 'helloworld') # re.search('패턴', '검사하려는 문자열')
print(p.groups()) # ('hello', 'world') # 각 그룹의 매치 결과가 튜플 형태로 리턴

# 💥 group() 메서드를 통해 각 그룹을 호출할 수 있다.
# 인자를 넣지 않으면 전체 매치 결과 리턴
print(p.group()) # helloworld
# group()와 같다
print(p.group(0)) # helloworld
# 1번 그룹 매치 결과 리턴
print(p.group(1)) # hello
# 2번 그룹 매치 결과 리턴
print(p.group(2)) # world

group 과 groups의 차이

m = re.search('\d{4}-(\d?\d)-(\d?\d)', '1868-12-10')
# group 메서드는 정규식 전체의 일치부를 찾는다.
print(m.group()) # 1868-12-10 
# 반면에 groups 메서드는 명시적으로 캡처(( )로 감싼 부분)한 부분을 반환한다.
print(m.groups()) # ('12', '10') 

Capturing Group

yyyy-mm-dd와 같이 날짜를 나타내는 문자열 중 월, 일을 각각 따로 빼서 쓰고 싶다고 하자.
그럼 따로 빼고 싶은 부분인 mmdd 부분에만 소괄호의 캡처 기능을 사용하면 된다.

print(re.findall('\d{4}-(\d\d)-(\d\d)', '2028-07-28')) # [('07', '28')]
print(re.findall('\d{4}-(\d\d)-(\d\d)', '1999/05/21 2018-07-28 2018-06-30 2019.01.01')) # [('07', '28'), ('06', '30')]


💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기

태그:

카테고리:

업데이트: