지난 글에 이어 Kotlin에서 정규 표현식 사용하는 법에 대해 이어 정리해보았다.
1. Kotlin에서 정규 표현식 사용하기
1-1. 그룹핑
지난 번에 정리해두었던 matchAt(), MatchEntire(), find(), findAll() 같은 함수를 이용하면 MatchResult의 인스턴스가 반환된다고 했었는데, 이 MatchResult의 속성중에 groups와 groupValues 라는 것이 있다.
groups 속성을 이용하면 매치된 부분 전체와 소괄호 안에 들어있는 그룹핑한 부분의 MatchGroup들을 리스트로 얻을 수 있다.
groupValues는 리스트로 0번 인덱스에는 매치 전체가, 그 다음 인덱스부터는 서브 패턴() 안에 있는 그룹핑이 되어있는 부분이 들어가게 된다.
아래 예시를 보면 이해가 조금 더 쉽다.
fun main() {
val text = "Hi Max. Hi Bob. Hi Paul. Hi Jake. Hi Emma."
val reg = Regex("""(Hi) (.*?)[.]""")
val matches = reg.findAll(text)
println(matches.map { it.groups }.joinToString())
/* [MatchGroup(value=Hi Max., range=0..6), MatchGroup(value=Max, range=3..5)],
[MatchGroup(value=Hi Bob., range=8..14), MatchGroup(value=Bob, range=11..13)],
[MatchGroup(value=Hi Paul., range=16..23), MatchGroup(value=Paul, range=19..22)],
[MatchGroup(value=Hi Jake., range=25..32), MatchGroup(value=Jake, range=28..31)],
[MatchGroup(value=Hi Emma., range=34..41), MatchGroup(value=Emma, range=37..40)] */
// groups 속성을 이용하면 매치된 부분 전체와 그룹핑한 부분의 MatchGroup들을 리스트로 얻을 수 있다.
println(matches.map { it.groupValues }.joinToString())
// [Hi Max., Max], [Hi Bob., Bob], [Hi Paul., Paul], [Hi Jake., Jake], [Hi Emma., Emma]
// 0번 인덱스에는 매치 전체가, 그 다음 인덱스에는 ()의 부분이 들어있다
val names = matches.map { it.groupValues[1] }.joinToString()
println(names) // Max, Bob, Paul, Jake, Emma
// 1번 인덱스에 담긴 이름들만 출력할 수 있다
println(matches.map { it.groups[1]?.value }.joinToString()) // Max, Bob, Paul, Jake, Emma
// groups에서도 괄호 안의 해당하는 부분만 값을 뽑아낼 수 있다
}
() 소괄호로 그룹핑을 할 때 소괄호 안에 ?<그룹이름>을 맨 앞에 넣어주면 그룹에 이름을 붙일 수 있다.
이렇게 이름을 붙여주면 이름으로 MatchGroup을 검색할 수 있다.
이 네이밍 기능은 코틀린 1.9 버전부터 지원하니 주의해야 한다.
groups에서 이름으로 MatchGroup을 가져오려면 get() 함수를 쓰거나 대괄호[]를 이용해야 한다. 대괄호를 이용할 때는 강제로 null을 허용하지 않는 타입으로 변환해야하기 때문에 매치가 없어서 null이 반환되었을때 사용하면 NPE가 발생한다.
fun main() {
val text = "1.2.3456.7.8.90.1.2.3.456"
val reg = """(?<first>\d)[.]\d[.](?<second>\d)""".toRegex()
val reg2 = """(?<first>\d)[.]\d[.](?<second>\d)/W/W""".toRegex()
println(reg.find(text)?.value) // 1.2.3
println(reg.find(text)?.groups)
// [MatchGroup(value=1.2.3, range=0..4), MatchGroup(value=1, range=0..0), MatchGroup(value=3, range=4..4)]
println(reg.find(text)?.groupValues) // [1.2.3, 1, 3]
println(reg.find(text)?.groups?.get("first"))
// MatchGroup(value=1, range=0..0)
// "first"에 해당하는 그룹의 매치 그룹이 반환된다
println(reg.find(text)!!.groups["second"])
// MatchGroup(value=3, range=4..4)
// 마찬가지로 "second"에 해당하는 그룹의 매치 그룹이 반환된다
println(reg.find(text)?.groups?.get("first")?.value) // 1
println(reg.find(text)?.groups?.get("first")?.range) // 0..0
// "first"에 해당하는 값과 인덱스 범위를 출력할 수 있다.
println(reg.find(text)!!.groups["second"]?.value) // 3
println(reg.find(text)!!.groups["second"]?.range) // 4..4
// "second"에 해당하는 값과 인덱스 범위를 출력할 수 있다.
println(reg2.find(text)!!.groups["first"]) // NullPointerException
/* 매치가 없어 reg2.find(text)는 null이 되지만 !!로 강제로 non-nullable로 변환시켰기 때문에
null을 non-nullable로 변환시킬 수는 없기 때문에 NPE가 발생한다 */
}
그룹핑한 것을 구조 분해하는 것도 가능하다.
구조 분해에 관해서는 공식문서가 있으니 참고해보면 좋을 것 같다.
https://kotlinlang.org/docs/destructuring-declarations.html
Destructuring declarations | Kotlin
kotlinlang.org
구조 분해시에는 null을 허용하지 않는 변수만 담을 수 있기 때문에 !! 연산자를 사용해야한다. 구조 분해하여 리스트로 만드는 것도 가능하다.
fun main() {
val inputString = "John 9731879"
val match = Regex("""(\w+) (\d+)""").find(inputString)!!
val (name, phone) = match.destructured
println(name) // John
// 첫 번째 그룹인 (\w+)가 분해되어 출력된다
println(phone) // 9731879
// 두 번째 그룹인 (\d+)가 분해되어 출력된다
val numberedGroupValues = match.destructured.toList()
// 구조 분해된 그룹 리스트는 오직 그룹핑된 부분만 요소로 가진다.
// 매치된 전체 문자열은 리스트에 포함되지 않는다.
println(numberedGroupValues) // [John, 9731879]
}
1-2. 치환하기
정규식을 만족하는 부분을 다른 부분으로 바꿀 수 있다. 이런 치환 작업을 하려면 replace(), replaceFirst() 함수를 사용하면 된다. 인자로 받은 문자열이 정규식을 만족하는 부분에 대신 들어가게 된다.
replace() 함수는 두 개의 인자를 받는다. 치환 작업을 하고 싶은 문자열(교체하고 싶은 부분이 있는 문자열)과 교체하고 싶은 부분에 대신 들어가게 될 문자열이 들어간다. 이 인자로 받은 문자열이 정규식을 만족하는 부분에 대신 들어가게 된다. replace()의 경우는 전체 문자열에서 정규식을 만족하는 모든 부분이 치환되게 된다.
replaceFirst()는 위와 동일하지만 오직 전체 문자열에서 첫 번째 매치되는 곳만 치환되게 된다.
직접 예시로 확인해 보는것이 이해가 쉽다.
fun main() {
val regex = """(011|016|017|018)-""".toRegex()
val oldnumber = "011-1234-5678, 016-2345-6789, 017-3456-7890, 018-4567-8901"
val newnumber = regex.replace(oldnumber, "010-")
println(newnumber)
// 010-1234-5678, 010-2345-6789, 010-3456-7890, 010-4567-8901
// "011-", "016-", "017-", "018-"에 해당하는 부분이 "010-"로 바뀌었음을 볼 수 있다.
val newnumber2 = oldnumber.replace(regex, "010-")
println(newnumber2)
// 010-1234-5678, 010-2345-6789, 010-3456-7890, 010-4567-8901
// 정규식과 문자열의 위치를 바꿔도 치환된 문자열을 똑같이 반환 받을 수 있다.
val firstonly = regex.replaceFirst(oldnumber, "010-")
println(firstonly)
// 010-1234-5678, 016-2345-6789, 017-3456-7890, 018-4567-8901
// 맨 처음 정규식과 매치되는 부분만 변경되었음을 볼 수 있다.
}
1-3. 분할하기
split() 함수를 이용하면 문자열을 정규식을 만족하는 부분을 기준으로 분할 할 수 있다. 정규식을 만족하는 부분은 리스트의 요소에 들어가지 않는다.
예시를 보면 이해가 쉽다.
fun main() {
val regex = """melong""".toRegex()
val numbers = "1melong2melong3melong4melong5melong6melong7melong8melong9"
println(regex.split(numbers))
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 정규식을 만족하는 부분을 기준으로 분할된다.
// 정규식을 만족하는 부분은 포함되지 않는다.
}
여기까지 코틀린에서 정규 표현식 사용하는 법을 정리해보았다. 언어마다 정규식 사용법이 다 다르기 때문에 주로 사용하는 언어에 대해서 한 번 정리해두면 나중에 참고하기 편할 것이다. 또 코틀린에서 얼마나 다양한 기능을 지원하는지도 알 수 있어서 필요한 경우 찾아서 사용하면 좋을 것 같다.
'Kotlin' 카테고리의 다른 글
Kotlin에서의 동등성과 동일성 (0) | 2024.12.30 |
---|---|
Kotlin에서 정규 표현식 사용하기 (1) (0) | 2024.12.20 |
Kotlin의 원시배열과 참조배열 (0) | 2024.12.16 |