지금까지 배운 패턴들은 "글자 한 개"에 대응하는 게 많았어요. [a-z]도 한 글자, \d도 한 글자였죠. 그런데 실제 데이터는 "숫자가 세 개", "b가 여러 개", "u가 있거나 없거나"처럼 개수가 제각각이에요. 이 반복 횟수를 표현하는 도구가 바로 오늘 배울 수량자예요. 별표(*), 플러스(+), 물음표(?) 이 세 개인데, 앞 편들에서 +를 살짝 맛봤으니 이제 셋을 제대로 파헤쳐 볼게요. 이걸 알면 정규식이 갑자기 유연해져요.
07.1 "몇 번 반복"을 어떻게 표현할까요?
수량자는 바로 앞의 요소가 몇 번 반복되는지를 정해요. 여기서 "바로 앞의 요소"라는 말이 중요해요. 수량자는 자기 왼쪽에 붙은 딱 한 덩어리에만 적용되거든요. 세 가지 뜻을 먼저 한 줄로 정리하면 이래요. *는 0번 이상(없어도 됨), +는 1번 이상(최소 하나), ?는 0번 또는 1번(있거나 없거나)이에요.
이 세 개의 차이는 "0개를 허용하느냐", "몇 개까지 허용하느냐"로 갈려요. 말로만 들으면 아리송하니까, 지금부터 하나씩 실제 결과로 확인해 볼게요. 같은 대상에 수량자만 바꿔 넣으면 결과가 어떻게 달라지는지 비교하는 게 오늘의 핵심 재미예요.
한 가지 미리 주의할 게 있어요. 수량자는 바로 왼쪽 한 글자(또는 한 묶음)에만 적용된다고 했죠? 그래서 ab+는 "ab가 반복"이 아니라 "a 다음에 b가 반복"이라는 뜻이에요. a는 딱 하나고, +는 오직 b에만 걸려요. 초보가 여기서 정말 많이 헷갈려요. 만약 "ab라는 묶음 전체"를 반복시키고 싶다면 소괄호로 묶어 (ab)+처럼 써야 하는데, 이 묶는 이야기는 그룹을 다룰 때 자세히 나오니 지금은 "수량자는 바로 앞 하나에만 붙는다"만 확실히 기억하세요.
07.2 별표 *는 "없어도 된다"는 뜻이에요
먼저 별표(*)예요. "0번 이상"이라는 게 무슨 뜻인지 감이 잘 안 오실 텐데, 핵심은 앞 글자가 아예 없어도 매치가 된다는 거예요. a 뒤에 b가 몇 개든, 심지어 0개여도 다 잡아보는 패턴 ab*를 볼게요.
import re
re.findall(r"ab*", "a ab abb abbb ac")
# ['a', 'ab', 'abb', 'abbb', 'a']
하나씩 볼게요. 그냥 a는 b가 0개인데도 잡혔어요. *가 0개를 허용하니까요. ab는 b가 하나, abb는 둘, abbb는 셋이라 다 잡혔고요. 맨 끝의 "ac"에서는 뭐가 잡혔을까요? a 하나만 잡혔어요. a 뒤의 c는 b가 아니니까 b는 0개로 치고, a만 매치된 거죠. 그래서 결과에 a가 두 번 나온 거예요. *의 "0개여도 OK"라는 성질이 이 예제에 다 담겨 있어요.
07.3 플러스 +는 "최소 하나는 있어야" 해요
다음은 플러스(+)예요. 별표와 딱 하나만 달라요. +는 최소 한 개는 반드시 있어야 매치돼요. 0개는 허용하지 않는 거죠. 방금과 똑같은 대상에 * 대신 +만 넣어볼게요.
re.findall(r"ab+", "a ab abb abbb ac")
# ['ab', 'abb', 'abbb']
결과가 확 줄었죠? ab, abb, abbb만 잡혔어요. 아까 *에서는 잡히던 외톨이 a(맨 앞의 a와 ac의 a)가 이번엔 전부 빠졌어요. 왜냐하면 +는 b가 최소 하나는 있어야 하는데, 그 a들은 뒤에 b가 없거든요. 그래서 매치 자체가 성립하지 않은 거예요. *와 +의 차이가 딱 이거예요. 0개를 허용하느냐 마느냐. 이 한 끗 차이가 결과를 이렇게 바꿔놔요.
07.4 물음표 ?는 "있거나 없거나"예요
세 번째는 물음표(?)예요. ?는 0번 또는 1번, 그러니까 "있어도 되고 없어도 되지만 있어봤자 하나"라는 뜻이에요. 선택적인 글자를 표현할 때 딱이에요. 대표적인 예가 미국식 color와 영국식 colour예요. u가 있을 수도, 없을 수도 있잖아요. 이걸 한 패턴으로 잡아볼게요.
re.findall(r"colou?r", "color colour")
# ['color', 'colour']
패턴 colou?r을 뜯어보면, u? 부분이 "u가 있어도 되고 없어도 된다"는 뜻이에요. 그래서 u가 없는 color도, u가 하나 있는 colour도 둘 다 잡혔어요. 이렇게 ?는 "이 글자는 선택 사항이야"라고 표시하는 도구예요. 철자가 갈리는 단어나, 있어도 그만 없어도 그만인 기호(예를 들어 전화번호의 붙임표)를 다룰 때 정말 자주 써요.
07.5 *와 +는 뭐가 결정적으로 다를까요?
이 둘의 차이가 워낙 중요하니까 다른 예제로 한 번 더 못을 박을게요. 글자 수가 유동적인 단어를 생각해봐요. gogle, google, gooogle처럼 가운데 o의 개수가 제각각인 경우요. 먼저 *로 잡아볼게요. go*gle은 "g 다음에 o가 0개 이상, 그다음 gle"이라는 뜻이에요.
re.findall(r"go*gle", "ggle gogle google gooogle")
# ['ggle', 'gogle', 'google', 'gooogle']
네 개가 전부 잡혔어요. 특히 ggle은 o가 0개인데도 잡혔죠? *가 0개를 허용하니까요. 이제 똑같은 대상에 +를 써볼게요. go+gle은 "o가 1개 이상"이라는 뜻이에요.
re.findall(r"go+gle", "ggle gogle google gooogle")
# ['gogle', 'google', 'gooogle']
이번엔 ggle이 빠졌어요. o가 0개라 "1개 이상"이라는 조건을 못 채웠거든요. 나머지 셋은 o가 하나 이상이라 그대로 잡혔고요. 같은 자리에서 *는 ggle을 잡고 +는 못 잡는다는 것, 이게 두 수량자의 결정적 차이예요. "0개를 허용할 것인가"를 스스로에게 물어보면 *와 + 중 뭘 쓸지 바로 답이 나와요.
07.6 수량자를 정리하면요?
![[정규식 07] 개수를 세는 수량자, * + ?](https://img.thenullpage.com/posts/6591/6591_1_5b664f.webp)
오늘 배운 세 수량자를 마지막으로 정리해 볼게요. 셋 다 바로 앞 요소의 반복 횟수를 정한다는 공통점이 있어요.
*는 0번 이상이에요. 앞 요소가 없어도 매치가 성립하죠. go*gle이 ggle까지 잡은 게 그 증거였어요. +는 1번 이상이에요. 최소 하나는 있어야 해서, +는 o 없는 ggle을 버렸죠. ?는 0번 또는 1번이에요. 선택적인 글자를 표현할 때 쓰고, colou?r이 color와 colour를 한 번에 잡은 게 좋은 예였어요.
실무에서 가장 손이 많이 가는 건 사실 +예요. "뭔가 하나 이상 있는 덩어리"를 잡는 일이 워낙 흔하거든요. 숫자 덩어리 \d+, 단어 덩어리 \w+처럼요. *는 "아예 없어도 되는" 특수한 경우에, ?는 "선택적인 글자"가 있을 때 골라 쓰면 돼요. 처음엔 셋을 두고 고민되겠지만, "없어도 되나? 최소 하나는 있어야 하나? 있어봤자 하나인가?" 이 세 질문만 스스로에게 던지면 어떤 수량자를 쓸지 답이 바로 나와요.
가장 헷갈리는 *와 +는 "0개를 허용하느냐"만 기억하면 절대 안 헷갈려요. 없어도 되면 *, 최소 하나는 있어야 하면 +예요. 이 수량자들은 앞으로 나올 거의 모든 패턴의 핵심 재료라, 지금 확실히 다져두면 정말 든든해요. 오늘 예제들을 직접 쳐보면서 수량자만 살짝 바꿔 결과가 어떻게 달라지는지 꼭 눈으로 확인해보세요. 그 감각이 손에 붙으면 원하는 만큼만 정확히 잡는 패턴을 자유자재로 만들 수 있게 될 거예요.