Atomic Habits

[JAVA] 정규식/정규표현식 본문

IT/Regular Expressions

[JAVA] 정규식/정규표현식

체계성 2021. 11. 22. 22:28

https://offbyone.tistory.com/400 : 전반적인 정리 참조

https://hermeslog.tistory.com/310 : 특수문자 처리 / 플래그(Pattern.MULTILINE (?m)) 참조

http://egloos.zum.com/sweeper/v/3064808 : 예문 참조

https://ohgyun.com/392 : 단어 경계(\b) 참조

1. 정규표현식에 쓰이는 특수문자

 

1. '.'
임의의 한 문자

ex)
s.e -> sae, sbe, sce, sde, ...
.ce -> ace, kce, dce, ...

***  a[.]b는 a와 b 사이에 ~~임의의 한 문자~~가 아닌 실제 Dot . 을 찾는다.**
 

2. '*'
바로 앞의 문자가 없거나 하나 이상

ex)
s*e -> e, se, see, ssse, ...
abc* -> ab, abc, abcc, abccc, ...
h*im -> im, him, hhim, hhhim, ...

 

3. '+'
바로 앞의 문자가 하나 이상

ex)
s+e -> se, sse, ssse, ...

 

4. '?'
바로 앞의 문자가 없거나 하나

ex)
th?e -> e, the 이 두가지표현이 유일하겠지.

 

5. '^'
바로 뒤의 문자열로 시작.

ex)
^The  -> The girl is, Theather, ... (뒷부분부터 공백까지 검사)
^a?bc -> bc, abc, ...
^.e -> he, me, request, settle, ...
^s.e? -> sa, sae, sb, sbe, ... (e는 나와도 되고 안나와도 되고)

 

6. '$'
바로 앞의 문자열로 종료

ex)
a?bc$ -> eeabe, seebc, bc, ...
+.e$ -> onthetoe, bctae, appetittle, ...
s?c+$ -> e, se, ee, eee, seee, seee, ...

 

7. '[]'
[] 안에 있는 문자 중 하나, 범위는 '-'로 지정

ex)
[ab]cd -> acd, bcd, ...
[a-z] -> 영문 소문자 (a부터 z까지)
[a-zA-Z] -> 영문자(대소문자)
[0-9] -> 0부터 9까지의 숫자
ag[a-z] -> aga, agbcd, agzzz, ...

 

^ab[cd]ef -> abcef, abdef, ...
^[a-zA-Z] -> 영문자로 시작
^[a-zA-Z_] -> 아이디 검사할 때 첫글자가 영문자와 '_' 만 쓰도록 할때
^[가-힣]  ->  한글로 시작해야 할 때
[^a-zA-Z0-9]  ->  ^이 안으로 들어가면 제외(부정)의 의미가 된다. 영문자나 숫자로 시작할 수 없을 때
[a-zA-Z0-9]$ -> 영문자나 숫자로 종료

 

[가-힣]  ->  한글(완성형)만 가능. ㅋㅋㅋ 같은 문자는 제외
[abc]  -> 이 안에 있는 문자중에 하나. 즉, a b c 중에 하나의 문자.

 

8. '{}'
{} 앞의 문자나 문자열 출현 횟수

ex)
a{2}b -> aab, ... a가 꼭 2번 나와야 한다는 뜻.
a{2,}b -> aab, aaab, aaaab, ...  a가 최소 2번 이상 나오도록 하라는 뜻.
a{2, 3}b -> aab, aaab, ... a는 최소 2번 최대 3번 나오도록 하라는 뜻.

 

9. '()'
()안에 있는 문자를 그룹화

ex)
a(bc){2} -> abcbc, ... a다음 bc가 2번 나와야 한다는 뜻..
a(bc)* -> abcbcbc, ...  a다음 bc의 출현횟수는 무한대가 가능하다는 뜻.

 

10. '|'
or 연산자

ex)
he|she -> he, she is, ...
(he|she)is -> heis, sheis, ...

 

11. 특수 문자 사용
^  []  $  ()  |  *  +  ?  {}  \\
앞에 \\ 붙여서 사용해야함

ex)
\\*+ : * 가 하나 이상 포함된 패턴
\\d : 순수한 숫자, 정수값, 0-9
\\d{2,3}-/d{3,4}-/d{4}  :   전화번호 정규식. -? 하이퍼뒤에 물음표가 있으면 하이퍼가 있어도 되고 없어도 된다는 뜻.
\\D : \\d와 반대 (숫자를 제외한 나머지)
\\w : [a-zA-Z0-9] 의 줄임표현.
\\W : [^a-zA-Z0-9] 영문자와 숫자만 아니면 된다는 뜻.
\\s : 공백 문자
\\S : \\s와 반대 (공백 문자를 제외한 나머지)

2. 정규식 사용법

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegExr {

	public RegExr() {
		// TODO Auto-generated constructor stub
	}

	public static void main(String[] args) {

		// 기본 정규식 사용
		String target = "자유석 시간권(10시간);자유석 기간권(4주);5인실 일회권(3시간)"; //
		String regEx = ".{3}\\\\s.{5,9}\\\\)+";

		Pattern pat = Pattern.compile(regEx);		
		Matcher mat  = pat.matcher(target);
		
		while (mat.find()) {
			System.out.println(mat.group()); // 매치된 문자열 반환
		}

		// 특정 문자열을 대체하는 정규식
		String removeRegEx = ".{3}\\\\s기.{5,9}\\\\)+;?"; // 비기간권만 남기기 replaceAll
		String removeRegEx = ".{3}\\\\s(?!기).{5,9}\\\\);?"; // 기간권만 남기기 replaceAll

		System.out.println(target.replaceAll(removeRegEx , ""));
		System.out.println(target.replaceAll(removeRegEx , "").split(";").length);;
		String[] ary = target.replaceAll(removeRegEx , "").split(";");

		System.out.println(ary[0]);
		System.out.println(ary[1]);
	}
}

* 반환하는 타입
group() : 매치된 문자열 자체
start() : 매치된 문자열 시작 위치
end()   : 매치된 문자열 종료 위치
span()  : 매치된 문자열 (시작, 종료) 위치 튜플

3. 정규식 함수 ( 일치 여부, 추출, 치환 )

자바 정규식 기본정리 : Matcher, Pattern, find(), group()

정규식을 사용하면 문자열(String)이 특정 패턴과 일치하는지 여부를 확인하거나, 패턴에 맞는 값을 찾아내거나, 해당 값을 새로운 값으로 바꿀 수 있다.

이 방법이 일목요연하게 작성되어 있는 곳이 마땅히 보이지 않았기에 직접 정리해서 써본다.

1. matches (일치하는지 확인)

target 은 대상이 되는 문자열(문장)을 담는 변수이고, regEx는 정규식(Regular Expression) 을 담는 변수라고 해보자.

public void isEqualRegEx() {
    String target = "나는 2008년도에 입학했다.";
    String regEx = ".*\\\\d{1}.*";
    // String regEx = ".*[0-9].*"; 와 동일함
  
    if (target.matches(regEx)) {
        System.out.println("일치");
   
    } else {
        System.out.println("불일치");
    }
}

여기서 regEx 는 ".*\\\\d{1}.*"; 이다. 즉 여기서 target.matches(regEx) 는 숫자가 1개라도 포함되어 있느냐고 묻는 것이다.
(".*" 는 모든 복수의 문자이고, "\\\\d{1}"는 한 자리 숫자이므로.)

이 경우 2008, 이렇게 숫자가 4개나 있으니까 당연히 "일치"가 출력되겠지.
만약 regEx 값이 바뀐다면 어떨까?

regEx == ".*\\\\d{1}.*"  -> 일치
regEx == ".*[0-9].*"  -> 일치 ([0-9]는 \\\\d{1} 와 정확히 같은 뜻임)

regEx == ".*[0-9][0-9][0-9][0-9].*" -> 일치 (숫자 4자리)
regEx == ".*[0-9][0-9][0-9][0-9][0-9].*" -> 불일치 (숫자 5자리)

regEx == ".*\\\\d{1,4}.*" -> 일치 (숫자 1자리 이상 4자리 이하)
regEx == ".*\\\\d{2,5}.*" -> 일치 (숫자 2자리 이상 5자리 이하)
regEx == ".*\\\\d{5,6}.*" -> 불일치 (숫자 5자리 이상 6자리 이하)

요런 식으로 되겠다.

2. replaceAll (패턴에 맞는 값을 새로운 값으로 치환)

public void replaceRegEx() {
    String target = "나는 2008년도에 입학했다.";
    String regEx = "[0-9]";
    Pattern pat = Pattern.compile(regEx);
  
    Matcher m = pat.matcher(target);
    String result = m.replaceAll("2"); // 패턴과 일치할 경우 "2"로 변경
    
    System.out.println("출력 : " + result);
    // 출력 : 나는 2222년도에 입학했다.
}

여기서 regEx 변수 값은 "[0-9]" 이다. 그러니까 위 소스는 1자리 숫자들을 "2"로 치환하게 된다.
결과는 당연히 "나는 2222년도에 입학했다."가 된다.

위 함수를 3줄로 줄이면 아래와 같다.

public void replaceRegEx() {
    String target = "나는 2008년도에 입학했다.";
    String regEx = "[0-9]";
    System.out.println("출력 : " + target.replaceAll(regEx, "2"));
}

String 객체에 replaceAll 라는 멤버함수가 있다. 이게 더 낫다.
전자는 Pattern 객체와 Matcher 객체를 추가로 임포트하는데, 후자는 String 객체만 있으면 되니까.

3-1. find(), group() (패턴에 맞는 값 1개씩 찾아내기)

find랑 group이라는 함수가 특이한데 Matcher 의 멤버함수다.

public void findRegEx(){
    String target = "나는 2008년도에 입학했다.";
    String regEx = "[0-9]";

    // 정규식(regEx)을 패턴으로 만들고,
    Pattern pat = Pattern.compile(regEx);
    // 패턴을 타겟 스트링(target)과 매치시킨다.
    Matcher match = pat.matcher(target);

    System.out.println(match.find());  // true
    System.out.println(match.group()); // 2
    
    System.out.println(match.find());  // true
    System.out.println(match.group()); // 0
    
    System.out.println(match.find());  // true
    System.out.println(match.group()); // 0
    
    System.out.println(match.find());  // true
    System.out.println(match.group()); // 8
    
    System.out.println(match.find());  // false
    System.out.println(match.group()); // 에러 발생! (IllegalStateException)
}

Pattern에 compile로 정규식(regEx)을 담고, Matcher에 타겟 스트링(target)을 담는게 먼저다.

그 다음 Matcher의 find() 함수를 쓰면 1번째 값을 찾아내고, true 혹은 false를 반환한다.
group() 을 쓰면 방금 찾은 1번째 스트링이 튀어나온다.

다시 find()를 쓰면 2번째 값을 찾고, group()을 쓰면 2번째 값이 튀어나오고... 이런 식이다.

보면 2, 0, 0, 8 까지 잘 가다가 (숫자가 더 이상 없으므로 당연히) 5번째에서 에러가 나는데,
따라서 에러가 나지 않도록 코드를 쓴다면 아래와 같이 작성해야 마땅하겠다.

...(전략)...

    if (match.find()) {
        System.out.println(match.group());
    }

...(후략)...

3-2. find(), group() (패턴에 맞는 값 모두 찾아내기)
3-1에서 설명한 바를 잘 정리한게 아래 메서드다.

public void findAllRegEx(){
    String target = "나는 2008년도에 입학했다.";
    String regEx = "[0-9]";

    Pattern pat = Pattern.compile(regEx); 
    Matcher match = pat.matcher(target);
    
    int matchCount = 0;
    while (match.find()) {
        System.out.println(matchCount + " : " + match.group());
        matchCount++;
    }
    System.out.println("총 개수 : " + matchCount);
    
    // 0 : 2
    // 1 : 0
    // 2 : 0
    // 3 : 8
    // 총 개수 : 4
}

4. 긍정형/부정형 전방/후방 탐색

* 긍정형은 찾아 달라는 의미이고, 부정형은 찾지 말아달라는 의미이다.

* 전방 : (괄호)를 기준으로 궁극적으로 출력하고자 하는 문자열이 앞에 있다. (?=) , (?!)
* 후방 : (괄호)를 기준으로 궁극적으로 출력하고자 하는 문자열이 뒤에 있다. (?<=) , (?<!)


* 긍정형 전방탐색 - 앞으로 찾기
일치하는 텍스트는 텍스트 자체는 소비하지(consume) 않고,  그 앞에 무엇이 오는지 출력한다.

* 긍정형 후방탐색 - 뒤로 찾기
일치하는 텍스트는 텍스트 자체는 소비하지(consume) 않고,  그 뒤에 무엇이 오는지 출력한다.

* 부정형 전방탐색 - 앞으로 찾기
불일치하는 텍스트 문자열 찾아서(소비하지 않고), 그 앞에 무엇이 오는지 출력한다.

* 긍정형 후방탐색 - 뒤로 찾기
불일치하는 텍스트 문자열 찾아서(소비하지 않고),  그 뒤에 무엇이 오는지 출력한다.

부정형 전방 탐색 (?!)   vs   (?=) 긍정형 전방 탐색
부정형 후방 탐색 (?<!)  vs   (?<=) 긍정형 후방 탐색

------------------------------------------------
예시 
문자열 : /Add_it/applylist?field=&query=&currentPage=2 -> 

(?<=\\&).+  --->  query=&currentPage=2

(?<=field=).*(?<=\\&)  --->  &query=&

(?<=field=).*(?=\\&query)  --->  ""

문자열 :  /Add_it/applylist?field=aa&query=&currentPage=2 -> 
(?<=field=).*(?=\\&query)  --->  aa

문자열 : /Add_it/applylist?field=aa&query=bb&currentPage=2
(?<=query=).*(?=\\&currentPage)  --->  bb

(?<=currentPage=)\\d* ---> 2
------------------------------------------------
* 긍정형 전방/후방 탐색
일치 조건에 포함은 시키되, 최종 결과에는 제외시켜달라는 의미이다.(=소비하지 않는다)

정규표현식 : (긍정형 전방탐색)(기본 정규표현식)
(긍정형 전방탐색) 조건과 (기본 정규표현식) 조건 동시에 만족하는 텍스트를 찾되 
											 (기본 정규표현식) 만 출력한다.

전방 (?<= 표현식)
후방 (?= 표현식 )

(?<= \\<\\w+\\>) .* (?= \\<\\/\\w+\\>)
(?<= \\<([Tt][Ii][Tt][Ll][Ee])\\>) .*? (?= \\<\\/\\1\\>)
<TITLE>SooKkaRak's homepage</TITLE>

* 부정형 전방/후방 탐색
부정형 탐색은 비교적 덜 쓰는 방법이다.

부정형 전방 탐색
정규표현식 : (부정형 전방탐색)(기본 정규표현식)
(부정형 전방탐색) 조건과는 일치하지 않고 (기본 정규표현식) 조건만 일치하는 텍스트를 찾고,

부정형 후방 탐색도 이와 비슷하게, 뒤쪽에서 지정한 패턴과 일치하지 않는 텍스트를 찾는다.

* 
부정형 전후방 탐색은 다음과 같이 사용한다.
부정형 전방 탐색 (?!)   vs   (?=) 긍정형 전방 탐색
부정형 후방 탐색 (?<!)  vs   (?<=) 긍정형 후방 탐색

즉, 긍정형 전후방 탐색의 = 대신 !를 사용하면 된다.

* 예문
서울 성북군 서성동 1번지
동읍리면 기준 앞 글자 선택 : .*[동,읍,리,면](?<!\\s)
동읍리면 기준 뒷 글자 선택 : (?<=[동]\\s).*

* 예문
다음은 긍정형 후방 탐색과 부정형 후방 탐색 사이에 어던 차이가 있는지 알아보는 예제이다.
예제 본문에 가격과 수량을 나타내는 숫자들이 있다.
우선 간단하게 가격만 얻어보자.

[예문]
I paid $30 for 100 apples, 50 oranges, and 60 pears.
I saved $5 on this order.

[긍정형 후방탐색]  (?<=\\$)  \\d+

[결과]
30
5

이번에는 가격이 아니라, 수량을 찾아보자.
[부정형 후방탐색]   (?<!\\$)  \\d+

[결과]
0 100 500 60

$30의 0 역시 $ 다음 위치가 아니기에 덩달아 같이 결과에 포함되어 버렸다.
이럴 땐 깔끔하게 단어 경계를 붙여 문제를 해결하면 되겠다.
\\b : 앞 혹은 뒤에 문자, 숫자, 특수문자 등이 오지 않고 다른 단어와 경계가 있어야 한다는 조건이다.

[부정형 후방탐색 + 단어 경계조건]   \\b  (?<!\\$)  \\d+  \\b

[결과]
100 500 60
Comments