Swift/개념 & 응용

[Swift] 정규표현식 사용해보기

유정주 2023. 7. 16. 13:07
반응형

서론

정규표현식 알아보기에 이어 두 번째 정규표현식 포스팅입니다.

 

아마 3편은 Swift 5.7의 RegexBuilder(https://developer.apple.com/documentation/regexbuilder)가 될건데요.

이건 아마 많이 늦을듯... ㅎ 최소 한 달? (부스트캠프 챌린지 끝나고요 ㅎㅎ)

 

아무튼, 오늘의 포스팅인 정규표현식 사용해보기는 지난 1편에서 알아봤던 정규표현식을 실제로 사용해보는 내용입니다.

문자열에서 정규표현식을 어떻게 사용하느냐에 대한 글이니 정규표현식에 대해 잘 모르신다면 1편을 먼저 봐주세요.

 

학습하면서 작성한 포스팅인 만큼 부족한 점이 있을 수 있습니다.

틀린 점, 부족한 점 댓글 달아주시면 즉시 수정하도록 하겠습니다.

 

 

테스트 데이터

아래 문자열과 정규식을 사용해 예제들을 살펴보겠습니다.

let str1 = "안녕은 Hello. 잘가는 Bye."
let str2 = "안녕하세요 반갑습니다"

let regex1 = try Regex(#"[가-힣]+"#)
let regex2 = try Regex(#"[a-zA-Z]+"#)

 

 

문자열에서 일치하는 첫 번째 항목 찾기

firstMatch(of:), firstMatch(in:)

문자열에 일치하는 첫 번째 항목을 찾는 작업은 firstMatch 메서드를 이용하면 됩니다.

firstMatch 메서드는 문자열에서 처음으로 정규표현식과 매칭되는 결과를 반환합니다.

 

아래는 한글+영어 문자열에서 첫 번째로 나오는 한글 단어를 찾는 코드입니다.

print("\n===== firstMatch =====")
if let result = str1.firstMatch(of: regex1) {
    print("str: \(str1[result.range])")
    print("result: \(result.output.compactMap { $0.value })")
}

if let result = try? regex1.firstMatch(in: str1) {
    print("str: \(str1[result.range])")
}

//===== firstMatch =====
//str: 안녕은
//result: ["안녕은"]
//str: 안녕은

 

문자열 메서드인 firstMatch(of:)와 Regex 메서드인 firstMatch(in:) 두 가지를 나눠서 작성해봤어요.

모두 result의 range 프로퍼티를 이용해 문자열의 substring을 얻을 수 있습니다.

 

만약 첫 번째 영단어를 찾고 싶다면,

if let result = str1.firstMatch(of: try Regex(#"[a-zA-z]+"#)) {
    print("str: \(str1[result.range])")
}

if let result = try /[a-zA-z]+/.firstMatch(in: str1) {
    print("str: \(str1[result.range])")
}

//str: Hello
//str: Hello

정규식을 영어로 바꿔서 작성하면 됩니다.

(이번에는 firstMatch 메서드 호출부에서 직접 정규식을 만드는 방법으로 작성해봤습니다.)

 

range(of:options:)

또다른 방법으로는 String의 range 메서드를 사용할 수도 있습니다.

이것도 일치하는 첫 번째 항목 결과를 반환합니다.

print("\n===== String range regularExpression =====")
if let range = str1.range(of: #"[가-힣]+"#, options: .regularExpression) {
    print("str: \(str1[range])")
}

//str: 안녕은

of 파라미터에 정규식 표현 문자열을 전달하고, options로 regularExpression을 설정하면

firstMatch와 동일한 동작을 수행합니다.

(regularExpression이 정규표현식이라는건 1편에서 언급했습니다.)

 

 

시작 지점부터 일치하는지 확인하기

문자열의 시작 지점부터 정규표현식과 일치하는지 확인하는 작업은 prefixMatch 메서드를 사용하면 됩니다.

print("\n===== prefixMatch =====")
if let result = str1.prefixMatch(of: regex1) {
    print("str: \(str1[result.range])")
}

if let result = str1.prefixMatch(of: regex2) {
    print("result: \(result.output.compactMap { $0.value })")
} else {
    print("일치하지 않음")
}

//===== prefixMatch =====
//str: 안녕은
//일치하지 않음

첫 번째 prefixMatch는 문자열이 한글로 시작하기 때문에 매칭 결과를 반환했습니다.

하지만 두 번째 prefixMatch는 문자열이 영어로 시작하지 않기 때문에 일치하지 않는다는 결과가 나왔네요.

 

firstMatch와의 차이점을 아시겠죠?

firstMatch는 일치하는 첫 번째 항목을, prefixMatch는 시작 지점부터 일치하는지 확인입니다.

 

 

문자열 전체가 일치하는지 판단

문자열 전체가 일치하는지 확인하려면 wholeMatch를 사용하면 됩니다.

print("\n===== wholeMatch =====")
if let result = str1.wholeMatch(of: regex1) {
    print("str: \(str1[result.range])")
} else {
    print("일치하지 않음")
}

if let result = try /[가-힣]{5}\s?[가-힣]{5}/.wholeMatch(in: "안녕하세요 반갑습니다") {
    print("result: \(result.output)")
} else {
    print("일치하지 않음")
}

//===== wholeMatch =====
//일치하지 않음
//result: 안녕하세요 반갑습니다

첫 번째 예시는 정규표현식과 일치하지 않는 문자열이 포함되어 있습니다. (영어요!)

그래서 일치하지 않는다는 결과가 나옵니다.

반대로 "안녕하세요 반갑습니다"는 가~힣 범위의 5글자 단어가 맞으므로 값이 반환됩니다.

 

이렇게 wholeMatch는 문자열 전체가 정규표현식과 일치하는지 확인할 때 사용하면 유용합니다.

 

 

일치하는 모든 항목 찾기

문자열에서 일치하는 항목을 잘라서 가져오고 싶은 경우도 있습니다.

그럴 때는 matches 함수를 사용하면 됩니다.

print("\n===== swift code =====")
let varString = "var a: Int"
let varRegex1 = try Regex(#"\w+"#)
let match = varString.matches(of: varRegex1)
for item in match {
    print("match output: \(varString[item.range])")
}

//===== swift code =====
//match output: var
//match output: a
//match output: Int

1편에서 다뤘던 문자열인 var a: Int에서 영숫자, _으로 이루어진 단어만 뽑고 있습니다.

그럼 공백과 클론(:)은 삭제되는 결과를 얻어야 합니다.

matches 메서드에 적절한 regex를 파라미터로 전달하면 일치하는 항목의 결과 배열을 반환합니다.

문자열에서 결과 아이템의 range를 사용하면 문자열에서 일치하는 항목을 찾을 수 있습니다.

 

let matchStrings = varString.matches(of: varRegex1).map { varString[$0.range] }
print("matchStrings: \(matchStrings)")

//matchStrings: ["var", "a", "Int"]

map 고차함수를 사용한다면 조금 더 깔끔한 코드가 되겠죠?

 

 

정규표현식 그룹 단어 찾기

정규표현식에서는 ( )를 이용해 그룹을 만들 수 있습니다.

firstMatch를 사용하면 그룹 매칭 결과를 튜플로 반환 받을 수 있습니다.

let keyAndValue = /(.+?): (\d+)\s+(\d+)\s+(\d+)/
let setting = "color: 161 103 230"

if let match = setting.firstMatch(of: keyAndValue) {
    print("Value: \(match.output)")
}

//===== firstMatch key-value =====
//Value: ("color: 161 103 230", "color", "161", "103", "230")

그룹 문자열을 사용할 때 정말 유용하게 사용할 수 있을 거 같네요.

 

 

NSRegularExpression

NSRegularExpression로도 여러 항목을 가져올 수 있습니다.

다른 블로그에서 소개하는 대부분의 코드가 NSRegularExpression더라고요.

정규표현식 문자열로 NSRegularExpression 객체를 생성하고, NSRange를 만들어서 firstMatch 등의 메서드에 전달하는 방법입니다.

print("\n===== NSRegularExpression 1 =====")
let varPattern = #"^var\s+(\w+):\s?(\w+)$"#

let varRegex2 = try! NSRegularExpression(pattern: varPattern, options: [])
let varRange = NSRange(location: 0, length: varString.count)

if let match = varRegex2.firstMatch(in: varString, options: [], range: varRange),
   let varNameRange = Range(match.range(at: 1), in: varString),
   let typeRange = Range(match.range(at: 2), in: varString) {
    print("varName: \(varString[varNameRange])")
    print("type: \(varString[typeRange])")
}

//===== NSRegularExpression 1 =====
//varName: a
//type: Int

 

1번 튜플과 2번 튜플을 읽어서 var 이름과 타입을 구했습니다.

 

 

이름으로 그룹 읽기

tuple의 번호가 아닌 이름을 설정할 수도 있습니다.

정규표현식 ( )에 ?<이름>을 추가하면 됩니다.

let varPattern2 = #"^var\s+(?<name>\w+):\s?(?<type>\w+)$"#

첫 번째 영단어에는 name이라는 이름을, 마지막 영단어에는 type이라는 이름을 설정했습니다.

if let match = varRegex3.firstMatch(in: varString, options: [], range: varRange),
   let nameRange = Range(match.range(withName: "name"), in: varString),
   let typeRange = Range(match.range(withName: "type"), in: varString) {
    print("name: \(varString[nameRange])")
    print("type: \(varString[typeRange])")
}

//===== NSRegularExpression 2 =====
//name: a
//type: Int

range(at:)이 아닌 range(withName:)을 사용하여 설정한 이름의 값을 가져옵니다.

 

물론 matches와 문자열의 firstMatch(of:)로도 사용할 수 있습니다.

print("\n===== NSRegularExpression 3 =====")
let match2 = varRegex3.matches(in: varString, options: [], range: varRange)
match2.forEach {
    let nameRange = Range($0.range(withName: "name"), in: varString)!
    let typeRange = Range($0.range(withName: "type"), in: varString)!
    print("name: \(varString[nameRange])")
    print("type: \(varString[typeRange])")
}

//===== NSRegularExpression 3 =====
//name: a
//type: Int

matches는 결과 배열을 반복하여 일치하는 모든 항목을 가져올 수 있습니다.

 

print("\n===== firstMatch 사용 =====")
if let match3 = varString.firstMatch(of: try Regex(varPattern2)) {
    print("name: \(match3.output.compactMap { $0.name })")
    print("value: \(match3.output.compactMap { $0.value })")
}

//===== firstMatch 사용 =====
//name: ["name", "type"]
//value: ["var a: Int", "a", "Int"]

문자열 메서드 firstMatch(of:)를 사용한 코드입니다.

마찬가지로 output을 이용해 이름으로 값을 가져올 수 있습니다.

 

 

마무리

이번 포스팅에서는 Swift에서 정규표현식을 사용하는 방법에 대해 알아보았습니다.

 

정규표현식 정말 쉽지 않네요..

그래도 조금씩 익숙해지고 있다는 걸 느끼고 있습니다 ㅎㅎ

 

Swift 5.7의 RegexBuilder를 사용하면 Swift스럽게 정규표현식을 나타낼 수 있다고 하는데요.

그걸 사용해도 기본적으로 문자열 정규표현식을 사용할 수 있어야겠죠?

 

더 연습하면서 화이팅합시다.

 

감사합니다!


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형