30 문자열과 프로그래밍하기

30.0.1 인코딩

일반적으로 베이스 R 의 Encoding() 이 유용하지 않은 경우가 많은데, 세가지 다른 인코딩 (의미하는 바는 중요함) 만 지원하고 실제 인코딩이 아닌 R 이 생각하는 인코딩을 알려주기만 하기 때문입니다. 일반적으로 문제는 선언되는 인코딩이 잘못된 경우입니다.

tidyverse 는 UTF-8 을 모든 곳에서 사용하는 best practices15 를 따르기 때문에 tidyverse 로 생성하는 문자열은 모두 UTF-8 을 사용할 것입니다. 문제가 발생할 경우도 있겠지만, 데이터 불러오기 과정에서 발생하게 됩니다. 인코딩 문제가 있습니다고 진단하게 되면, 데이터 불러오기 과정에서 바로잡아야 합니다. (즉 readr::locale()encoding 인수를 사용).

30.0.2 길이와 서브셋하기

영어와 친숙하기만 하다면 이는 어렵지 않은 계산인 것 같이 보이지만, 다른 언어로 작업하게 되면 복잡해지기 시작합니다.

가장 많은 언어는 라틴어, 중국어, 아라비아어, Devangari 인데, 작성하는 세가지 시스템을 대표합니다:

  • 라틴어는 알파벳을 사용하는데, 각 자음과 모음이 각자의 글자를 가지고 있습니다.

  • Chinese. Logograms. Half width vs full width. English letters are roughly twice as high as they are wide. Chinese characters are roughly square.

  • Arabic is an abjad, only consonants are written and vowels are optionally as diacritics. Additionally, it’s written from right-to-left, so the first letter is the letter on the far right.

  • Devangari is an abugida where each symbol represents a consonant-vowel pair, , vowel notation secondary.

For instance, ‘ch’ is two letters in English and Latin, but considered to be one letter in Czech and Slovak. — http://utf8everywhere.org

# But
str_split("check", boundary("character", locale = "cs_CZ"))
#> [[1]]
#> [1] "c" "h" "e" "c" "k"

많은 언어가 glyphs 가 기본알파벳에 추가된 diacritics 를 사용하기 때문에, 라틴어 알파벳에서도 문제가 됩니다. 유니코드는 액센트가 있는 문자를 표현하는 두가지 방법이 있기 때문에 문제가 됩니다: 특별 codepoint 가 있는 공통문자들이 많지만, 다른 문자들은 개별 요소들로부터 만들 수 있습니다.

x <- c("á", "x́")
str_length(x)
#> [1] 1 2
# str_width(x)
str_sub(x, 1, 1)
#> [1] "á" "x"

# stri_width(c("全形", "ab"))
# 0, 1, or 2
# but this assumes no font substitution
cyrillic_a <- "А"
latin_a <- "A"
cyrillic_a == latin_a
#> [1] FALSE
stringi::stri_escape_unicode(cyrillic_a)
#> [1] "\\u0410"
stringi::stri_escape_unicode(latin_a)
#> [1] "A"

30.0.3 str_c

NULLs are silently dropped. if 와 함께 사용할 때 특별히 유용합니다:

name <- "Hadley"
time_of_day <- "morning"
birthday <- FALSE

str_c(
  "Good ", time_of_day, " ", name,
  if (birthday) " and HAPPY BIRTHDAY",
  "."
)
#> [1] "Good morning Hadley."

30.0.4 str_dup()

str_c() 와 밀접한 관계를 가진 것은 str_dup() 입니다. str_c(a, a, a)a + a + a 와 같을 때, 3 * a 와 같은 것은 무엇일까요? str_dup() 입니다:

str_dup(letters[1:3], 3)
#> [1] "aaa" "bbb" "ccc"
str_dup("a", 1:3)
#> [1] "a"   "aa"  "aaa"

30.1 성능

fixed(): matches exactly the specified sequence of bytes. It ignores all special regular expressions and operates at a very low level. This allows you to avoid complex escaping and can be much faster than regular expressions. The following microbenchmark shows that it’s about 3x faster for a simple example.

microbenchmark::microbenchmark(
  fixed = str_detect(sentences, fixed("the")),
  regex = str_detect(sentences, "the"),
  times = 20
)
#> Unit: microseconds
#>   expr   min    lq mean median  uq  max neval
#>  fixed  61.5  67.1  166   71.4  83 1714    20
#>  regex 177.8 189.3  209  193.0 203  370    20

As you saw with str_split() you can use boundary() to match boundaries. You can also use it with the other functions:

x <- "This is a sentence."
str_view_all(x, boundary("word"))
str_extract_all(x, boundary("word")) #> [[1]] #> [1] "This" "is" "a" "sentence"

30.1.1 추출

colours <- c("red", "orange", "yellow", "green", "blue", "purple")
colour_match <- str_c(colours, collapse = "|")
colour_match
#> [1] "red|orange|yellow|green|blue|purple"

more <- sentences[str_count(sentences, colour_match) > 1]
str_extract_all(more, colour_match)
#> [[1]]
#> [1] "blue" "red" 
#> 
#> [[2]]
#> [1] "green" "red"  
#> 
#> [[3]]
#> [1] "orange" "red"

If you use simplify = TRUE, str_extract_all() will return a matrix with short matches expanded to the same length as the longest:


str_extract_all(more, colour_match, simplify = TRUE)
#>      [,1]     [,2] 
#> [1,] "blue"   "red"
#> [2,] "green"  "red"
#> [3,] "orange" "red"

x <- c("a", "a b", "a b c")
str_extract_all(x, "[a-z]", simplify = TRUE)
#>      [,1] [,2] [,3]
#> [1,] "a"  ""   ""  
#> [2,] "a"  "b"  ""  
#> [3,] "a"  "b"  "c"

We don’t talk about matrices here, but they are useful elsewhere.

30.1.2 Exercises

  1. From the Harvard sentences data, extract:

    1. The first word from each sentence.
    2. All words ending in ing.
    3. All plurals.

30.2 그룹화 매치

이 장 앞부분에서 연산 우선순위를 명확히 할 목적과 역참조 목적의 괄호 사용에 대해 이야기했었습니다. 이 외에도 복잡한 매치의 일부를 추출하기 위해서도 괄호를 사용할 수 있습니다. 예를 들어 문장에서 명사를 추출하고 싶다고 가정합시다. 휴리스틱 방법으로 “a” 또는 “the” 다음에 오는 단어를 찾아볼 것입니다. 정규표현식에서 “단어” 를 정의하는 것은 약간 까다롭기 때문에, 여기서는 "최소 하나 이상의 문자(공백 제외) 시퀀스’라는 간단한 정의를 이용합니다.

noun <- "(a|the) ([^ ]+)"

has_noun <- sentences %>%
  str_subset(noun) %>%
  head(10)
has_noun %>%
  str_extract(noun)
#>  [1] "the smooth" "the sheet"  "the depth"  "a chicken"  "the parked"
#>  [6] "the sun"    "the huge"   "the ball"   "the woman"  "a helps"

str_extract() 는 완전한 매치를 제공하는 반면, str_match() 는 각각의 개별 요소를 제공합니다. str_match() 는 문자형 벡터 대신 행렬을 반환하는데, 이 행렬에는 완전한 매치가 첫 번째 열을 구성하고, 이어서 각 그룹당 하나씩의 열이 뒤따릅니다:

has_noun %>%
  str_match(noun)
#>       [,1]         [,2]  [,3]     
#>  [1,] "the smooth" "the" "smooth" 
#>  [2,] "the sheet"  "the" "sheet"  
#>  [3,] "the depth"  "the" "depth"  
#>  [4,] "a chicken"  "a"   "chicken"
#>  [5,] "the parked" "the" "parked" 
#>  [6,] "the sun"    "the" "sun"    
#>  [7,] "the huge"   "the" "huge"   
#>  [8,] "the ball"   "the" "ball"   
#>  [9,] "the woman"  "the" "woman"  
#> [10,] "a helps"    "a"   "helps"

(예상했지만, 명사를 검출하는 이 휴리스틱 방법은 좋지 않다. smooth나 parked 같은 형용사도 검출하고 있습니다.)

30.3 분할하기

문자열을 조각으로 분할하려면 str_split() 을 사용하면 됩니다. 예를 들어 문장을 단어로 분할할 수 있습니다:

sentences %>%
  head(5) %>%
  str_split(" ")
#> [[1]]
#> [1] "The"     "birch"   "canoe"   "slid"    "on"      "the"     "smooth" 
#> [8] "planks."
#> 
#> [[2]]
#> [1] "Glue"        "the"         "sheet"       "to"          "the"        
#> [6] "dark"        "blue"        "background."
#> 
#> [[3]]
#> [1] "It's"  "easy"  "to"    "tell"  "the"   "depth" "of"    "a"     "well."
#> 
#> [[4]]
#> [1] "These"   "days"    "a"       "chicken" "leg"     "is"      "a"      
#> [8] "rare"    "dish."  
#> 
#> [[5]]
#> [1] "Rice"   "is"     "often"  "served" "in"     "round"  "bowls."

각 구성요소가 포함하는 조각의 개수가 다를 수 있으므로, 이 함수는 리스트를 반환합니다. 길이가 1인 벡터로 작업하는 경우, 리스트의 첫 번째 요소를 추출하는 것이 가장 쉽습니다.

"a|b|c|d" %>%
  str_split("\\|") %>%
  .[[1]]
#> [1] "a" "b" "c" "d"

한편, 리스트를 반환하는 다른 stringr 함수처럼 simplify = TRUE 를 사용하여 행렬을 반환할 수도 있습니다:

sentences %>%
  head(5) %>%
  str_split(" ", simplify = TRUE)
#>      [,1]    [,2]    [,3]    [,4]      [,5]  [,6]    [,7]     [,8]         
#> [1,] "The"   "birch" "canoe" "slid"    "on"  "the"   "smooth" "planks."    
#> [2,] "Glue"  "the"   "sheet" "to"      "the" "dark"  "blue"   "background."
#> [3,] "It's"  "easy"  "to"    "tell"    "the" "depth" "of"     "a"          
#> [4,] "These" "days"  "a"     "chicken" "leg" "is"    "a"      "rare"       
#> [5,] "Rice"  "is"    "often" "served"  "in"  "round" "bowls." ""           
#>      [,9]   
#> [1,] ""     
#> [2,] ""     
#> [3,] "well."
#> [4,] "dish."
#> [5,] ""

반환할 조각의 최대 개수를 지정할 수도 있습니다:

fields <- c("Name: Hadley", "Country: NZ", "Age: 35")
fields %>% str_split(": ", n = 2, simplify = TRUE)
#>      [,1]      [,2]    
#> [1,] "Name"    "Hadley"
#> [2,] "Country" "NZ"    
#> [3,] "Age"     "35"

또 패턴으로 문자열을 분할하는 대신 boundary() 함수를 사용하여 문자, 줄, 문장 혹은 단어를 경계로 분할할 수도 있습니다:

x <- "This is a sentence.  This is another sentence."
str_view_all(x, boundary("word"))
str_split(x, " ")[[1]] #> [1] "This" "is" "a" "sentence." "" "This" #> [7] "is" "another" "sentence." str_split(x, boundary("word"))[[1]] #> [1] "This" "is" "a" "sentence" "This" "is" "another" #> [8] "sentence"

Show how separate_rows() is a special case of str_split() + summarise().

30.4 함수로 대체하기

30.5 위치

str_locate()str_locate_all() 는 각 매치의 처음과 마지막 위치를 제공합니다. 다른 함수들이 정확히 원하는 것을 하는 함수가 없을 때 특별히 유용합니다. str_locate() 를 사용하여 매칭패턴을 구하고 str_sub() 을 이용하여 추출하거나 수정할 수 있습니다.

30.6 stringi

stringr 은 stringi 패키지 기반으로 만들어졌습니다. stringr 은 학습할 때 유용한데, 왜냐하면 이 패키지는 자주 사용하는 문자열 조작 함수들을 다루기 위해 엄선된 최소한의 함수들만 보여주기 때문입니다. 반면, stringi 는 전체를 포괄하도록 설계 되었고, 필요한 거의 모든 함수를 포함합니다. stringi 에는 256 개의 함수가 있지만, stringr 에는 53 개가 있습니다.

stringr 에서 잘 안 될 경우, stringi 에서 한번 찾아보는 것이 좋습니다. 두 패키지는 매우 유사하게 동작하므로 stringr 에서 배운 것을 자연스럽게 활용할 수 있을 것입니다. 주요 차이점은 접두사입니다 (str_stri_).

30.6.1 Exercises

  1. Find the stringi functions that:

    1. Count the number of words.
    2. Find duplicated strings.
    3. Generate random text.
  2. How do you control the language that stri_sort() uses for sorting?

30.6.2 Exercises

  1. What do the extra and fill arguments do in separate()? Experiment with the various options for the following two toy datasets.

    tibble(x = c("a,b,c", "d,e,f,g", "h,i,j")) %>%
      separate(x, c("one", "two", "three"))
    
    tibble(x = c("a,b,c", "d,e", "f,g,i")) %>%
      separate(x, c("one", "two", "three"))
  2. Both unite() and separate() have a remove argument. What does it do? Why would you set it to FALSE?

  3. Compare and contrast separate() and extract(). Why are there three variations of separation (by position, by separator, and with groups), but only one unite?

  4. In the following example we’re using unite() to create a date column from month and day columns. How would you achieve the same outcome using mutate() and paste() instead of unite?

    events <- tribble(
      ~month, ~day,
      1     , 20,
      1     , 21,
      1     , 22
    )
    
    events %>%
      unite("date", month:day, sep = "-", remove = FALSE)
  5. Write a function that turns (e.g.) a vector c("a", "b", "c") into the string a, b, and c. Think carefully about what it should do if given a vector of length 0, 1, or 2.