❐ Description
프리코스 1주차에서 Regex로 개고생했다. 이제 나의 것으로 만들자.
❐ Regex란?
정규 표현식(regex, regular expression, rational expression)이란, 특정한 규칙을 가진 문자열을
표현하는데 사용하는 formal_language이며 정규 표현식은 문자열의 검색과 치환할 때 사용될 수 있다.
정규 표현식이라는 문구는 일치하는 텍스트가 준수해야 하는 "패턴"을 표현하기 위해 특정한 표준의
텍스트 syntax를 의미하기 위해 사용된다. 정규 표현식의 각 문자는 메타문자로 이해되거나 정규 문자로
이해된다.
❐ Regex 문자 - 기본 기호
. | 임의의 문자 1개 |
^ | 문자열의 시작을 의미 |
$ | 문자열의 마지막을 의미 |
[ ] | 괄호 내의 문자 중 하나라도 있는 경우 |
[^] | 괄호 내의 문자는 제외 |
- | 범위를 나타냄 |
| | OR 연산자 |
\b | 단어의 경계(word boundary) |
\B | non-word boundary |
\d | 0~9 사이의 숫자 (digit) |
\D | 0~9 외의 숫자 |
\s | white-space |
\S | non-white space |
\w | 알파벳 대소문자 + 숫자 + _ |
\W | \w의 반대 |
1. word\b : word 바로 뒤에 'non-word'가 오는 경우
Input Find Match word! ✅ ❌ hello word! ✅ ❌ wordworld ❌ ❌ word ✅ ✅
2. \bword\b : word 앞 뒤로 'non-word'가 오는 경우
Input Find Match word! ✅ ❌ hello word ✅ ❌ word word ✅ ❌ helloword ❌ ❌ worldword ❌ ❌
3. word\B : word 뒤에 'word'가 오는 경우
Input Find Match word! ❌ ❌ hello word ❌ ❌ helloword ❌ ❌ wordworld ✅ ❌
1. ^gilbert$ : 정확히 일치하는 문자열 'gilbert'를 찾을 경우
Input Find Match gilbedfddrt ❌ ❌ gilbert9172 ❌ ❌ gilbertgilbertgilbert ❌ ❌ gilbert ✅ ✅
2. ^g(.*)t$ : g로 시작하고 t로 끝나는 문자열
Input Find Match gilbedfddrt ✅ ✅ gilbert9172 ❌ ❌ gilbertgilbertgilbert ✅ ✅ gilbert ✅ ✅
❐ Regex 문자 - 수량 기호
? | 바로 앞 표현식이 없거나, 최대 한개만 있는 경우 |
* | 바로 앞 표현식이 없거나, 한 번 이상 있는 경우 |
+ | 바로 앞 표현식이 한 번 이상 있는 경우 |
{n} | 정확히 n번 표현식이 반복되는 경우 |
{n,} | 정확히 n번 이상 표현식이 반복되는 경우 |
{n,m} | 정확히 n번 이상 m버 이하로 표현식이 반복되는 경우 |
1. colou?r : color이거나 colour 인 문자열 찾기
Input Find Match color ✅ ✅ colour ✅ ✅ colouur ❌ ❌ coloouur ❌ ❌
2. to*ss : o가 없을 수도 있고, 1회 이상 있는 경우
Input Find Match tss ✅ ✅ toss ✅ ✅ tooss ✅ ✅ toosss ✅ ❌
3. to+ss : o가 1회 이상 있는 경우
Input Find Match tss ❌ ❌ toss ✅ ✅ tooss ✅ ✅ toosss ✅ ❌
❐ Regex 문자 - (일반) 그룹 캡쳐 기호
( ) | Capturing |
(?:) | 찾지만 그룹에 포함되지 않음(Non-capturing) |
1. (abc) : 일반 capturing
Input Find Match abc ✅ ✅ abd ❌ ❌ abdabc ✅ ❌ abcabc ✅ ❌
2. (?:abc)+ : 일반 non-capturing
Input Find Match abcabcabc ✅ ✅ abd ❌ ❌ abdabc ✅ ❌
3. (?:cat|dog) is cute : OR 연산자를 사용한 non-capturing
Input Find Match cat is cute ✅ ❌ dog is cute ✅ ❌ bird is cute ❌ ❌
❐ Regex 문자 - (탐색) 그룹 캡쳐 기호
(?=) | 전방 탐색 |
(?<=) | 후방 탐색 |
1. 전방 탐색 (Lookahead)
전방 탐색은 정규 표현식에서 특정 패턴 뒤(오른쪽)에 어떤 조건이 충족되는지를 확인하지만, 그 조건에 해당하는
문자열은 실제 매칭 결과에는 포함하지 않는 기능이다. 즉, 매칭하려는 패턴 뒤에 특정 패턴이 있는지 검사하지만,
그 패턴을 매칭 결과에 포함시키지는 않는다. 이를 "소비하지 않는다(non-consume)."이라고 표현한다.
#️⃣ 긍정 전방 탐색 (Positive Lookahead)
표기법 : (?=기준문자)
긍정 전방 탐색은 특정 패턴 뒤(오른쪽)에 기준문자가 존재하는 경우에만 매칭된다.
예를 들어, 문자열 "15-7606"에서 15만 추출하기 위한 정규식은 [0+9]+(?=-) 이 될 것이다.
@Test
@DisplayName("연도 추출하기")
void test() {
// given
String regex = "[0-9]+(?=-)";
Pattern pattern = Pattern.compile(regex);
// when
Matcher matcher = pattern.matcher("15-7607");
// then
if (matcher.find()) {
System.out.printf("결과 = %s", matcher.group(0));
}
}
// 결과 = 15
"-"를 소비하지 않았기 때문에 결과가 15로 출력된다.
#️⃣ 부정 전방 탐색 (Negative Lookahead)
표기법 : (?!기준문자)
부정 전방 탐색은 특정 패턴 뒤(오른쪽)에 기준문자가 존재하지 않는 경우에만 매칭된다.
예를 들어, 서비스의 남성 가입자만 찾기 위한 정규식은 \d{2}-(?!F)[M][0-9]+이 될 것이다.
@Test
@DisplayName("모든 남성 가입자 아이디 조회")
void test() {
// given
String regex = "\\d{2}-(?!F)[M][0-9]+";
Pattern pattern = Pattern.compile(regex);
List<String> source = List.of("23-M001", "24-M001", "24-M002", "22-F001", "24-F001");
// when
source.forEach(user -> {
Matcher matcher = pattern.matcher(user);
if (matcher.find()) {
// then
System.out.printf("결과 = %s", matcher.group());
}
});
}
// 결과 = 23-M001
// 결과 = 24-M001
// 결과 = 24-M002
2. 후방 탐색 (Lookbehind)
후방 탐색은 특정 패턴 앞(왼쪽)에 기준문자가 있는지 확인하는 메커니즘이다.
후방 탐색은 조건만 확인하고 매칭 결과에는 포함되지 않는다. (긍정 전방 탐색과 동일)
#️⃣ 긍정 후방 탐색 (Positive Lookbehind)
표기법 : (?<=기준문자)
긍정 후방 탐색은 특정 조건 앞(왼쪽)에 기준 문자가 오는 경우에만 매칭된다.
예를 들어, naver 도메인을 사용하는 가입자를 찾기 위한 정규식은 (?<=@)naver[.]com이 될 것이다.
@Test
@DisplayName("네이버 도메인 사용자 조회")
void test() {
// given
String regex = "(?<=@)naver[.]com";
Pattern pattern = Pattern.compile(regex);
List<String> source = List.of("a@naver.com", "b@gmail.com", "c@yahoo.kr", "d@naver.com");
// when
source.forEach(email -> {
Matcher matcher = pattern.matcher(email);
if (matcher.find()) {
System.out.println(matcher.group());
}
});
}
// 결과 = naver.com
// 결과 = naver.com
긍정 후방 탐색에서도 기준 문자가 소비가 되지 않음을 확인할 수 있다.
#️⃣ 부정 후방 탐색 (Negative Lookbehind)
표현식 : (?<!기준문자)
부정 후방 탐색은 특정 조건 앞(왼쪽)에 기준 문자가 오지 않는 경우에만 매칭된다.
예를 들어, 서비스의 여성 가입자만 찾기 위한 정규식은 .*(?<!M)[0-9]{3}이 될 것이다.
@Test
@DisplayName("모든 여성 가입자")
void test() {
// given
String regex = ".*(?<!M)[0-9]{3}";
Pattern pattern = Pattern.compile(regex);
List<String> source = List.of("23-M001", "24-M001", "24-M002", "22-F001", "24-F001");
// when
source.forEach(user -> {
Matcher matcher = pattern.matcher(user);
if (matcher.find()) {
// then
System.out.printf("결과 = %s", matcher.group());
}
});
}
// 결과 = 22-F001
// 결과 = 24-F001
❐ Greedy & Non-Greedy quantifier
Greedy quantifier와 Non-greedy quantifier는 정규 표현식에서 패턴을 얼마나 많이 매칭할 것인지
결정하는 수량자(quantifier)이다. 이 두 가지는 패턴을 찾는 방식에서 차이가 있으며, 이를 통해 동일한 패턴도
서로 다른 결과를 만들어낼 수 있다.
1. Non-Greedy Quantifier
Non-greedy quantifier는 가능한 한 최소한의 문자를 매칭하려고 한다. 즉, 매칭이 가능해지는 순간 멈추고
더 이상 확장하지 않는다. Greedy 수량자 뒤에 ?를 붙이면 Non-greedy로 동작한다.
1주차 프리코스 미션에서 커스텀 구분자를 단 하나만 입력해야 하는 요구사항이 있었다.
이를 검증하기 위해서 정규식 (//.*?)\\n 을 작성하였다.
@Test
@DisplayName("커스텀 구분자 갯수 2개")
void test() {
// given
String regex = "(//.*?)\\\\n";
Pattern pattern = Pattern.compile(regex);
// when
Matcher matcher = pattern.matcher("//*\\n6*7*8//?\\n1?2?3");
// then
while (matcher.find()) {
System.out.println(matcher.group(1));
}
}
// 결과 = //*
// 결과 = //?
이와 같은 결과가 나온 이유는 Non-greedy 방식으로 regex를 작성하였기 때문이다.
Non-greedy는 최소한의 문자를 매칭하기 때문에 각각 //*\\n6*7*8, //?\\n1?2?3 을 매칭한다.
2. Greedy Quantifier
가능한 한 최대한 많은 문자를 매칭하려고 한다. 즉, 패턴이 매칭될 수 있는 범위 내에서 최대로 확장한다.
만약 non-greedy에서 작성했던 정규식을 greedy하게 (//.*)\\n로 바꾸면 어떻게 될까?
@Test
@DisplayName("커스텀 구분자 갯수 2개")
void test() {
// given
String regex = "(//.*)\\\\n";
Pattern pattern = Pattern.compile(regex);
// when
Matcher matcher = pattern.matcher("//*\\n6*7*8//?\\n1?2?3");
// then
while (matcher.find()) {
System.out.println(matcher.group(1));
}
}
// 결과 = //*\n6*7*8//?
역시 greedy는 최대한 많은 문자를 매칭하기 때문에 //*\\n6*7*8//?\\n, 단 한 개만을 매칭하게 된다.
결과적으로 위 정규식 표현은 사용자가 몇 개의 커스텀 구분자를 입력했는지 제대로 찾아 내지 못한다.
❐ Matcher & Pattern 클래스
각 클래스에 정의된 메소드는 잘 정리된 블로그를 참고.
❐ 마무리
이렇게 전체적으로 정리를 하면서 스스로 예제를 만들어보니 regex 작성하는게 재미도 있고
확실히 전 보다는 나아진 거 같다. 앞으로도 개발을 하면서 문자열 관련 해서 regex도 고려하는
습관을 가져봐야겠다.
Regex Tester
‣ https://regexr.com/
‣ https://regexper.com/
‣ https://www.regexplanet.com/
참고 블로그
‣ inpa 티스토리
‣ 민소네 깃허브
‣ HEROPYTech
'우테코 7기 > 1주차' 카테고리의 다른 글
[JUnit5] 입력 값을 mockking할 수 있을까? (0) | 2024.10.21 |
---|---|
[Java] 객체를 복사해보자 (0) | 2024.10.19 |
[Refactoring] Pattern을 캐싱하자 (0) | 2024.10.19 |
1주차 회고 (0) | 2024.10.17 |
문자열 덧셈 계산기 (0) | 2024.10.14 |