콘텐츠로 건너뛰기
Home » NSRegularExpression : 정규식 사용하기 – Swift

NSRegularExpression : 정규식 사용하기 – Swift

Swift의 정규식

Swift는 언어 자체에서 정규식을 지원하지 않고 FoundationNSRegularExpression 클래스를 이용한다.

  1. NSRegulareExpressioninitthrows이기 때문에 try와 같이 사용되어야 한다.
  2. 매치 결과는 TextCheckingResult 클래스의 인스턴스를 얻게 된다. 이는 매치영역 및 영역 내 각 매치 그룹의 범위를 NSRange값으로 가지고 있다.
  3. 문제는 Swift 문자열의 부분문자열은 Index<String.Index>에 의해서 얻을 수 있지, NSRange를 이용할 수 없다. 따라서 이를 컨버팅하는 편의함수나 타입 확장을 이용해야 한다. (사실 이 부분은 Linux 버전의 Swift의 문제이다. Apple Swift에서는 Foundation/Cocoa를 임포트하게 되면  NSString의 API가 그대로 String으로도 노출되기 때문에 그대로 사용이 가능하다.)


먼저 NSRange를 이용하여 부분 문자열을 구하게 하는 문자열 확장은 다음과 같다.

extension String {
  public func range(with r: NSRange) -> String.Index {
    let a = index(startIndex, offsetBy: r.location)
    let b = index(startIndex, offsetBy: r.location + r.length)
    return a..<b
  }
  public subscript(range: NSRange) -> String {
    return self[self.range(with:range)]
  }
}

또, 흔히 문자열 전체 범위에 대해서 탐색을 수행하는 경우가 많기 때문에 다음과 같은 확장을 하나 주는 것도 좋다.

extension String {
  public var fullRange: NSRange {
    return NSRange(location:0, length:characters.count)
  }
}

정규식 객체를 만드는 것은 패턴과 옵션을 이용한다.
옵션은 잘 쓰이지는 않지만 정리해보면 다음과 같다.

static class Options: OptionSet {
  static var caseInsensitive { get }
  static var allowCommentsAndWhiteSpace { get }
  static var ignoreMetacharacters { get }
  static var dotMatchesLineSeparators { get }
  static var anchorsMatchLines { get }
  static var useUnixLineSeparators { get }
  static var useUnicodeWordBoundaries { get }
}

특이한 점은 failable 생성자가 아닌 예외를 던지는 생성자를 쓴다는 점이다.


if let regex = try? RegularExpression(pattern:"\\d{1,4}:\\d{1,2}:\\d{1,2}", options:[]) {
  ...
}

검색

탐색에 쓰이는 메소드는 크게 네 가지로 나뉜다.

  • numberOfMatches(in:options:range:) -> Int – 테스트 결과 매치되는 영역의 개수를 리턴한다.
  • firstMatch(in:options:range:) -> TextCheckingResult? – 첫 매치를 리턴한다.
  • matches(in:options:range:) -> [TextCheckingResult] – 모든 매치를 리턴한다.
  • enumerateMatches(in:options:range:using) – 각 매치에 대해서 블럭을 적용한다.

기본적으로 검사할 문자열과 옵션, 검사범위를 주고 검사하는데, 1)매치의 수, 2)첫번째매치, 3)전체매치, 4)각 매치에 대해 반복작업지정의 동작을 수행한다.

매치 수 찾기

let str = "1234567890"
let pattern = "\\d{1,3}" // 숫자 1~3개
let regex = try! RegularExpression(pattern:pattern, options:[])
let n = regex.numberOfMatches(in:str, options:[], range:str.fullRange) // 4 (123|456|789|0)

위 예제의 패턴은 숫자 1~3개의 매칭을 검사하는데, 한 번 스캔한 영역은 되돌아가서 다시 스캔하지 않으므로 최대 4개의 영역이 발생한다.

첫번째 매치 뽑기

let str = "1234567890"
let pattern = "\\d{3}(?=8)" // 8앞의 숫자 3개 --> 567 밖에 없다.
let regex = try! RegularExpression(pattern:pattern, options:[])
if let n = regex.firstMatch(in:str, options:[], range:str.fullRange) {
  print(str[n.range]) // prints "567"
}

TextCheckingResultrange 속성은 패턴 전체가 매치하는 영역을 리턴한다. 만약에 패턴 내에 캡쳐링그룹이 정의되어 있다면 range(at:)을 이용하여 각각 그룹의 범위를 얻을 수 있는데, 이 때 0은 전체 범위, 1은 1번 그룹… 이런 식으로 정의될 수 있다.

전체 매치

정규식 패턴은 주어진 문자열 내에서 여러 번 매칭될 수 있다. 따라서 firstMatch(in:options:range:)와 거의 유사한 API로 matches(in:options:range)가 있다. 이는 TextCheckingResult의 배열을 리턴한다.

결과 순회

matches(in:options:range:)를 이용해서 전체를 검사한 각 결과를 순회하는 방법도 있지만, enumerateMatches(in:options:range:using:)을 써서 순회하는 방법도 있다.
이 때 넘겨주는 클로저의 타입은 @noescape (TextCheckingResult?, RegularExpression.MatchingFlag, UnsafeMutablePointer<ObjBool>) -> Void로, 각각 (result, flag, stop)이 된다. result는 탐색 결과를 담고 있고, flags는 매칭 처리에 사용된 옵션 정보를 담는다. stop은 불리언 값에 대한 포인터로 이 값을 true로 설정하면 더 이상 순회하지 않고 멈추게 된다.


let str = "123456789"
let regex = try! RegularExpression(pattern:"\\d{3}", options:[])
regex.enumerateMatches(in:str, options:[], range:str.fullRange){ result, flags, stop in
  print(str[result!.range(at:0)])
  stop.pointee = true // stop = true 가 되므로 더 이상 진행하지 않는다.
}

stop의 타입은 UnsafeMutablePointer<ObjBool> 타입인데 ObjBoolBool 타입으로 브릿징되므로 UnsafeMutablePointer<Bool>과 같다고 볼 수 있으며, .memory 프로퍼티는 .pointee로 보다 직관적인 이름으로 바뀌었다.

바꾸기

RegularExpression은 탐색 결과의 위치를 구하는 클래스이기 때문에, 문자열의 내용을 변경하는 기능은 제공하지 않는다. 하지만, 서브레인지의 범위를 알면 내용을 교체하는 것이 어렵지는 않다.

var str = "1234567890"
let regex = try! RegularExpression(pattern:"\\d{3}(?=8)", options:[])
if let match = regex.firstMatch(in:str, options:[], range:str.fullRange) {
  str.replaceSubrange(str.range(with:r), with:"abc")
}
print(str)
// "1234abc890