우테코 7기/1주차

Regex 뿌시기

gilbert9172 2024. 10. 17. 22:31

 

❐ Description 


프리코스 1주차에서 Regex로 개고생했다. 이제 나의 것으로 만들자.
 
 
 
 
 

❐ Regex란?


정규 표현식(regex, regular expression, rational expression)이란, 특정한 규칙을 가진 문자열을
표현하는데 사용하는 formal_language이며 정규 표현식은 문자열의 검색과 치환할 때 사용될 수 있다.

더보기
더보기

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 quantifierNon-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