트랙
/
R
R
/
학습 과정
/
문자열
문자

문자열 에서 R

2개의 연습 문제

문자열 소개

배경

문자열은 따옴표로 감싼 텍스트예요.

mystring <- "some text"
mystring
#> [1] "some text"
typeof(mystring)
#> [1] "character"

아쉽게도, 다른 언어에 익숙한 프로그래머에게는 이 용어가 헷갈릴 수 있어요.

type이 character이긴 하지만, R은 한 글자(다른 여러 언어에서는 char라고 부르지만, R에서는 아니에요)와 긴 문자열을 구분하지 않아요. 둘 다 하나의 항목이고, 작은따옴표 ' '나 큰따옴표 " " 어느 것으로 감싸도 돼요.

스타일 가이드는 큰따옴표를 쓰기를 권장해요. 다만 문자열 안에 큰따옴표를 이스케이프 없이 쓰고 싶을 때는 예외예요.

str2 <- 'String with "quotes"'  # convenience format
str2
#> [1] "String with \"quotes\""    # standard format

따라서 character vector는 문자열의 벡터이고, 각 문자열이 하나의 원소예요.

s <- c("some", "strings")
s
#> [1] "some"    "strings"
length(s)
#> [1] 2
length(s[1])  # the number of vector elements, not the string length
#> [1] 1

nchar(s[1])  # the number of characters in the string "some"
#> [1] 4

위 예제에서 length(s[1])은 length("some")과 같아요. 즉 _벡터 원소_의 개수이고, _문자열 길이_가 아니에요.

개별 문자열을 조작하는 것도 물론 가능해요. 위 예제의 nchar()처럼요. 다만 아래에서 다룰 특별한 함수들이 필요해요.

이스케이프

위에서 봤듯이, 일부 문장 부호 문자는 백슬래시 \를 앞에 붙여 "이스케이프"해야 해요.

이 주제는 언어 전반에 걸쳐 꽤 잘 표준화되어 있어서, 이 목록은 대부분의 숙련된 프로그래머에게 놀랍지 않을 거예요.

위에서 본 따옴표뿐 아니라, 목록에는 줄바꿈 \n, 탭 \t, 백슬래시 \\, 그리고 \U나 \u로 시작하는 유니코드 기호도 포함돼요.

이스케이프 문자를 많이 쓰는 걸 피하려면, r"( )" 안에 raw 문자열을 정의할 수 있어요:

r"(raw "string")"
#> [1] "raw \"string\""

괄호 ( )를 포함하는 raw 문자열을 위한 문법 옵션이 많이 있어요.

로케일

현재 지원되는 모든 R 버전은 문자열을 UTF-8로 인코딩하므로, 대부분의 세계 언어를 지원할 수 있어요.

"आधुनिक मानक हिन्दी"
#> [1] "आधुनिक मानक हिन्दी"

또한 많은 R 함수가 locale을 인식하고, 문자열 정렬, 대소문자 변환, 날짜와 시간 등에 지역 관례를 따르려고 해요.

이 교육 과정에서는 주로 미국 영어를 예로 들어요. 흔한 기본값이기도 하고, 교육 과정 저자들이 마침 미국에 거주하기 때문이에요.

로케일은 복잡한 주제이니, 자신의 상황에 맞는 문서를 직접 찾아보세요.

문자열 함수

대부분의 작업은 base R에서도 _가능_하지만, 많은 프로그래머가 문자열 조작 지원이 다소 제한적이고 헷갈린다고 느껴요.

다행히도, 훨씬 직관적이고 유연하며 스스로 일관성 있는 tidyverse 패키지들이 있어요.

Base R

구식 문자열 함수를 사용하는 레거시 코드가 아직 많아요. Exercism의 대부분의 커뮤니티 풀이도 여기에 해당해요.

또한 작은 작업이라면 큰 라이브러리를 불러오는 대신 내장 함수를 쓰는 게 유용할 수 있어요. R 트랙 유지보수자들은 GitHub 저장소에서 연습 문제를 검증하기 위한 예시 풀이를 작성할 때 이 방식을 택하는 편이에요.

따라서 예전 방식에 대한 지식도 알아 둘 가치가 있어요.

아래 함수들은 가장 단순한 형태로 소개하지만, 대부분은 선택적 인자도 가지고 있어요. 자세한 내용은 문서를 참고해요.

nchar

문자열의 문자 개수를 반환해요.

nchar("string")
#> [1] 6

nchar("Hrōðgār") # a person in Beowulf, written in Old English
#> [1] 7

"문자"가 무엇을 뜻하는지에 관한 (복잡한!) 주제는 이 범위를 벗어나요. 위 예제에서 몇 가지만 짚어 볼게요:

  • 내부적으로 C로 구현되었지만, R의 문자열은 \0로 종료되지 않아요(그러면 길이에 1이 더해질 텐데요).
  • 여러 바이트를 차지하는 유니코드 문자도 기대하는 대로 처리돼요. "Hrōðgār"은 7개의 문자(또는 "룬"이나 "코드 포인트"라고 불러요)를 가지지만, 저장에는 10바이트가 필요해요.

nchar에 무엇을 전달하는지 조심해요. character가 아닌 값에 대해서는 결과가 여전히 예측 불가능할 수 있어요. 최근 R 버전에서는 나아졌지만요.

grep

문자열 벡터에서 패턴을 찾아, 일치하는 위치의 인덱스를 반환해요.

grep("Jav", c("Java", "Python", "Javascript"))
#> [1] 1 3
paste and paste0

임의 개수의 문자열을 하나의 문자열로 결합해요.

paste에는 선택적으로 구분자를 지정할 수 있어요. 기본값은 공백이에요.

paste0(v)는 paste(v, sep = "")와 같아요.

paste("bits", "of", "a", "string")
#> [1] "bits of a string"
paste0("bits", "of", "a", "string")
#> [1] "bitsofastring"
sprintf

문자열 조립을 더 세밀하게 제어하기 위해, R은 C의 sprintf 함수를 가져왔어요(그리고 이후의 여러 언어도 마찬가지예요. 위키백과에는 약 30개의 언어가 나열되어 있어요).

r <- 5.3
pi <- 3.14159
sprintf("A circle of radius %.1f has area %.2f", r, pi * r^2)
#> [1] "A circle of radius 5.3 has area 88.25"
trimws

앞뒤의 공백은 trimws()로 제거할 수 있어요:

s <- "    messy string   "
trimws(s)
#> [1] "messy string"

양쪽 끝을 모두 다듬지 않도록 which = "left"(또는 "right") 매개변수도 있어요.

substr

하나의 문자열에 작동하며, 두 (양 끝을 포함하는) 경계 사이의 부분 문자열을 반환해요.

substr("Exercism", 5, 7)
#> [1] "cis"
strsplit

구분자 문자열을 기준으로 문자열을 R 리스트로 나눠요. 구분자는 필수예요(기본값이 없어요). 빈 문자열도 가능해요.

strsplit("R, Python, Julia", ", ")
[[1]]
#> [1] "R"      "Python" "Julia" 

strsplit("Exercism", "")
[[1]]
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"

리스트는 별도의 개념에서 다뤄요. 아직 교육 과정에서 그 부분에 도달하지 않았다면, 지금은 unlist() 함수가 (이 경우) 리스트를 벡터로 변환한다는 것만 알아 두세요.

unlist(strsplit("Exercism", ""))
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"
regexpr and regexpr

"정규 표현식"의 줄임말인 이 함수들은 문자열에서 패턴의 출현을 찾아, (잠재적으로) 유용한 데이터의 리스트를 반환해요. 완전성을 위해 언급할 뿐이에요: 더 나은 선택지가 있어요!

regexpr은 첫 번째 출현만 반환하고, gregexpr은 모든 출현을 반환해요("g"는 "전역" 검색을 뜻해요).

이름에서 알 수 있듯이, 패턴은 정규식일 수 있는데, 이는 나중 개념에서 다뤄요.

Advanced

왜 이 예전 문자열 함수들은 Unix 셸 명령어처럼 생겼을까요?

R은 1993년에 만들어졌지만, S 언어와 대부분 하위 호환되도록 설계됐어요.

S는 Bell Labs에서 설계되어 1976년에 처음 등장했어요. Bell Labs는 Unix(1969년부터)와 C 언어(1972년)도 만들어냈어요.

훨씬 이전 시대치고는 정말 대단한 팀이었어요! 그들이 서로 이야기하고 아이디어를 나눴으리라고 짐작할 수밖에 없어요.

Tidyverse

좋게 말하자면, 예전 방식의 문자열 함수가 R이 제공하는 최선은 아니에요(덜 좋게 보는 시각도 있어요).

이 상황을 개선하기 위해, stringr 라이브러리가 2009년부터 개발되어 왔고, 지금은 Tidyverse 모음의 핵심 부분이에요.

library(stringr)나 library(tidyverse)로 네임스페이스에 불러와요.

몇 가지 핵심 특징은 다음과 같아요:

  • 모든 함수 이름에 str_ 접두사가 붙어 목적을 분명히 하려고 해요.
  • 인자의 순서는 입력 데이터를 첫 번째 인자로 두어 스스로 일관성을 유지하려고 해요.
  • 입력은 단일 문자열부터 복잡한 데이터 구조까지 다양할 수 있어요. 함수는 상황에 맞춰 가장 합리적인 일을 하려고 해요.

모든 Tidyverse 패키지처럼, stringr은 좋은 문서를 갖추고 있어요. 보통 패키지 작성자가 쓴 문서예요.

stringr에는 수십 개의 함수가 있고, 대개 다양한 선택적 인자를 가지고 있어요. 처음에는 다소 벅찰 수 있으니, 문서가 친구가 되어 줄 거예요.

무엇을 넣고 싶은지(단일 문자열? 벡터? 더 복잡한 무언가?), 무엇을 얻고 싶은지 생각해 봐요.

올바른 함수와 올바른 인자를 찾을 수 있다면, 원하는 것을 하는 쉬운 방법이 아마 있을 거예요.

잠깐: 파이프 빠르게 훑어보기

함수를 연결하는 파이프 |>의 사용은 교육 과정의 다른 부분에서 더 자세히 다뤄요.

지금은 다음을 알아 두면 유용해요:

  • 파이프 왼쪽의 결과가 오른쪽 함수의 첫 번째 인자가 돼요(여기서 "오른쪽"은 선택적 줄바꿈을 무시해요).
  • 나머지 인자는 함수 호출에 포함할 수 있어요.
  • R에서는 _이름_이 아니라 함수 _호출_로 파이프해야 하므로, 괄호가 항상 필요해요(이는 일부 다른 언어와 달라요).

아래 예제가 문법을 보여 줘요. 개별 함수는 나중에 다뤄요.

library(stringr)

"Monday-25-February" |> 
  str_split("-") |>     
  unlist() |>               # [1] "Monday"   "25"       "February"
  str_sub(1, 3) |>          # [1] "Mon" "25"  "Feb"
  str_to_upper() |>         # [1] "MON" "25"  "FEB"
  str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"
길이 다루기

str_length는 코드 포인트를 세요(nchars와 비슷해요).

str_trim()과 str_squish()을 비롯한 여러 다른 함수가 공백을 다뤄요. 이는 어떤 데이터 과학 프로젝트든 시작할 때 흔히 꼭 필요한 정리 작업이에요.

# The first 3 kernels in JuPyteR notebooks:
str_length(c("Julia", "Python", "R"))           #> [1] 1 6 5
#> [1] 5 6 1
str_pad(c("Julia", "Python", "R"), 8, "right")  # add spaces if necessary
#> [1] "Julia   " "Python  " "R       "

str_trim("Julia    Python    R    ")   # remove leading & trailing whitespace
#> [1] "Julia    Python    R"
str_squish("Julia   Python  R    ")  # trim, collapse multiple spaces 
#> [1] "Julia Python R"
문자열 부분 추출

str_sub(string, start, end) 함수는 start부터 end까지(양 끝 포함)의 부분 문자열을 반환하고, str_sub_all()은 여러 문자열에 작동할 수 있어요.

유연성이 아주 커요. start와 end는 각각 첫 번째와 마지막 문자를 기본값으로 해요. 인덱스의 벡터일 수도 있어요. 벡터 인덱싱과는 달리 반갑게도, 음수 값은 문자열 끝에서부터 거꾸로 세요. Python 방식이에요.

s <- "abcdefgh"
str_sub(s, 2, 4)
#> [1] "bcd"
str_sub(s, 5)
#> [1] "efgh"
str_sub(s, -3, -2)
#> [1] "fg"
str_sub(s, c(1, 4), c(2, 5))
#> [1] "ab" "de"
str_sub(s, c(1, 4, 6), c(2, 5, -1))
#> [1] "ab"  "de"  "fgh"

s |> str_sub(2, 4) # these functions are designed for pipes
#> [1] "bcd"

# str_sub_all takes vector input, gives list output
str_sub_all(c(s, "ijklmnop"), c(1, 4, 6), c(2, 5, -1))
[[1]]
#> [1] "ab"  "de"  "fgh"

[[2]]
#> [1] "ij"  "lm"  "nop"
문자열 합치기

str_flatten은 character 벡터를 하나의 문자열로 변환해요. collapse 인자는 각 원소 사이에 삽입되고 기본값은 ""예요.

c("R", "Julia", "Python") |> str_flatten()
#> [1] "RJuliaPython"
c("R", "Julia", "Python") |> str_flatten(collapse = " - ")
#> [1] "R - Julia - Python"

str_c는 여러 character 벡터를 하나의 character 벡터로 변환하며, 필요에 따라 재활용해요.

str_c(LETTERS[1:8], 1:8, sep = ":")
#> [1] "A:1" "B:2" "C:3" "D:4" "E:5" "F:6" "G:7" "H:8"

str_glue는 간단한 문자열 보간을 R에 가져와요. 중괄호 { } 안의 표현식이 해석되고 그 텍스트 표현이 대체돼요.

여러 표현식 문자열을 제공하면, str_flatten에서처럼 합쳐져요. 문자열의 앞뒤 공백을 선택적으로 다듬을 수도 있어요.

r <- 5.3
pi <- 3.14159
str_glue("A circle of radius {r} has area {pi * r^2}")
A circle of radius 5.3 has area 88.2472631

str_glue는 유연하지만(그리고 꽤 복잡하지만), 훨씬 더 넓은 가능성이 그 기반이 되는 glue 라이브러리에 있어요.

문자열 나누기

R의 아주 흔한 용도는 지저분한 데이터를 분석하기 좋은 일관된 형식으로 다듬는 것이에요. 이를 돕기 위해, 텍스트를 부분 문자열로 나누는 여러 방법이 있어요.

str_split_1이 가장 단순해요. 하나의 문자열을 지정된 패턴에서 나눠 조각들의 벡터로 변환해요(기본 패턴은 없어요).

s <- "R Julia Python"
str_split_1(s, " ")
#> [1] "R"      "Julia"  "Python"

str_split_i는 character 벡터와 인덱스 n을 받아, 각 입력 문자열의 n번째 부분 문자열의 벡터를 반환해요.

s2 <- c(s, "C Kotlin, F#")  #> [1] "R Julia Python" "C Kotlin, F#"
str_split_i(s2, " ", 2)  # 2nd element of each
#> [1] "Julia"   "Kotlin,"

str_split은 character 벡터를 받아, 벡터들의 리스트를 반환해요. 출력을 설명하는 리스트 개념이 우리 교육 과정에 있어요.

str_split(s2, " ")
[[1]]
#> [1] "R"      "Julia"  "Python"

[[2]]
#> [1] "C"       "Kotlin," "F#"     

행렬을 반환하는 str_split_fixed도 있어요. 행렬은 나중 개념에서 다뤄요.

문자열 일치

전체 문자열을 일치시키기 위해, str_equal()은 다양한 "동등함"의 정의를 사용할 수 있어요. 가장 단순하게는, 기본값이 대소문자를 구분하지만 바꿀 수 있어요. 로케일과 유니코드는 훨씬 더 많은 가능성을 만들어요.

str_equal("Odin", "odin")
#> [1] FALSE
str_equal("Odin", "odin", ignore_case = TRUE)
#> [1] TRUE

많은 다른 함수가 있는데, 정규 표현식 개념에서 더 자세히 다뤄요.

문자열 변형

일부 언어(주로 유럽 언어)는 대문자와 소문자를 구분해요. 이들을 로케일에 맞게 상호 변환하는 stringr 함수들이 있어요:

  • str_to_lower()
  • str_to_upper()
  • str_to_title() : 각 단어의 첫 글자를 대문자로
  • str_to_sentence() : 문장의 첫 글자를 대문자로
str_to_upper("elixir")    # defaults to locale = "en"
#> [1] "ELIXIR"
str_to_upper("ελληνικά")  # infers Greek
#> [1] "ΕΛΛΗΝΙΚΆ"

str_to_title("the cat sat on the mat")
#> [1] "The Cat Sat On The Mat"
str_to_sentence("the cat sat on the mat")
#> [1] "The cat sat on the mat"

패턴 일치를 기반으로 한 부분 문자열 교체에는, str_replace()가 패턴의 첫 번째 출현을 바꾸고, str_replace_all()이 모든 출현을 바꿔요.

문자 인덱싱을 기반으로 한 부분 문자열 교체에는, str_sub()의 할당 형식이 있어요.

 s <- "abcde"
str_sub(s, 3, 4) <- "yz"
s
#> [1] "abyze"
기타

str_dup은 복제본을 만들고 str_unique는 중복을 제거해요.

기본적으로 str_dup에는 구분자가 없지만, 지정할 수 있어요.

str_dup("xyz", 4, sep = ", ")
#> [1] "xyz, xyz, xyz, xyz"

str_unique는 unique()의 확장이에요. 둘 다 벡터에서 중복 항목을 제거하지만, stringr 함수는 대소문자 구분과 다양한 로케일별 일치도 처리할 수 있어요.

inp <- c("some", "strings", "Some", "Strings")
str_unique(inp)
#> [1] "some"    "strings" "Some"    "Strings"
str_unique(inp, ignore_case = TRUE)
#> [1] "some"    "strings"
Advanced

stringr 라이브러리는 저수준 함수들의 라이브러리인 stringi를 기반으로 해요.

위에서 다루지 않은 특별한 필요가 있다면:

  1. stringr 문서를 확인해 봐요. 이 긴 개념 문서에서 언급할 수 있는 것보다 더 많은 옵션을 다뤄요.
  2. stringi 문서를 확인해 봐요. 훨씬 더 많은 기능을 설명해요.
GitHub에서 편집 링크가 새 창이나 탭에서 열려요

문자열 배우기

연습이 잠겨 있어요

문자열 개념을 연습하려면 연습 문제 1개를 더 잠금 해제해요