티스토리 뷰
알고리즘을 공부하면서 문자열 처리와 관련된 문제를 많이 접하게 되었습니다. 패턴을 찾고 문자열을 변경하는 데 있어 하나씩 찾아 탐색하기에는 코드의 가독성도 떨어지고 실수도 종종 존재하였습니다. 이를 간편히 처리하기 위해서는 정규표현식
에 대한 이해와 숙련이 필요했고 이를 정리해보고자 합니다.
정규 표현식
정규 표현식
이란 특정한 규칙을 가진 문자열의 집합을 표현하는 데 사용하는 형식 언어입니다. 문자열을 다루는 데 굉장히 유용하기 때문에 여러 프로그래밍 언어에서 이를 지원하고 있습니다.
문법이 일반적인 프로그래밍 언어에 비해 굉장히 난해하여서 지속적인 시간 투자가 필요합니다. 저도 정규 표현식이 무엇인지 알고 있었지만, 학습 어려움으로 인해 미루고 있었던 내용입니다.
컴파일러의 파서 부분에는 정규 표현식이 반드시 들어가기 때문에 유닉스 계열 운영체제에서 CLI 환경을 주로 사용하는 경우 필수적으로 학습이 필요합니다.
Java 정규 표현식
자바 역시 정규 표현식 작성을 위해 많은 유틸리티를 제공하고 있습니다. 주로 java.util.regex
패키지에 있는 클래스를 사용하게 됩니다. 대표적인 클래스로 Pattern
과 Matcher
클래스가 존재합니다. 이에 대해 하나씩 알아보도록 하겠습니다.
그전에 정규 표현식의 패턴에서 선행적으로 알아야 할 정규 표현식 문법을 먼저 정리해보도록 하겠습니다.
정규 표현식 문법
대표적으로 알려진 것과 주로 사용되는 것들을 정리하면 다음과 같습니다.
^ | 문자열의 시작 |
$ | 문자열의 끝 |
. | 임의의 한 문자 |
* | 문자가 0개 이상 존재 |
+ | 문자가 1개 이상 존재 |
? | 0개나 1개의 문자 |
[] | 문자의 집합 내부의 ^는 NOT을 의미 |
{} | 횟수 또는 범위 |
() | 하나의 문자로 인식 |
| | 패턴에서 OR을 의미 |
\ | 확장된 문자의 시작 |
\s | 공백 문자 |
\S | 공백이 아닌 문자 |
\d | 숫자 문자 |
\D | 숫자가 아닌 문자 |
\w | 알파벳, 숫자, 언더바(_) 문자 |
\W | 알파벳, 숫자, 언더바(_)가 아닌 문자 |
Pattern
정규 표현식의 컴파일된 표현을 위한 클래스입니다. 문자열로 지정된 정규식을 통해 클래스의 인스턴스를 생성 후 해당 정규식에 일치하는 결과를 가지는 Matcher
인스턴스를 생성할 수 있습니다. 대표적인 메서드들은 아래와 같습니다.
compile()
static Pattern pattern(String regex)
함수 시그니처는 다음과 같습니다. 매개변수로 받은 정규식을 통해 패턴으로 컴파일한 Pettern
객체를 반환합니다. static
함수이기에 별도의 인스턴스 생성 없이 사용할 수 있습니다.
matcher()
Matcher matcher(String text)
패턴을 찾고자 하는 문자열을 매개변수로 하여 정규식 일치 결과가 있는 Matcher
객체를 반환합니다.
pattern()
String pattern()
컴파일된 정규식 표현을 문자열로 반환합니다.
split()
String[] split(String text)
해당하는 패턴을 기준으로 분리한 문자열 배열을 반환합니다.
Matcher
주어진 패턴을 해석하여 문자열에서 일치 연산을 수행하는 클래스입니다. Matcher
객체는 Pattern
을 통해 생성될 수 있습니다. 대표적인 메서드들은 아래와 같습니다.
find()
boolean find()
패턴에 일치하는 다음 시퀸스를 찾아 존재한다면 true
, 존재하지 않으면 false
를 반환합니다.
group()
String group()
패턴에 일치하는 다음 시퀀스 문자열을 반환합니다.
start()
int start()
패턴에 일치하는 이전 시퀀스 문자열의 시작 인덱스를 반환합니다. 이전 시퀀스를 반환하는 점을 주의해서 사용해야 합니다.
end()
int end()
패턴에 일치하는 이전 시퀀스 문자열의 시작에서 간격을 더한 값을 반환합니다. 이전 시퀀스 문자열의 마지막 인덱스를 반환하는 것이 아니기에 주의해서 사용해야 합니다.
실습
위에서 알아보았던 정규 표현식 문법을 하나씩 작성해보고 자주 사용되는 패턴들은 정리하고자 합니다.
야구 상황 예시
5회까지 진행되는 야구 경기에서 한 팀 투수의 기록을 스트라이크(S) 개수, 볼(B) 개수를 연속해서 표현하는 문자열을 가정합니다. 그렇다면 1S2B3S4B5S6B10S9B7S6B
과 같은 문자열을 확인할 수 있습니다. 회마다 기록을 알기 위해 문자열을 분리하고 싶습니다. 그러기 위해서는 다음과 같은 방법을 사용할 수 있습니다.
S와 B 문자는 필수적으로 포함될 것이고 개수는 한 자릿수부터 많으면 세 자리(최대라고 가정)까지 나타날 수 있음을 알 수 있습니다.
public static void main(String[] args) {
Pattern pattern = Pattern.compile("[\\d]{1,}S[\\d]{1,}B");
Matcher matcher = pattern.matcher("1S2B3S4B5S6B10S9B7S6B");
int count = 0;
while(matcher.find()) {
count += 1;
System.out.println(count + "회 : " + matcher.group());
System.out.println("start : " + matcher.start());
System.out.println("end : " + matcher.end());
}
}
// 실행 결과
1회 : 1S2B
start : 0
end : 4
2회 : 3S4B
start : 4
end : 8
3회 : 5S6B
start : 8
end : 12
4회 : 10S9B
start : 12
end : 17
5회 : 7S6B
start : 17
end : 21
S와 B를 포함하고 S와 B 앞에 숫자가 1개 이상 존재하는 패턴을 찾아 Matcher
의 연산을 통해 결과를 출력하고 있습니다.
조금 응용해보도록 하겠습니다. 기계 오류로 인해 S와 B 앞에 숫자가 존재하지 않는 문자가 존재할 수 있습니다. 이런 데이터를 잘 걸러내서 출력하는지 알아보도록 하겠습니다.
public static void main(String[] args) {
Pattern pattern = Pattern.compile("[\\d]+S[\\d]+B");
Matcher matcher = pattern.matcher("1S2B3S4B5S6B10S9B7S6BSB");
int count = 0;
while(matcher.find()) {
count += 1;
System.out.println(count + "회 : " + matcher.group());
System.out.println("start : " + matcher.start());
System.out.println("end : " + matcher.end());
}
}
문자열 마지막에 SB 문자를 추가하였습니다. +
는 문자가 1개 이상 존재함을 뜻하는 문법이기 때문에 S와 B 앞에 숫자가 존재하는 패턴만 출력하므로 위와 동일한 결과를 얻을 수 있습니다. +
를 문자가 0개 이상 존재함을 뜻하는 *
로 변경하게 되면 숫자가 존재하지 않는 SB까지 출력하게 됩니다.
휴대폰 번호 예시
휴대폰 번호 또한 하나의 패턴으로 볼 수 있습니다. 일반적으로 휴대폰 번호는 010-1234-5678
과 같은 형태로 존재하게 됩니다. 숫자 3개, 4개, 4개가 존재하고 그 숫자 사이에 -
가 포함되어 있습니다. 이를 Pattern
과 Matcher
를 이용해 올바른 패턴일 경우에만 출력하도록 작성해보았습니다.
public static void main(String[] args) {
Pattern pattern = Pattern.compile("[\\d]{3}-[\\d]{4}-[\\d]{4}");
Matcher matcher = pattern.matcher("010-1234-5678");
while(matcher.find()) {
System.out.println("전화번호 : " + matcher.group());
System.out.println("start : " + matcher.start());
System.out.println("end : " + matcher.end());
}
}
숫자 3개, -, 숫자 4개, -, 숫자 4개의 패턴을 위와 같이 표현할 수 있습니다. 숫자 3개, 4개, 4개는 고정되어 등장해야 하기에 개수 지정이 필수적으로 들어가야 합니다.
문자 처리 예시
public static void main(String[] args) {
Pattern pattern1 = Pattern.compile("[\\s]");
String[] array1 = pattern1.split("cat dog pig");
for (String s : array1) {
System.out.println(s);
}
System.out.println("----------");
Pattern pattern2 = Pattern.compile("[\\W]");
String[] array2 = pattern2.split("o_o&v_v#u_u");
for (String s : array2) {
System.out.println(s);
}
}
// 실행 결과
cat
dog
pig
----------
o_o
v_v
u_u
첫 번째 예시는 공백 문자 패턴을 기준으로 문자열을 분리하여 String 배열을 반환하는 것이고, 두 번째 예시는 영문자, 숫자, 언버바(_)가 아닌 문자 패턴을 기준으로 문자열을 분리하여 String 배열을 반환하는 예시입니다.
replaceAll
문자열에서 패턴을 찾아 다른 문자로 치환하는 것은 알고리즘 문제에서 많이 확인할 수 있는 유형입니다. String
클래스에는 replaceAll
메서드가 존재하는데 정규식을 적용해 해당 패턴에 맞는 문자를 모두 간편하게 치환할 수 있습니다.
public static void main(String[] args) {
String text = "1-d2nk-df3";
System.out.println(text.replaceAll("-[a-z]", "S"));
}
// 실행 결과
1S2nkSf3
조금 간단한 예시지만 주어진 문자열에서 -
뒤에 영어 소문자가 존재하는 패턴을 찾아 S
로 바꾸는 코드입니다.
마치며
직접 해보지 않고 보기에 난해하다는 이유로 학습을 미뤄뒀었는데 직접 예시를 만들어보며 코드를 작성해보니 그렇게까지 어려운 내용은 아니였다는 생각이 들었습니다. 저와 같은 생각으로 학습을 미루고 계신 분들이 있으시다면 한번 부딪혀 보시는 것을 추천해 드립니다.
간단한 패턴에 대해서만 처리해보았기에 앞으로 많은 문자열을 접해보며 패턴이 존재하게 되는 문자열이라면 자연스럽게 처리할 수 있도록 의식해서 자주 사용할 예정입니다.
'Java' 카테고리의 다른 글
[Java] JDBC 개념 정리 (4) | 2023.06.08 |
---|---|
[Java] 상속을 언제 사용해야 할까 (8) | 2023.03.26 |
[Java] 자바의 특징 (0) | 2022.11.26 |
방어적 복사에서 clone은 안전한가 (0) | 2022.07.12 |
Java Enum (0) | 2022.06.16 |