[Kotlin] 정규표현식의 개념과 코틀린에서 정규표현식 사용해보기


 

정규 표현식에 대해서 들어보신 분들도 있을테고, 완전 처음 들어보시는 분들도 있을 것입니다. 정규 표현식은 어떤 문자열을 특정 형식에 맞춰서 처리하는 데에 좀 더 수월하게 할 수 있도록 도와줍니다. 물론 안 써도 엄청 큰 지장은 없습니다. 그 문자열 형식을 하드 코딩 한다거나 등의 방법을 사용한다면요.. 그래도 있으면 좋겠죠? 정규 표현식이 뭐고, 코틀린에서 어떻게 사용하는지 알아봅시다.




정규 표현식이란?

일단 가장 정확한 설명은 역시 위키피디아죠! 좀 더 자세한 내용을 알고 싶다면 아래의 링크에서 확인이 가능합니다.

https://ko.wikipedia.org/wiki/%EC%A0%95%EA%B7%9C_%ED%91%9C%ED%98%84%EC%8B%9D

간단히 얘기하자면, 어떤 패턴과 규칙을 이용해서 특정한 문자열을 표현할 수 있도록 사용하는 식입니다. 이게 말로는 뭔지 확 와닿지 않는데, 직접 본다면 어디선가 본 듯한 느낌을 받으실 수 있을 겁니다. (아니면 말고) 아래에는 정규 표현식을 어떻게 작성하는 지에 대한 규칙을 설명했습니다. 위키피디아의 설명을 토대로 작성했으며, 뭔가 이론 공부하는 느낌이라 굉장히 귀찮지만, 아래의 내용을 이해해야 정규 표현식을 작성할 수 있습니다.


^ : 문자열의 시작을 의미합니다. 정규 표현식을 시작할 때 ^[a-z] ... 이런 식의 형태가 됩니다.


$ : 문자열의 끝을 의미합니다. 마찬가지로 ^[a-z] ... $ 이런 식의 형태가 되겠네요.


.   : 문자 한 개를 의미합니다. 즉 '.' 이 위치한 곳에 어떤 문자든지 1개의 문자가 들어간다는 얘기죠.


[ ]  : 대괄호에 있는 문자 중 한 개를 의미합니다. 가령 [abc]는 a, b, c 중에 하나를 선택한다는 것이겠네요. 이 때 대괄호 안에 '-'를 써서 [a-z] : a~z까지 중에서 한개, [0~9] : 0에서 9까지 중 하나의 의미도 표현할 수 있습니다.


[^] : '^'은 not의 의미인데, 대괄호에서 쓴다면, [^abc] : a, b, c 빼고 나머지, 즉 d~z, 0~9 등 a, b, c를 제외한 모든 문자를 의미합니다. [^a-z] : 알파벳을 뺀 나머지 문자를 의미하겠네요.


| : 이건 많이 봐서 눈치 챘을 수도 있겠지만, or을 의미합니다. a|b : a 또는 b가 되겠네요.


( ) : 공통되는 부분을 묶을 때 등 서브 패턴을 지정할 때 사용할 수 있습니다. abc|abd -> ab(c|d)로 바꿀 수 있습니다. 이렇게 하면 ab(c) 또는 ab(d), 즉 abc|abd와 똑같으니까요.


? : 문자가 0회 또는 1회 등장합니다. 즉 a?b : a가 나올 수도, 없을 수도 있으니 ab, b가 됩니다.


* : 마찬가지로 문자가 0회 이상 등장합니다. a*b : b, ab, aab, aaab, aaaab, ... 등이 됩니다.


+ : 마찬가지로 문자가 1회 이상 등장합니다. a+b : ab, aab, aaab, ... 등이 됩니다.


{n} : 문자가 n개 나옵니다. 즉 a{2}b : aab가 됩니다.


{n,} : 문자가 n개 이상 나옵니다. 즉 a{2,}b : aab, aaab, aaaab, ... 로 나올 수 있습니다.


{n, m} : 문자가 n개 이상 m개 이하로 나옵니다. a{1,3}b : ab, aab, aaab가 됩니다.


/s : 공백


/t : 탭 


/d : 숫자, 즉 [0~9]와 같습니다.


/b : 문자 사이의 공백


/w : 영,숫자 + '_', 즉 0_ , a_ ... 등등을 의미합니다. [a-zA-Z0-9_]로도 표현할 수 있습니다.


참고로 /s, /t, /d, /b, /w와 같은 경우엔, 각각 대문자로 바꾸면 반대의 의미가 됩니다. 가령 /S는 공백 문자를 뺀 나머지가 됩니다.






코틀린에서 정규 표현식 사용하기

정규 표현식이 뭐고, 어떻게 사용하는 지 대충 알아봤으니 이제 코틀린에서 한번 사용해 봅시다. 사용한다는 의미가, 어떤 문자열을 본인이 만든 정규 표현식에 매칭되는지, 매칭되는 문자열을 지정한 방법으로 바꾸는 방법이라던지 등등 여러가지로 표현하고 싶을 것입니다. 방법도 많고 메서드도 많겠지만, 사용해 볼 법한 문자열 처리 방법을 알아봅시다.



예시 1. http 형식의 인터넷 주소 체크

가령 어떤 문자열이 https://keykat7.blogspot.com 와 같은 형식의 주소를 가지고 있는지 확인하려면 어떻게 하면 될까요? 어떤 문자열이 직접 만든 패턴과 매칭이 되는지 알고 싶다면 toRegex()로 정규 표현식 지정을 한 후에 matches()를 이용하면 됩니다.


fun main() {

    val regex = "https://(.+)".toRegex()
    val path = "https://keykat7.blogspot.com"
    
    if (path.matches(regex)) println("match!")
    else println("not match..")
}


진짜 간단하게 표현한다면 위와 같이만 해도 되겠죠? 그런데 인터넷 주소가 https도 있지만, http도 있을 것입니다. 하다 못해 ftp, telnet 같은 프로토콜을 이용할 수도 있는데, 일일이 패턴을 만들어야 될까요? 물론 그럴 필요 없이 한꺼번에 지정해주면 됩니다.


fun main() {

    val regex = "(https|http|ftp|telnet)://(.+)".toRegex()
    val path = "https://keykat7.blogspot.com"
    val path2 = "http://keykat7.blogspot.com"
    val path3 = "ftp://keykat7.blogspot.com"
    
    if (path.matches(regex)) println("match!")
    else println("not match..")
    
    if (path2.matches(regex)) println("match!")
    else println("not match..")
    
    if (path3.matches(regex)) println("match!")
    else println("not match..")
}

위에서 '( )' 는 서브 패턴을 정할 때 사용한다고 했으니, "https 또는 http 또는 ftp 또는 telnet"이라는 서브 패턴을 하나로 묶었습니다. 

또는 https://www.naver.com 과 같이 "https://" + "www" + "." + 문자열 + "." + 문자열의 형식을 만들고자 할 때는 어떻게 작성할 수 있을까요?


fun main() {

    val regex = "(https|http|ftp|telnet)://www\\.(.+)\\.(com|net)".toRegex()
    val path = "https://www.naver.com"
    
    if (path.matches(regex)) println("match!")
    else println("not match..")
    
    
}

당연히 더 간단한 방법도 많고, 다른 방법도 많지만, 그냥 좀 직관적으로 작성해 봤습니다. (잘 아시는 분은 제가 작성한 걸 보고 비웃을 지도 모르겠습니다..) 아무튼 여러가지 방식을 활용해서 인터넷 주소를 매칭시킬 수 있다는 것을 알아주셨으면 좋겠습니다.



예시 2. 파일 구조 확인하기

사실 코딩 테스트에 나올 법한 정규 표현식을 생각하면서 예시를 작성하고 있습니다. 최근에 파일 구조를 정규 표현식으로 표현할 수 있는 지를 확인하는 문제를 푼 기억이 있어서 가져와 보았습니다. 

예를 들어 folder1/folder2/folder3/filename.java 와 같은 구조가 있을 때, 이 디렉토리 주소에서 파일 이름과 파일 확장자를 가져오려면 어떻게 하면 될까요? (문제도 대충 이것과 비슷했습니다.)


fun main() {

    val regex = "(.+)/(.+)\\.(.+)".toRegex()
    val path = "folder1/folder2/folder3/filename.java"
    
    if (path.matches(regex)){
        println("match!")
        val res = regex.matchEntire(path)
        if (res != null){
        	val (p, n, e) = res.destructured
            println("$p, $n, $e")
        }
    }
    else println("not match..")
    
    
    
    
}

 '/' 를 기준으로 나누고, '.'을 기준으로 파일 이름과 확장자명을 나누면 되겠네요. 이 때 matchEntire로 가져온 결과를 destructured 로 나눌 수 있습니다. (이건 저도 몰랐는데 다들 이렇게 하더군요. 파이썬으로만 해왔던지라..)



예시 3. IP 주소의 서브넷 주소까지 남긴 뒷자리를 바꾸기

예를 들어 192.168.0.125를 192.168.0.1로 바꾸려고 한다면 어떻게 하면 될까요? 이번에는 replace를 이용해 봅시다.


fun main() {

    // 맨 뒤를 .1로 바꾸기
    println( "192.168.0.125".replace("\\.\\d+$".toRegex(), ".1") )
    
    // 모든 주소를 .x로 바꾸기
    println( "192.168.0.125".replace("\\d+".toRegex(), "x") )
       
}

덤으로 모든 숫자를 x로 바꿔 보았습니다. 위의 경우, \\.\\d+$를 해석하자면, "\\."는 "."을 의미하고 "\\d"는 숫자를 의미하며, 즉 "\\d+"는 숫자를 1개 이상 사용한 것을 의미하고, "$" 는 문자열의 맨 끝을 의미합니다. 따라서 문자열의 끝 부분 바로 앞 부분을 보고, .숫자 부분을 .1로 바꾸라는 의미가 됩니다. 

아래의 경우는 단순히 \\d+ 부분을 전부 x로 바꾸라는 의미가 되구요.






마치며..

그 밖에도 주민등록번호 형식 이라던지, 전화 번호 형식 등 여러 가지 케이스를 정규 표현식으로 만들 수 있는데, 구글링을 조금만 해보면 여러 능력자 분들이 예시를 만들어주신 것을 볼 수 있습니다. 정규 표현식을 연습할 수 있는 사이트도 있다고 하니 찾아서 연습해보는 것도 좋을 듯 하구요.

사실 파이썬으로 코딩 테스트를 준비하면서 정규식을 표현하는 것이나 자바로 개발을 하면서 정규식을 표현하는 것은 종종 해보았는데 코틀린으로 하는 것은 처음이네요. 뭔가 잘못 된 부분이 있지는 않을까 걱정됩니다. 틀린 부분이 있으면 바로 얘기해 주세요. 비판은 언제나 환영입니다. (비난은 하지 말아주세요.. 소심해서..)

그럼 다들 즐거운 코딩 되길 바라겠습니다!



댓글