Swift의 정규식
Swift는 언어 자체에서 정규식을 지원하지 않고 Foundation
의 NSRegularExpression
클래스를 이용한다.
NSRegulareExpression
의init
은throws
이기 때문에try
와 같이 사용되어야 한다.- 매치 결과는
TextCheckingResult
클래스의 인스턴스를 얻게 된다. 이는 매치영역 및 영역 내 각 매치 그룹의 범위를NSRange
값으로 가지고 있다. - 문제는 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"
}
TextCheckingResult
의 range
속성은 패턴 전체가 매치하는 영역을 리턴한다. 만약에 패턴 내에 캡쳐링그룹이 정의되어 있다면 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>
타입인데 ObjBool
은 Bool
타입으로 브릿징되므로 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