본문 바로가기

Swift

[Swift] 정규표현식

안녕하세요 Wody입니다.

 

코딩테스트에서 빠질 수 없는 문자열 제어 중, 무조건 쓸 수 밖에 없는 정규표현식을 배워보고자 합니다.

 

정규표현식이란?

정규식은 특정한 규칙을 가진 문자열의 집합을 표현하는데 사용하는 형식 언어입니다. 정규 표현식은 많은 텍스트 편집기와 프로그래밍 언어에서 문자열의 검색과 치환을 위해 지원하고 있습니다.

from. 위키백과

 

결론은 문자열을 검색하고 치환하기 위해 사용하는 기능이라고 볼 수 있습니다.

 

그럼 Swift 에서는 정규표현식을 어떻게 사용할 수 있을까요?

 

정규표현식 사용하기

Apple Developer - NSRegularExpression

 

 

 

 

정규표현식에 대한 공식문서를 위에서부터 살펴보면

 

분류는 `Foundation`에 속해있고 `String and Text` 기능에 포함되어있음을 알 수 있습니다.

> 이 말은 `import Foundation` 해야 정규표현식 기능을 사용할 수 있다는 뜻!

 

그리고 NSObject를 상속받는 클래스 타입입니다!

 

어떻게 사용하나요?

참고 블로그 - Swift에서 정규표현식(Regular Expression)을 이용하기

 

먼저 풀 코드를 적어드리고 해석해드릴게요!

 

let str = "helloHelloMyNameIsWody wwow 123wody 3$!wody@#$ 23Wddot"

let parttern = "[A-Za-z]+"

extension String {
    func getArrayAfterRegex(regex: String) -> [String] {
        do {
            let regex = try NSRegularExpression(pattern: regex)
            let results = regex.matches(in: self, range: NSRange(self.startIndex..., in: self))
            return results.map {
                String(self[Range($0.range, in: self)!])
            }
        } catch let error {
            print("에러러러")
            return []
        }
    }
}

print(str.getArrayAfterRegex(regex: parttern))

// print result
// ["helloHelloMyNameIsWody", "wwow", "wody", "wody", "Wddot"]

상수(let)

let str은 우리가 정규표현식을 통해서 검사할 문자열입니다. 소문자와 대문자, 숫자, 특수문자 등 다양한 패턴이 섞여 있습니다.

 

let parttern은 정규표현식에 적용할 패턴입니다. 정규표현식 규칙을 통해 패턴을 정의합니다.

 

확장(extension)

확장은 String 타입 자체를 타겟으로 잡고 있습니다. 타입 자체를 확장하기 때문에 String 타입인 값에서 언제든지 extension 안의 메소드를 호출할 수 있습니다. 

 

인스턴스 메소드

이번에 정의하는 getArrayAfterRegex(regex: String) 메소드는 regex 매개변수가 있으며 받는 타입은 String 타입입니다. 

 

이 안에 정규식을 전달해주면 되는데요, 예제 코드에선 상수로 정의한 parttern을 정규식으로 전달해주었습니다.

 

그렇다면 왜 NSRegularExpression은 do-catch문을 통해 try해야 할까요? 공식문서에 따르면 다음과 같습니다.

동시성 및 스레드 안전성

NSRegularExpression단일 인스턴스가 여러 스레드에서 한 번에 일치 작업에 사용될 수 있도록 변경 불가능하고 스레드로부터 안전하도록 설계되었습니다. 그러나 작동 중인 문자열은 다른 스레드에서든 반복에 사용된 블록 내에서든 일치 작업 중에 변경되어서는 안 됩니다.

 

스레드 안정성 때문에 예외처리를 해줘야 하기 때문이었군요! 

 

만약 스레드 안정성에 대해 잘 모르신다면 더보기를 눌러 예시를 읽어보셔도 좋을 것 같습니다

 

더보기

위 문장이 이해가 안가신다면 이렇게 예시를 들어보면 좋을 것 같아요.

여러분이 대학생들의 기말고사 시험지를 체점하는 대학원생인데, 다행히 객관식 문제여서 열심히 체점을 하고 있었습니다.

정말 열심히 해서 거의 다 끝냈어요...! 그런데 교수(멀티쓰레드)님이 오셔서 이렇게 말씀하시면 어떨까요?

어, 미안하다 깜박하고 다른 강의 시험지를 줬네 그거 말고 새로 시험지(작업물 변경) 줄테니깐 그거 체점해놔

 

😱 난 다 해놨는데.... ㅠㅠ

 

아무튼! do-catch문 안에서 정규표현식을 선언합니다!

let regex = try NSRegularExpression(pattern: regex)

여기서 NSRegularExpression의 초기화는 init(pattern: String, options: NSRegularExpression.Options) 입니다.

 

그런데 options가 없네요?! 괜찮습니다. 옵션의 경우는 다양한 상수가 존재하는데 그 상수가 하는 역할을 패턴에 넣어서 옵션을 정해줄겁니다!

 

예시로 옵션 중 `.anchoresMatchLines`가 있는데 이 옵션은 `^[parttern]$'을 통해 정의할 수 있습니다.

 

자 우리는 정규식 상수 regex를 만들었으니 정규식을 통해 문자열을 검색해봐야겠죠?

 

정규표현식에 있는 인스턴스 메소드 `matches(in: String, options: NSRegularExpression.MatchingOptions, range: NSRange) -> [NSTextCheckingResult]`를 통해 문자열을 검색할 수 있습니다.

 

let results = regex.matches(in: self, range: NSRange(self.startIndex..., in: self))

메소드 안의 파라매터 하나하나를 봐볼게요,

 

여기서 in: self 는 `String`타입을 `extension`한 메소드를 호출한 타겟을 의미합니다. 

 

너무 어렵다고요...? 

 

print(str.getArrayAfterRegex(regex: parttern))

str이 `String 타입` 메소드 `getArrayAfterRegex`를 호출하고 있죠?

 

즉 `str`이 메소드 안에서 `self`가 되는 것입니다.

 

`regex.matches`의 검색할 문자열은 self가 되고, 검색할 범위(range)는 `in: self`의 `시작 인덱스로부터 끝가지`로 NSRange타입으로 만들어 전달합니다.

 

이렇게 매칭한 결과값은 [NSTextCheckingResult] 형태로 반환되어 results에 저장됩니다.

 

결과적으론 정규표현식이 문자열을 검색했는데요!

 

아직 이 결과물을 우리가 사용할 순 없습니다. 배열안에 있는 result는`String` 타입이 아니거든요 ㅎㅎ

 

그래서 `NSTextCheckingResult`타입을 `String`타입으로 만들어줄겁니다.

 

return results.map {
                String(self[Range($0.range, in: self)!])
            }

정규표현식 결과물로 가득한 results를 map 함수를 통해 각각 적용시켜줄거구요!

 

String()을 통해 스트링 타입으로 바꿔줄겁니다.

 

그리고 String으로 변환될 값들은 self[Range()]를 통해 인덱스를 기준으로 뽑아갑니다.

 

Range는 생성될 때 초기화로 init(_ range: NSRange, in string: String)을 요구합니다. 

 

NSRange는 클로져의 인자 이름 축약을 통해 $0이라는 인자 값을 전달합니다. (map 함수를 사용중이니깐요!)

 

그래서 우리는 `NSTextCheckingResult`타입의 인스턴스 프로퍼티 `range: NSRange`를 호출해 `Range`의 초기화 값을 전달합니다.

더보기

(추가로 Range를 생성할 때 인자값으로 전달되는 값들이 존재하지 않거나 맞지 않을 수 있기 때문에 Range<String.Index>는 옵셔널하게 반환됩니다. 이를 해제하려면 옵셔널 바인딩, 체이닝, 강제 언랩핑 등의 방법이 있습니다.)

 

자...! 우리는 self의 NSRange 값을 알아내서 Range 타입을 만들어냈습니다...!!!!!!

 

그럼 그걸 self[Range]를 통해 검색된 문자열을 뽑아왔습니다. 그리고 그걸 String()을 통해 스트링 값으로 못을 박아줍니다 땅땅땅!!!

 

휴 ㅠㅠㅠ

 

결과적으로 우리는 .getArrayAfterRegex 메소드를 호출함과 패턴을 전달해주면,

 

정규표현식에 따라 검색된 문자열의 배열을 반환값으로 얻는 메소드를 배워봤습니다.

 

요약

아래 방법으로 정규표현식 패턴을 구성할 경우, 문자열의 각 문자마다 패턴이 적용되므로 `^[parttern]$`는 가정하지 않는다.

확장

extension String {
    func getArrayAfterRegex(parttern: String) -> [String] {
        do {
            let regex = try NSRegularExpression(pattern: parttern)
            let results = regex.matches(in: self, range: NSRange(self.startIndex..., in: self)).map { String(self[Range($0.range, in: self)!]) }
            return results
        } catch let error {
            print("regex error")
            return []
        }
    }
}

 

패턴

[a-z] : 소문자만

[A-Z] : 대문자만

[0-9] : 숫자만

ex. [a-zA-Z0-9] : 대소문자, 숫자만

let str = "hi my name is Wody"




let parttern = "[a-zA-Z]"

print(str.getArrayAfterRegex(regex: parttern)

// result 
// ["h", "i", "m", "y", "n", "a", "m", "e", "i", "s", "W", "o", "d", "y"]


let parttern = "[a-zA-Z]+"

print(str.getArrayAfterRegex(regex: parttern)

// result 
// ["hi", "my", "name", "is", "Wody"]