트랙
/
R
R
/
학습 과정
/
정규 표현식
정규

정규 표현식 에서 R

1개의 연습 문제

정규 표현식 소개

정규 표현식은 문자열을 _패턴 매칭_하는 매우 다재다능한 방법으로, 이를 위해 설계된 도메인 특화 언어(DSL)를 사용해요.

Advanced

다른 많은 프로그래밍 언어와 마찬가지로, R은 자체 정규식 라이브러리를 구현하려고 하지 않아요. 대신 ICU 정규 표현식 엔진을 감싸서 사용해요. ICU는 대부분의 최신 언어가 사용하는 인기 있는 PCRE2 라이브러리와 비슷하지만, 일부 예외적인 경우는 다르게 처리돼요.

많은 정규식 기능이 Base R에 내장되어 있고, 오래된 코드에서 그런 예를 볼 수 있어요.

새로 작성하는 코드라면 stringr 라이브러리를 사용하는 것을 강력히 권장해요.

문자열 개념에서 언급했듯이, stringr은 문서가 잘 갖춰져 있고, 보통 패키지 작성자가 직접 작성해요.

Note

이 R 강의에서는 이미 기본적인 정규식 문법에 익숙하다고 가정해요. R에 특화된 기능만 집중적으로 살펴볼게요.

정규 표현식 지식을 되살릴 수 있는 자료 몇 가지를 아래에 정리했어요.

Strings 개념에서 이미 다룬 함수 중 상당수는 기본적으로 정규식 검색을 위해 만들어졌어요.

문자열 개념에서는 문자열 리터럴에만 일치시키는 비교적 단순한 내용으로 다뤘지만, 대신 어떤 정규식 패턴이든 사용할 수 있어요.

엄밀히 말하면, R에서는 regex()를 호출해서 정규식을 만들어요. 편의를 위해 stringr 함수들은 어떤 패턴 문자열이든 정규식으로 취급해요. 아래 예시에서 첫 번째 str_replace_all()은 두 번째 버전을 더 간편하게 쓴 표현이에요:

input <- "Some input string"

# "\\s" matches any whitespace character
input |> str_replace_all("\\s", "_")
[1] "Some_input_string"

input |> str_replace_all(regex("\\s"), "_")
[1] "Some_input_string"

치트시트는 유용한 안내서예요. pattern 인자를 가지는 모든 함수(빨간색으로 표시)는 정규식을 받아들이고, 2쪽에는 정규식 문법이 요약되어 있어요.

대소문자 구분이나 로케일처럼 기본값을 바꿔야 할 때는 regex() 함수가 여전히 중요할 수 있다는 점에 유의해요.

# default is case sensitive
"A Mixed Assortment" |> 
    str_replace_all(regex("[aeiou]", ignore_case = TRUE), "*")
#> [1] "* M*x*d *ss*rtm*nt"

정규식 코드에는 위에서 사용한 \s처럼 백슬래시가 들어가는 경우가 많아요. 일반 R 문자열에서는 \를 두 번 써서 이스케이프해야 하므로 "\\s"가 돼요.

백슬래시가 많아지면 헷갈리기 쉬우니, 복잡한 경우에는 원시 문자열을 사용하는 게 도움이 될 수 있어요.

기본적으로 원시 문자열을 정의하는 문법은 r"( )"이지만, 다른 구분자도 사용할 수 있어요. 정규식 문법에서는 괄호 ( )와 대괄호 [ ]가 둘 다 흔히 쓰이므로(캡처 그룹과 옵션 그룹), 중괄호 { }가 좋은 선택이 될 수 있어요.

input |> str_replace_all(r"{\s\w}", "__")
#> [1] "Some__nput__tring"

일치 탐지

몇 가지 질문과, 그 답을 찾는 데 도움이 되는 함수를 소개할게요.

일부 예시에서는 stringr에 연습용으로 포함된 80개의 과일 이름 벡터인 fruit를 사용해요.

문자열에 일치하는 부분이 있는지? str_detect(), 또는 ^나 $ 앵커를 쓰지 않으려면 str_starts(), str_ends()를 사용해요.

# berries begining with b
fruit |> str_detect("^b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE FALSE

# alternative syntax
fruit |> str_starts("b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE FALSE

일치하는 문자열만 반환하려면? 문자열을 얻으려면 str_subset(), 인덱스를 얻으려면 str_which()를 사용해요.

fruit |> str_subset("a...e")
[1] "apple"       "blackberry"  "mandarine"   "nectarine"   "pineapple"   "pomegranate" "raspberry"   "salal berry"
> fruit |> str_which("a...e")
[1]  1  7 48 51 62 64 70 73

각 문자열에 일치하는 부분이 몇 개인지? str_count()

# count vowels
fruit |> str_count("[aeiou]") |> head(10)
#> [1] 2 3 4 3 3 2 2 3 5 3

일치하는 위치는 어디인지? 대화형으로 쓸 때는 str_view(), 더 일반적으로는 str_locate()나 str_locate_all()을 사용해요.

# str_view only shows matching elements, with match in < > delimiters
fruit |> str_view("a...e")
 [1] │ <apple>
 [7] │ bl<ackbe>rry
[48] │ mand<arine>
[51] │ nect<arine>
[62] │ pine<apple>
[64] │ pomegr<anate>
[70] │ r<aspbe>rry
[73] │ sal<al be>rry

# str_locate returns all elements
fruit |> str_locate("a...e") |> head(10)
      start end
 [1,]     1   5
 [2,]    NA  NA
 [3,]    NA  NA
 [4,]    NA  NA
 [5,]    NA  NA
 [6,]    NA  NA
 [7,]     3   7
 [8,]    NA  NA
 [9,]    NA  NA
[10,]    NA  NA

캡처

흔히 무엇이 일치하는지 알고 싶을 때가 있어요. 이는 정규식 안에 괄호로 캡처 그룹을 넣으면 해결돼요.

같은 정규식 안에서 \1, \2 등을 사용해 캡처를 다시 참조할 수 있어요. 예를 들어 반복되는 두 글자 쌍을 찾는 경우는 이렇게 해요:

fruit |> str_subset("(..)\\1")
[1] "banana"      "coconut"     "cucumber"    "jujube"      "papaya"      "salal berry"

비슷한 문법으로, 캡처를 대체 문자열에서 사용할 수도 있어요:

# double to triple
fruit |> 
  str_subset("(..)\\1") |> 
  str_replace("(..)\\1", "\\1\\1\\1")
[1] "bananana"      "cococonut"     "cucucumber"    "jujujube"      "papapaya"      "salalal berry"

나중에 처리할 일치 결과를 모으려면 첫 번째 일치만 필요할 때는 str_match() 함수를, 모든 일치가 필요할 때는 str_match_all()을 사용해요.

캡처 그룹은 어떤 언어에서든 복잡한 출력을 만들어내는 경향이 있어요. R의 방식(덤으로 벡터화까지 되어 있다는 점이 추가 난관이에요)은 str_match()로 행렬을 만드는 것으로, 입력 문자열마다 행이 하나씩 생기고, 1번 열에는 일치한 전체 문자열이, 나머지 열에는 각 캡처 그룹이 들어가요.

이름 있는 그룹을 사용하면 결과 행렬의 열 이름이 되기 때문에 유리할 수 있어요.

recipe <- "dissolve 25g sugar in 200ml water"
recipe |> str_match("(\\d+g) .* (\\d+ml)")
     [,1]                 [,2]  [,3]   
[1,] "25g sugar in 200ml" "25g" "200ml"

# use named capture groups
recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")
                          wt    vol    
[1,] "25g sugar in 200ml" "25g" "200ml"

이 강의에서는 행렬을 많이 다루지 않았지만, 참고할 수 있는 개념 문서가 있어요. 행렬은 오랫동안 Base R의 일부였음에도 Tidyverse 생태계와는 잘 맞지 않아요.

간단히 요약하면, 행렬은 인덱스가 두 개인 벡터처럼 다루면 되고, 순서는 [rows, cols]예요. 두 인덱스 중 하나를 비워 두면 행이나 열 전체를 얻을 수 있어요.

m[r,]로 행 r 하나를, 또는 m[, c]로 열 c 하나를 가져올 때, 반환되는 값은 기본적으로 벡터로 펼쳐져요.

> amounts <- recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")

# get all of row 1, as a named vector
> amt1 <- amounts[1,]
> amt1
                                       wt                  vol 
"25g sugar in 200ml"                "25g"              "200ml" 

# get a vector element
amt1["wt"] |> unname()
[1] "25g"

불가피하게도, str_match_all()은 구조가 훨씬 더 복잡한 출력을 만들어요:

  • 결과는 배열이고, 입력 문자열마다 원소가 하나씩 들어 있어요.
  • 입력 문자열이 하나뿐이라면, 배열 개념에서 설명한 대로 [[1]]로 인덱싱해서 첫 번째 원소를 꺼내면 돼요.
  • 각 원소는 행렬이고, 열은 match()와 같아요.
  • 행렬의 각 행은 성공한 캡처 하나예요.
mix <- "Mix 200g of sugar, 180g of flour and 150g of butter"

# raw output
> str_match_all(mix, "(\\d+)g")
[[1]]
     [,1]   [,2] 
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"

# we just want the first list element
> str_match_all(mix, "(\\d+)g")[[1]]
     [,1]   [,2] 
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"

# captures are in column 2, so get that as a vector
> str_match_all(mix, "(\\d+)g")[[1]][,2]
[1] "200" "180" "150"

물론 일치에 실패할 수도 있어요. 그럴 때 결과는 특별한 값인 NA가 돼요. 없음 개념에서 설명한 대로 이 경우를 검사할 준비를 해 두세요.

# failed match
recipe |> str_match("(not here)")
     [,1] [,2]
[1,] NA   NA  
Note

R for Data science에서 지적하기를:

str_match()는 행렬을 반환해서 다루기가 그리 편하지는 않아요.

각주를 달아서:

이 책에서는 행렬을 아예 다루지 않기 때문이죠!

이 책의 저자 중 한 명은 stringr 라이브러리도 만들었기 때문에, 사실상 자신의 작업을 비판하고 있는 셈이에요.

이 함수의 구현이 앞으로 나올 릴리스에서 어떻게 바뀔지 지켜보는 것도 흥미로울 거예요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요

정규 표현식 배우기