15 정규 표현식 (Regular expressions)
15.1 소개 (Introduction)
Chapter 14 장에서 문자열 작업을 위한 유용한 함수들을 많이 배웠습니다. 이번 장에서는 문자열 내의 패턴을 설명하는 간결하고 강력한 언어인 정규 표현식(regular expressions)을 사용하는 함수들에 초점을 맞출 것입니다. “정규 표현식”이라는 용어는 발음하기 좀 길기 때문에 대부분의 사람들은 “regex”1 또는 “regexp”로 줄여서 부릅니다.
이 장은 정규 표현식의 기초와 데이터 분석에 가장 유용한 stringr 함수들로 시작합니다. 그런 다음 패턴에 대한 지식을 확장하여 7가지 중요한 새 주제(이스케이프, 앵커, 문자 클래스, 단축 클래스, 수량자, 우선순위, 그룹화)를 다룰 것입니다. 다음으로 stringr 함수가 작업할 수 있는 다른 유형의 패턴들과 정규 표현식의 동작을 조정할 수 있는 다양한 “플래그(flags)”에 대해 이야기할 것입니다. 마지막으로 tidyverse와 기본 R(base R)의 다른 곳에서 정규 표현식을 사용할 수 있는 경우들을 살펴보며 마무리하겠습니다.
15.1.1 선수 과목 (Prerequisites)
이 장에서는 tidyverse의 핵심 멤버인 stringr과 tidyr의 정규 표현식 함수들과 babynames 패키지의 데이터를 사용할 것입니다.
이 장 전반에 걸쳐 기본 아이디어를 얻을 수 있는 매우 간단한 인라인 예제, 아기 이름 데이터, 그리고 stringr의 세 가지 문자 벡터를 혼합하여 사용할 것입니다:
-
fruit: 80가지 과일 이름이 들어 있습니다. -
words: 980개의 일반적인 영어 단어가 들어 있습니다. -
sentences: 720개의 짧은 문장이 들어 있습니다.
15.2 패턴 기초 (Pattern basics)
str_view()를 사용하여 정규 표현식 패턴이 어떻게 작동하는지 알아볼 것입니다. 지난 장에서는 문자열과 출력된 표현을 더 잘 이해하기 위해 str_view()를 사용했지만, 이제는 두 번째 인수로 정규 표현식을 사용하여 활용할 것입니다. 정규 표현식이 제공되면 str_view()는 일치하는 문자열 벡터의 요소만 보여주며, 각 일치 항목을 <>로 감싸고 가능한 경우 파란색으로 강조 표시합니다.
가장 간단한 패턴은 문자와 숫자로 구성되며, 이는 해당 문자와 정확히 일치합니다:
str_view(fruit, "berry")
#> [6] │ bil<berry>
#> [7] │ black<berry>
#> [10] │ blue<berry>
#> [11] │ boysen<berry>
#> [19] │ cloud<berry>
#> [21] │ cran<berry>
#> ... and 8 more문자와 숫자는 정확히 일치하며 이를 리터럴 문자(literal characters)라고 합니다. ., +, *, [, ], ?와 같은 대부분의 구두점 문자는 특별한 의미2를 가지며 메타문자(metacharacters)라고 합니다. 예를 들어, .은 어떤 문자3와도 일치하므로 "a."는 “a” 뒤에 다른 문자가 오는 모든 문자열과 일치합니다:
또는 “a” 뒤에 세 글자가 오고 그 뒤에 “e”가 오는 모든 과일을 찾을 수도 있습니다:
str_view(fruit, "a...e")
#> [1] │ <apple>
#> [7] │ bl<ackbe>rry
#> [48] │ mand<arine>
#> [51] │ nect<arine>
#> [62] │ pine<apple>
#> [64] │ pomegr<anate>
#> ... and 2 more수량자(Quantifiers)는 패턴이 일치할 수 있는 횟수를 제어합니다:
-
?: 패턴을 선택적으로 만듭니다 (즉, 0회 또는 1회 일치). -
+: 패턴을 반복하게 합니다 (즈, 적어도 1회 이상 일치). -
*: 패턴을 선택적이거나 반복하게 합니다 (즈, 0회를 포함하여 횟수에 상관없이 일치).
# ab?는 "a" 뒤에 선택적으로 "b"가 오는 것과 일치합니다.
str_view(c("a", "ab", "abb"), "ab?")
#> [1] │ <a>
#> [2] │ <ab>
#> [3] │ <ab>b
# ab+는 "a" 뒤에 적어도 하나 이상의 "b"가 오는 것과 일치합니다.
str_view(c("a", "ab", "abb"), "ab+")
#> [2] │ <ab>
#> [3] │ <abb>
# ab*는 "a" 뒤에 "b"가 횟수에 상관없이(0회 포함) 오는 것과 일치합니다.
str_view(c("a", "ab", "abb"), "ab*")
#> [1] │ <a>
#> [2] │ <ab>
#> [3] │ <abb>문자 클래스(Character classes)는 []로 정의되며 문자 집합과 일치시킬 수 있습니다. 예를 들어 [abcd]는 “a”, “b”, “c” 또는 “d”와 일치합니다. ^로 시작하여 일치를 반전시킬 수도 있습니다: [^abcd]는 “a”, “b”, “c”, “d”를 제외한 모든 것과 일치합니다. 이 아이디어를 사용하여 모음으로 둘러싸인 “x”나 자음으로 둘러싸인 “y”가 포함된 단어를 찾을 수 있습니다:
대안(alternation)인 |를 사용하여 하나 이상의 대체 패턴 중에서 선택할 수 있습니다. 예를 들어, 다음 패턴은 “apple”, “melon”, “nut” 또는 반복되는 모음이 포함된 과일을 찾습니다.
str_view(fruit, "apple|melon|nut")
#> [1] │ <apple>
#> [13] │ canary <melon>
#> [20] │ coco<nut>
#> [52] │ <nut>
#> [62] │ pine<apple>
#> [72] │ rock <melon>
#> ... and 1 more
str_view(fruit, "aa|ee|ii|oo|uu")
#> [9] │ bl<oo>d orange
#> [33] │ g<oo>seberry
#> [47] │ lych<ee>
#> [66] │ purple mangost<ee>n정규 표현식은 매우 간결하고 많은 구두점 문자를 사용하기 때문에 처음에는 압도적이고 읽기 어려워 보일 수 있습니다. 걱정하지 마세요. 연습하면 나아질 것이고, 간단한 패턴은 곧 자연스러워질 것입니다. 유용한 stringr 함수들로 연습하며 그 과정을 시작해 봅시다.
15.3 주요 함수 (Key functions)
이제 정규 표현식의 기초를 익혔으니, stringr 및 tidyr 함수들과 함께 사용해 봅시다. 다음 섹션에서는 일치 항목의 존재 여부 감지, 일치 횟수 계산, 일치 항목을 고정 텍스트로 교체, 패턴을 사용한 텍스트 추출 방법을 배울 것입니다.
15.3.1 일치 감지 (Detect matches)
str_detect()는 패턴이 문자 벡터의 요소와 일치하면 TRUE, 그렇지 않으면 FALSE인 논리형 벡터를 반환합니다:
str_detect(c("a", "b", "c"), "[aeiou]")
#> [1] TRUE FALSE FALSEstr_detect()는 초기 벡터와 길이가 같은 논리형 벡터를 반환하므로 filter()와 잘 어울립니다. 예를 들어, 이 코드는 소문자 “x”가 포함된 가장 인기 있는 이름들을 찾습니다:
babynames |>
filter(str_detect(name, "x")) |>
count(name, wt = n, sort = TRUE)
#> # A tibble: 974 × 2
#> name n
#> <chr> <int>
#> 1 Alexander 665492
#> 2 Alexis 399551
#> 3 Alex 278705
#> 4 Alexandra 232223
#> 5 Max 148787
#> 6 Alexa 123032
#> # ℹ 968 more rows또한 str_detect()를 sum() 또는 mean()과 짝을 지어 summarize()와 함께 사용할 수도 있습니다: sum(str_detect(x, pattern))은 일치하는 관측값의 수를 알려주고, mean(str_detect(x, pattern))은 일치하는 비율을 알려줍니다. 예를 들어, 다음 스니펫은 “x”가 포함된 아기 이름4의 비율을 연도별로 계산하고 시각화합니다. 최근 들어 인기가 급격히 증가한 것 같네요!
str_detect()와 밀접하게 관련된 두 가지 함수가 있습니다: str_subset()과 str_which()입니다. str_subset()은 일치하는 문자열만 포함하는 문자 벡터를 반환합니다. str_which()는 일치하는 문자열의 위치를 나타내는 정수 벡터를 반환합니다.
15.3.2 일치 횟수 계산 (Count matches)
str_detect()보다 한 단계 더 복잡한 것은 str_count()입니다: 참 또는 거짓 대신 각 문자열에 일치하는 항목이 몇 개인지 알려줍니다.
각 일치는 이전 일치가 끝난 곳에서 시작한다는 점에 유의하세요. 즉, 정규 표현식 일치는 절대 겹치지 않습니다. 예를 들어, "abababa"에서 "aba" 패턴은 몇 번 일치할까요? 정규 표현식은 세 번이 아니라 두 번이라고 말합니다:
str_count()를 mutate()와 함께 사용하는 것은 자연스럽습니다. 다음 예제는 str_count()와 문자 클래스를 사용하여 각 이름의 모음과 자음 수를 계산합니다.
babynames |>
count(name) |>
mutate(
vowels = str_count(name, "[aeiou]"),
consonants = str_count(name, "[^aeiou]")
)
#> # A tibble: 97,310 × 4
#> name n vowels consonants
#> <chr> <int> <int> <int>
#> 1 Aaban 10 2 3
#> 2 Aabha 5 2 3
#> 3 Aabid 2 2 3
#> 4 Aabir 1 2 3
#> 5 Aabriella 5 4 5
#> 6 Aada 1 2 2
#> # ℹ 97,304 more rows자세히 보면 계산에 뭔가 이상한 점이 있음을 알 수 있습니다: “Aaban”에는 “a”가 세 개 있지만 요약에는 모음이 두 개뿐이라고 보고합니다. 이는 정규 표현식이 대소문자를 구분하기 때문입니다. 이 문제를 해결할 수 있는 세 가지 방법이 있습니다:
- 문자 클래스에 대문자 모음을 추가합니다:
str_count(name, "[aeiouAEIOU]"). - 정규 표현식에 대소문자를 무시하도록 지시합니다:
str_count(name, regex("[aeiou]", ignore_case = TRUE)). 이에 대해서는 Section 15.5.1 에서 더 자세히 다룰 것입니다. -
str_to_lower()를 사용하여 이름을 소문자로 변환합니다:str_count(str_to_lower(name), "[aeiou]").
이러한 다양한 접근 방식은 문자열 작업을 할 때 꽤 일반적입니다. 패턴을 더 복잡하게 만들거나 문자열에 전처리를 수행하는 등 목표에 도달하는 방법은 종종 여러 가지가 있습니다. 한 가지 접근 방식으로 막히면 기어를 바꿔 다른 관점에서 문제를 해결하는 것이 종종 유용할 수 있습니다.
이 경우 이름에 두 가지 함수를 적용하고 있으므로 먼저 변환하는 것이 더 쉽다고 생각합니다:
babynames |>
count(name) |>
mutate(
name = str_to_lower(name),
vowels = str_count(name, "[aeiou]"),
consonants = str_count(name, "[^aeiou]")
)
#> # A tibble: 97,310 × 4
#> name n vowels consonants
#> <chr> <int> <int> <int>
#> 1 aaban 10 3 2
#> 2 aabha 5 3 2
#> 3 aabid 2 3 2
#> 4 aabir 1 3 2
#> 5 aabriella 5 5 4
#> 6 aada 1 3 1
#> # ℹ 97,304 more rows15.3.3 값 교체 (Replace values)
일치 항목을 감지하고 계산하는 것 외에도 str_replace()와 str_replace_all()을 사용하여 수정할 수도 있습니다. str_replace()는 첫 번째 일치 항목을 교체하고, 이름에서 알 수 있듯이 str_replace_all()은 모든 일치 항목을 교체합니다.
x <- c("apple", "pear", "banana")
str_replace_all(x, "[aeiou]", "-")
#> [1] "-ppl-" "p--r" "b-n-n-"str_remove()와 str_remove_all()은 str_replace(x, pattern, "")의 편리한 단축형입니다:
x <- c("apple", "pear", "banana")
str_remove_all(x, "[aeiou]")
#> [1] "ppl" "pr" "bnn"이 함수들은 데이터 정리를 할 때 mutate()와 자연스럽게 짝을 이루며, 일관성 없는 서식의 층을 벗겨내기 위해 반복적으로 적용하는 경우가 많습니다.
15.3.4 변수 추출 (Extract variables)
마지막으로 논의할 함수는 정규 표현식을 사용하여 한 열의 데이터를 하나 이상의 새 열로 추출하는 separate_wider_regex()입니다. 이것은 Section 14.4.2 에서 배운 separate_wider_position() 및 separate_wider_delim() 함수의 동료입니다. 이 함수들은 개별 벡터가 아닌 데이터 프레임의 (열)에서 작동하기 때문에 tidyr에 있습니다.
작동 방식을 보여주기 위해 간단한 데이터셋을 만들어 보겠습니다. 여기 babynames에서 파생된 데이터가 있는데, 이름, 성별, 나이가 다소 이상한 형식5으로 되어 있습니다:
df <- tribble(
~str,
"<Sheryl>-F_34",
"<Kisha>-F_45",
"<Brandon>-N_33",
"<Sharon>-F_38",
"<Penny>-F_58",
"<Justin>-M_41",
"<Patricia>-F_84",
)separate_wider_regex()를 사용하여 이 데이터를 추출하려면 각 조각과 일치하는 정규 표현식 시퀀스를 구성하기만 하면 됩니다. 해당 조각의 내용이 출력에 나타나게 하려면 이름을 지정합니다:
df |>
separate_wider_regex(
str,
patterns = c(
"<",
name = "[A-Za-z]+",
">-",
gender = ".",
"_",
age = "[0-9]+"
)
)
#> # A tibble: 7 × 3
#> name gender age
#> <chr> <chr> <chr>
#> 1 Sheryl F 34
#> 2 Kisha F 45
#> 3 Brandon N 33
#> 4 Sharon F 38
#> 5 Penny F 58
#> 6 Justin M 41
#> # ℹ 1 more row일치에 실패하면 separate_wider_delim() 및 separate_wider_position()과 마찬가지로 too_few = "debug"를 사용하여 무엇이 잘못되었는지 파악할 수 있습니다.
15.3.5 연습문제 (Exercises)
모음이 가장 많은 아기 이름은 무엇인가요? 모음의 비율이 가장 높은 이름은 무엇인가요? (힌트: 분모는 무엇인가요?)
"a/b/c/d/e"의 모든 슬래시(forward slashes)를 백슬래시(backslashes)로 바꾸세요. 모든 백슬래시를 슬래시로 바꾸어 변환을 취소하려고 하면 어떻게 되나요? (이 문제는 곧 논의할 것입니다.)str_replace_all()을 사용하여 간단한 버전의str_to_lower()를 구현하세요.여러분의 국가에서 일반적으로 쓰이는 전화번호와 일치하는 정규 표현식을 만드세요.
15.4 패턴 상세 (Pattern details)
이제 패턴 언어의 기초와 이를 stringr 및 tidyr 함수와 함께 사용하는 방법을 이해했으므로, 더 자세한 내용을 파헤쳐 볼 시간입니다. 먼저, 특별하게 취급될 메타문자를 일치시킬 수 있게 해주는 이스케이프(escaping)로 시작할 것입니다. 다음으로 문자열의 시작이나 끝을 일치시킬 수 있게 해주는 앵커(anchors)에 대해 배울 것입니다. 그 다음, 집합의 모든 문자와 일치시킬 수 있게 해주는 문자 클래스(character classes)와 그 단축형에 대해 더 자세히 알아볼 것입니다. 다음으로 패턴이 일치할 수 있는 횟수를 제어하는 수량자(quantifiers)의 마지막 세부 사항을 배울 것입니다. 그리고 중요하지만 복잡한 주제인 연산자 우선순위(operator precedence)와 괄호를 다뤄야 합니다. 마지막으로 패턴의 구성 요소를 그룹화(grouping)하는 세부 사항으로 마무리하겠습니다.
여기서 사용하는 용어는 각 구성 요소의 기술적인 이름입니다. 항상 그 목적을 가장 잘 연상시키는 것은 아니지만, 나중에 더 자세한 내용을 구글링하려면 정확한 용어를 아는 것이 매우 도움이 됩니다.
15.4.1 이스케이프 (Escaping)
리터럴 .과 일치시키려면 정규 표현식에 메타문자6를 문자 그대로 일치시키도록 지시하는 이스케이프(escape)가 필요합니다. 문자열과 마찬가지로 정규 표현식은 이스케이프에 백슬래시를 사용합니다. 따라서 .과 일치시키려면 정규 표현식 \.이 필요합니다. 불행히도 이것은 문제를 일으킵니다. 우리는 정규 표현식을 나타내기 위해 문자열을 사용하며, \는 문자열에서도 이스케이프 기호로 사용됩니다. 따라서 정규 표현식 \.을 만들려면 다음 예제와 같이 문자열 "\\."이 필요합니다.
이 책에서는 보통 \.와 같이 따옴표 없이 정규 표현식을 작성할 것입니다. 실제로 입력해야 할 내용을 강조해야 하는 경우 "\\."와 같이 따옴표로 묶고 추가 이스케이프를 추가할 것입니다.
\가 정규 표현식에서 이스케이프 문자로 사용된다면, 리터럴 \는 어떻게 일치시킬까요? 음, 그것을 이스케이프하여 정규 표현식 \\를 만들어야 합니다. 그 정규 표현식을 만들려면 문자열을 사용해야 하는데, 문자열도 \를 이스케이프해야 합니다. 즉, 리터럴 \ 하나와 일치시키려면 "\\\\"를 작성해야 합니다. 하나를 일치시키기 위해 백슬래시 네 개가 필요합니다!
대안으로, Section 14.2.2 에서 배운 원시 문자열(raw strings)을 사용하는 것이 더 쉬울 수 있습니다. 이렇게 하면 이스케이프 계층 하나를 피할 수 있습니다:
str_view(x, r"{\\}")
#> [1] │ a<\>b리터럴 ., $, |, *, +, ?, {, }, (, )와 일치시키려는 경우 백슬래시 이스케이프를 사용하는 대신 문자 클래스를 사용할 수 있습니다: [.], [$], [|], … 모두 리터럴 값과 일치합니다.
15.4.2 앵커 (Anchors)
기본적으로 정규 표현식은 문자열의 어느 부분과도 일치합니다. 시작이나 끝에서 일치시키려면 ^를 사용하여 시작을 일치시키거나 $를 사용하여 끝을 일치시키도록 정규 표현식을 앵커(anchor)해야 합니다:
$가 문자열의 시작과 일치해야 한다고 생각하기 쉬운데, 달러 금액을 그렇게 쓰기 때문입니다. 하지만 정규 표현식이 원하는 것은 그게 아닙니다.
정규 표현식이 전체 문자열과만 일치하도록 강제하려면 ^와 $로 모두 앵커를 걸어야 합니다:
또한 \b를 사용하여 단어 사이의 경계(즉, 단어의 시작 또는 끝)를 일치시킬 수 있습니다. 이는 RStudio의 찾기 및 바꾸기 도구를 사용할 때 특히 유용할 수 있습니다. 예를 들어, sum()의 모든 사용을 찾으려면 \bsum\b를 검색하여 summarize, summary, rowsum 등과 일치하는 것을 피할 수 있습니다:
단독으로 사용될 때 앵커는 너비가 0인 일치(zero-width match)를 생성합니다:
이것은 독립형 앵커를 교체할 때 어떤 일이 발생하는지 이해하는 데 도움이 됩니다:
str_replace_all("abc", c("$", "^", "\\b"), "--")
#> [1] "abc--" "--abc" "--abc--"15.4.3 문자 클래스 (Character classes)
문자 클래스 또는 문자 집합을 사용하면 집합 내의 모든 문자와 일치시킬 수 있습니다. 위에서 논의했듯이 []를 사용하여 자신만의 집합을 구성할 수 있습니다. 여기서 [abc]는 “a”, “b” 또는 “c”와 일치하고 [^abc]는 “a”, “b”, “c”를 제외한 모든 문자와 일치합니다. ^ 외에도 [] 내부에서 특별한 의미를 갖는 두 가지 다른 문자가 있습니다:
-
-는 범위를 정의합니다. 예를 들어[a-z]는 모든 소문자와 일치하고[0-9]는 모든 숫자와 일치합니다. -
\는 특수 문자를 이스케이프하므로[\^\-\]]는^,-또는]와 일치합니다.
몇 가지 예는 다음과 같습니다:
x <- "abcd ABCD 12345 -!@#%."
str_view(x, "[abc]+")
#> [1] │ <abc>d ABCD 12345 -!@#%.
str_view(x, "[a-z]+")
#> [1] │ <abcd> ABCD 12345 -!@#%.
str_view(x, "[^a-z0-9]+")
#> [1] │ abcd< ABCD >12345< -!@#%.>
# [] 내부에서 특별한 문자를 일치시키려면 이스케이프가 필요합니다.
str_view("a-b-c", "[a-c]")
#> [1] │ <a>-<b>-<c>
str_view("a-b-c", "[a\\-c]")
#> [1] │ <a><->b<-><c>일부 문자 클래스는 너무 자주 사용되어 자체 단축형을 가지고 있습니다. 이미 .을 보았는데, 이는 개행 문자를 제외한 모든 문자와 일치합니다. 특히 유용한 다른 세 쌍이 있습니다7:
-
\d는 모든 숫자와 일치합니다;\D는 숫자가 아닌 모든 것과 일치합니다. -
\s는 모든 공백(예: 스페이스, 탭, 개행)과 일치합니다;\S는 공백이 아닌 모든 것과 일치합니다. -
\w는 모든 “단어” 문자, 즉 문자와 숫자와 일치합니다;\W는 모든 “비단어” 문자와 일치합니다.
다음 코드는 문자, 숫자 및 구두점 문자의 선택으로 6가지 단축형을 보여줍니다.
x <- "abcd ABCD 12345 -!@#%."
str_view(x, "\\d+")
#> [1] │ abcd ABCD <12345> -!@#%.
str_view(x, "\\D+")
#> [1] │ <abcd ABCD >12345< -!@#%.>
str_view(x, "\\s+")
#> [1] │ abcd< >ABCD< >12345< >-!@#%.
str_view(x, "\\S+")
#> [1] │ <abcd> <ABCD> <12345> <-!@#%.>
str_view(x, "\\w+")
#> [1] │ <abcd> <ABCD> <12345> -!@#%.
str_view(x, "\\W+")
#> [1] │ abcd< >ABCD< >12345< -!@#%.>15.4.4 수량자 (Quantifiers)
수량자는 패턴이 일치하는 횟수를 제어합니다. Section 15.2 에서 ? (0회 또는 1회 일치), + (1회 이상 일치), * (0회 이상 일치)에 대해 배웠습니다. 예를 들어 colou?r는 미국식 또는 영국식 철자와 일치하고, \d+는 하나 이상의 숫자와 일치하며, \s?는 단일 공백 항목과 선택적으로 일치합니다. {}를 사용하여 일치 횟수를 정확하게 지정할 수도 있습니다:
-
{n}: 정확히 n번 일치합니다. -
{n,}: 적어도 n번 일치합니다. -
{n,m}: n번에서 m번 사이로 일치합니다.
15.4.5 연산자 우선순위와 괄호 (Operator precedence and parentheses)
ab+는 무엇과 일치할까요? “a” 뒤에 하나 이상의 “b”가 오는 것과 일치할까요, 아니면 “ab”가 횟수에 상관없이 반복되는 것과 일치할까요? ^a|b$는 무엇과 일치할까요? 완전한 문자열 a 또는 완전한 문자열 b와 일치할까요, 아니면 a로 시작하는 문자열 또는 b로 끝나는 문자열과 일치할까요?
이 질문들에 대한 답은 학교에서 배웠을 PEMDAS 또는 BEDMAS 규칙과 유사한 연산자 우선순위에 의해 결정됩니다. a + b * c는 (a + b) * c가 아니라 a + (b * c)와 동일하다는 것을 알고 있습니다. 왜냐하면 *가 더 높은 우선순위를 가지고 +가 더 낮은 우선순위를 가지기 때문입니다: +보다 *를 먼저 계산합니다.
마찬가지로 정규 표현식에도 고유한 우선순위 규칙이 있습니다: 수량자는 높은 우선순위를 가지고 대안(alternation)은 낮은 우선순위를 가집니다. 즉, ab+는 a(b+)와 동일하고 ^a|b$는 (^a)|(b$)와 동일합니다. 대수학에서와 마찬가지로 괄호를 사용하여 일반적인 순서를 재정의할 수 있습니다. 하지만 대수학과는 달리 정규 표현식의 우선순위 규칙을 기억하기 어려울 수 있으므로 괄호를 자유롭게 사용하세요.
15.4.6 그룹화와 캡처 (Grouping and capturing)
연산자 우선순위를 재정의하는 것 외에도 괄호는 또 다른 중요한 효과를 가집니다: 일치의 하위 구성 요소를 사용할 수 있게 해주는 캡처 그룹(capturing groups)을 생성합니다.
캡처 그룹을 사용하는 첫 번째 방법은 역참조(back reference)를 사용하여 일치 내에서 다시 참조하는 것입니다: \1은 첫 번째 괄호에 포함된 일치를 참조하고, \2는 두 번째 괄호, 이런 식입니다. 예를 들어, 다음 패턴은 반복되는 문자 쌍이 있는 모든 과일을 찾습니다:
str_view(fruit, "(..)\\1")
#> [4] │ b<anan>a
#> [20] │ <coco>nut
#> [22] │ <cucu>mber
#> [41] │ <juju>be
#> [56] │ <papa>ya
#> [73] │ s<alal> berry그리고 이것은 동일한 문자 쌍으로 시작하고 끝나는 모든 단어를 찾습니다:
str_view(words, "^(..).*\\1$")
#> [152] │ <church>
#> [217] │ <decide>
#> [617] │ <photograph>
#> [699] │ <require>
#> [739] │ <sense>str_replace()에서도 역참조를 사용할 수 있습니다. 예를 들어, 이 코드는 sentences에서 두 번째와 세 번째 단어의 순서를 바꿉니다:
sentences |>
str_replace("(\\w+) (\\w+) (\\w+)", "\\1 \\3 \\2") |>
str_view()
#> [1] │ The canoe birch slid on the smooth planks.
#> [2] │ Glue sheet the to the dark blue background.
#> [3] │ It's to easy tell the depth of a well.
#> [4] │ These a days chicken leg is a rare dish.
#> [5] │ Rice often is served in round bowls.
#> [6] │ The of juice lemons makes fine punch.
#> ... and 714 more각 그룹에 대한 일치 항목을 추출하려면 str_match()를 사용할 수 있습니다. 하지만 str_match()는 행렬을 반환하므로 작업하기가 특별히 쉽지는 않습니다8:
티블(tibble)로 변환하고 열 이름을 지정할 수 있습니다:
sentences |>
str_match("the (\\w+) (\\w+)") |>
as_tibble(.name_repair = "minimal") |>
set_names("match", "word1", "word2")
#> # A tibble: 720 × 3
#> match word1 word2
#> <chr> <chr> <chr>
#> 1 the smooth planks smooth planks
#> 2 the sheet to sheet to
#> 3 the depth of depth of
#> 4 <NA> <NA> <NA>
#> 5 <NA> <NA> <NA>
#> 6 <NA> <NA> <NA>
#> # ℹ 714 more rows하지만 그러면 기본적으로 separate_wider_regex()의 자체 버전을 다시 만든 셈이 됩니다. 실제로 separate_wider_regex()는 내부적으로 패턴 벡터를 그룹화를 사용하여 명명된 구성 요소를 캡처하는 단일 정규 표현식으로 변환합니다.
가끔은 일치 그룹을 생성하지 않고 괄호를 사용하고 싶을 때가 있습니다. (?:)를 사용하여 비캡처 그룹(non-capturing group)을 만들 수 있습니다.
15.4.7 연습문제 (Exercises)
리터럴 문자열
"'\와 어떻게 일치시키겠습니까?"$^$"는 어떻습니까?이 패턴들이 왜
\와 일치하지 않는지 설명하세요:"\","\\","\\\".-
stringr::words의 일반적인 단어 코퍼스가 주어졌을 때, 다음 단어를 모두 찾는 정규 표현식을 만드세요:- “y”로 시작하는 단어.
- “y”로 시작하지 않는 단어.
- “x”로 끝나는 단어.
- 정확히 세 글자인 단어. (
str_length()를 사용하여 속이지 마세요!) - 일곱 글자 이상인 단어.
- 모음-자음 쌍이 포함된 단어.
- 적어도 두 개의 모음-자음 쌍이 연속으로 포함된 단어.
- 반복되는 모음-자음 쌍으로만 구성된 단어.
다음 각 단어의 영국식 또는 미국식 철자와 일치하는 11개의 정규 표현식을 만드세요: airplane/aeroplane, aluminum/aluminium, analog/analogue, ass/arse, center/centre, defense/defence, donut/doughnut, gray/grey, modeling/modelling, skeptic/sceptic, summarize/summarise. 가능한 가장 짧은 정규 표현식을 만들어 보세요!
words에서 첫 글자와 마지막 글자를 바꾸세요. 그 문자열 중 여전히words인 것은 무엇입니까?-
이 정규 표현식들이 무엇과 일치하는지 말로 설명하세요: (각 항목이 정규 표현식인지 아니면 정규 표현식을 정의하는 문자열인지 주의 깊게 읽으세요.)
^.*$"\\{.+\\}"\d{4}-\d{2}-\d{2}"\\\\{4}"\..\..\..(.)\1\1"(..)\\1"
https://regexcrossword.com/challenges/beginner 에서 초보자용 정규 표현식 십자말풀이를 풀어보세요.
15.5 패턴 제어 (Pattern control)
단순한 문자열 대신 패턴 객체를 사용하여 일치의 세부 사항에 대해 추가적인 제어를 행사할 수 있습니다. 이를 통해 아래 설명된 대로 소위 정규 표현식 플래그를 제어하고 다양한 유형의 고정 문자열을 일치시킬 수 있습니다.
15.5.1 정규 표현식 플래그 (Regex flags)
정규 표현식의 세부 사항을 제어하는 데 사용할 수 있는 여러 설정이 있습니다. 이러한 설정은 다른 프로그래밍 언어에서 종종 플래그(flags)라고 불립니다. stringr에서는 regex() 호출로 패턴을 감싸서 이를 사용할 수 있습니다. 가장 유용한 플래그는 아마도 ignore_case = TRUE일 것입니다. 이를 통해 문자가 대문자 또는 소문자 형태 모두와 일치할 수 있기 때문입니다:
여러 줄 문자열(즉, \n이 포함된 문자열)로 많은 작업을 하는 경우 dotall과 multiline도 유용할 수 있습니다:
-
dotall = TRUE는.이\n을 포함한 모든 것과 일치하게 합니다: -
multiline = TRUE는^와$가 전체 문자열의 시작과 끝이 아니라 각 줄의 시작과 끝과 일치하게 합니다:
마지막으로, 복잡한 정규 표현식을 작성하고 있고 나중에 이해하지 못할까 봐 걱정된다면 comments = TRUE를 시도해 볼 수 있습니다. 이것은 패턴 언어를 조정하여 공백과 새 줄, 그리고 # 뒤의 모든 것을 무시하게 합니다. 이를 통해 다음 예제와 같이 주석과 공백을 사용하여 복잡한 정규 표현식을 더 이해하기 쉽게 만들 수 있습니다9:
phone <- regex(
r"(
\(? # 선택적 여는 괄호
(\d{3}) # 지역 번호
[)\-]? # 선택적 닫는 괄호 또는 대시
\ ? # 선택적 공백
(\d{3}) # 또 다른 세 숫자
[\ -]? # 선택적 공백 또는 대시
(\d{4}) # 네 개의 추가 숫자
)",
comments = TRUE
)
str_extract(c("514-791-8141", "(123) 456 7890", "123456"), phone)
#> [1] "514-791-8141" "(123) 456 7890" NA주석을 사용하면서 공백, 개행 또는 #과 일치시키려면 \로 이스케이프해야 합니다.
15.5.2 고정 일치 (Fixed matches)
fixed()를 사용하여 정규 표현식 규칙에서 제외(opt-out)할 수 있습니다:
fixed()는 대소문자를 무시하는 기능도 제공합니다:
영어가 아닌 텍스트로 작업하는 경우 fixed() 대신 coll()을 원할 것입니다. coll()은 지정한 locale에서 사용하는 대문자화에 대한 전체 규칙을 구현하기 때문입니다. 로케일에 대한 자세한 내용은 Section 14.6 를 참조하세요.
15.6 실습 (Practice)
이러한 아이디어를 실습하기 위해 다음으로 몇 가지 반(semi)-실제적인 문제를 해결해 보겠습니다. 세 가지 일반적인 기술에 대해 논의할 것입니다:
- 간단한 긍정 및 부정 대조군(controls)을 생성하여 작업 확인하기
- 정규 표현식과 불리언 대수(Boolean algebra) 결합하기
- 문자열 조작을 사용하여 복잡한 패턴 생성하기
15.6.1 작업 확인 (Check your work)
먼저, “The”로 시작하는 모든 문장을 찾아봅시다. ^ 앵커만 사용하는 것으로는 충분하지 않습니다:
str_view(sentences, "^The")
#> [1] │ <The> birch canoe slid on the smooth planks.
#> [4] │ <The>se days a chicken leg is a rare dish.
#> [6] │ <The> juice of lemons makes fine punch.
#> [7] │ <The> box was thrown beside the parked truck.
#> [8] │ <The> hogs were fed chopped corn and garbage.
#> [11] │ <The> boy was there when the sun rose.
#> ... and 271 more왜냐하면 그 패턴은 They나 These와 같은 단어로 시작하는 문장과도 일치하기 때문입니다. “e”가 단어의 마지막 글자인지 확인해야 하는데, 이는 단어 경계를 추가하여 수행할 수 있습니다:
str_view(sentences, "^The\\b")
#> [1] │ <The> birch canoe slid on the smooth planks.
#> [6] │ <The> juice of lemons makes fine punch.
#> [7] │ <The> box was thrown beside the parked truck.
#> [8] │ <The> hogs were fed chopped corn and garbage.
#> [11] │ <The> boy was there when the sun rose.
#> [13] │ <The> source of the huge river is the clear spring.
#> ... and 250 more대명사로 시작하는 모든 문장을 찾는 것은 어떨까요?
str_view(sentences, "^She|He|It|They\\b")
#> [3] │ <It>'s easy to tell the depth of a well.
#> [15] │ <He>lp the woman get back to her feet.
#> [27] │ <He>r purse was full of useless trash.
#> [29] │ <It> snowed, rained, and hailed the same morning.
#> [63] │ <He> ran half way to the hardware store.
#> [90] │ <He> lay prone and hardly moved a limb.
#> ... and 57 more결과를 빠르게 검사해 보면 가짜 일치(spurious matches)가 발생하고 있음을 알 수 있습니다. 이는 괄호를 사용하는 것을 잊었기 때문입니다:
str_view(sentences, "^(She|He|It|They)\\b")
#> [3] │ <It>'s easy to tell the depth of a well.
#> [29] │ <It> snowed, rained, and hailed the same morning.
#> [63] │ <He> ran half way to the hardware store.
#> [90] │ <He> lay prone and hardly moved a limb.
#> [116] │ <He> ordered peach pie with ice cream.
#> [127] │ <It> caught its hind paw in a rusty trap.
#> ... and 51 more처음 몇 개의 일치 항목에서 발생하지 않았다면 그러한 실수를 어떻게 발견할 수 있을지 궁금할 것입니다. 좋은 기술은 몇 가지 긍정 및 부정 일치를 생성하고 이를 사용하여 패턴이 예상대로 작동하는지 테스트하는 것입니다:
pos <- c("He is a boy", "She had a good time")
neg <- c("Shells come from the sea", "Hadley said 'It's a great day'")
pattern <- "^(She|He|It|They)\\b"
str_detect(pos, pattern)
#> [1] TRUE TRUE
str_detect(neg, pattern)
#> [1] FALSE FALSE일반적으로 부정적인 예보다 좋은 긍정적인 예를 생각해 내는 것이 훨씬 쉽습니다. 정규 표현식에 충분히 익숙해져서 자신의 약점이 어디인지 예측할 수 있게 되기까지는 시간이 걸리기 때문입니다. 그럼에도 불구하고 그것들은 여전히 유용합니다: 문제를 작업하면서 천천히 실수의 컬렉션을 축적하여 같은 실수를 두 번 다시 하지 않도록 할 수 있습니다.
15.6.2 불리언 연산 (Boolean operations)
자음만 포함된 단어를 찾고 싶다고 상상해 보세요. 한 가지 기술은 모음을 제외한 모든 문자를 포함하는 문자 클래스([^aeiou])를 만든 다음, 그것이 임의의 수의 문자와 일치하도록 허용하고([^aeiou]+), 시작과 끝에 앵커를 걸어 전체 문자열과 일치하도록 강제하는 것입니다(^[^aeiou]+$):
str_view(words, "^[^aeiou]+$")
#> [123] │ <by>
#> [249] │ <dry>
#> [328] │ <fly>
#> [538] │ <mrs>
#> [895] │ <try>
#> [952] │ <why>하지만 문제를 뒤집어서 이 문제를 좀 더 쉽게 만들 수 있습니다. 자음만 포함된 단어를 찾는 대신, 모음이 하나도 포함되지 않은 단어를 찾을 수 있습니다:
str_view(words[!str_detect(words, "[aeiou]")])
#> [1] │ by
#> [2] │ dry
#> [3] │ fly
#> [4] │ mrs
#> [5] │ try
#> [6] │ why이것은 논리적 조합, 특히 “and” 또는 “not”과 관련된 조합을 다룰 때마다 유용한 기술입니다. 예를 들어, “a”와 “b”가 포함된 모든 단어를 찾고 싶다고 상상해 보세요. 정규 표현식에는 “and” 연산자가 내장되어 있지 않으므로, “a” 뒤에 “b”가 오거나 “b” 뒤에 “a”가 오는 모든 단어를 찾는 방식으로 해결해야 합니다:
str_view(words, "a.*b|b.*a")
#> [2] │ <ab>le
#> [3] │ <ab>out
#> [4] │ <ab>solute
#> [62] │ <availab>le
#> [66] │ <ba>by
#> [67] │ <ba>ck
#> ... and 24 more두 번의 str_detect() 호출 결과를 결합하는 것이 더 간단합니다:
words[str_detect(words, "a") & str_detect(words, "b")]
#> [1] "able" "about" "absolute" "available" "baby" "back"
#> [7] "bad" "bag" "balance" "ball" "bank" "bar"
#> [13] "base" "basis" "bear" "beat" "beauty" "because"
#> [19] "black" "board" "boat" "break" "brilliant" "britain"
#> [25] "debate" "husband" "labour" "maybe" "probable" "table"모든 모음이 포함된 단어가 있는지 확인하고 싶다면 어떨까요? 패턴으로 한다면 5! (120)개의 서로 다른 패턴을 생성해야 합니다:
words[str_detect(words, "a.*e.*i.*o.*u")]
# ...
words[str_detect(words, "u.*o.*i.*e.*a")]다섯 번의 str_detect() 호출을 결합하는 것이 훨씬 간단합니다:
words[
str_detect(words, "a") &
str_detect(words, "e") &
str_detect(words, "i") &
str_detect(words, "o") &
str_detect(words, "u")
]
#> character(0)일반적으로 문제를 해결하는 단일 정규 표현식을 만들다가 막히면, 한 걸음 물러서서 문제를 더 작은 조각으로 나누어 다음 단계로 넘어가기 전에 각 과제를 해결할 수 있는지 생각해 보세요.
15.6.3 코드로 패턴 생성하기 (Creating a pattern with code)
색상을 언급하는 모든 sentences를 찾고 싶다면 어떨까요? 기본 아이디어는 간단합니다: 대안(alternation)을 단어 경계와 결합하기만 하면 됩니다.
str_view(sentences, "\\b(red|green|blue)\\b")
#> [2] │ Glue the sheet to the dark <blue> background.
#> [26] │ Two <blue> fish swam in the tank.
#> [92] │ A wisp of cloud hung in the <blue> air.
#> [148] │ The spot on the blotter was made by <green> ink.
#> [160] │ The sofa cushion is <red> and of light weight.
#> [174] │ The sky that morning was clear and bright <blue>.
#> ... and 20 more하지만 색상의 수가 늘어나면 이 패턴을 손으로 구성하는 것은 금방 지루해질 것입니다. 색상을 벡터에 저장할 수 있다면 좋지 않을까요?
rgb <- c("red", "green", "blue")음, 할 수 있습니다! str_c()와 str_flatten()을 사용하여 벡터에서 패턴을 생성하기만 하면 됩니다:
str_c("\\b(", str_flatten(rgb, "|"), ")\\b")
#> [1] "\\b(red|green|blue)\\b"좋은 색상 목록이 있다면 이 패턴을 더 포괄적으로 만들 수 있습니다. 시작할 수 있는 한 곳은 R이 플롯에 사용할 수 있는 내장 색상 목록입니다:
하지만 먼저 번호가 매겨진 변형을 제거해 봅시다:
cols <- colors()
cols <- cols[!str_detect(cols, "\\d")]
str_view(cols)
#> [1] │ white
#> [2] │ aliceblue
#> [3] │ antiquewhite
#> [4] │ aquamarine
#> [5] │ azure
#> [6] │ beige
#> ... and 137 more그런 다음 이것을 하나의 거대한 패턴으로 바꿀 수 있습니다. 패턴이 너무 커서 여기서는 보여주지 않겠지만, 작동하는 것을 볼 수 있습니다:
pattern <- str_c("\\b(", str_flatten(cols, "|"), ")\\b")
str_view(sentences, pattern)
#> [2] │ Glue the sheet to the dark <blue> background.
#> [12] │ A rod is used to catch <pink> <salmon>.
#> [26] │ Two <blue> fish swam in the tank.
#> [66] │ Cars and busses stalled in <snow> drifts.
#> [92] │ A wisp of cloud hung in the <blue> air.
#> [112] │ Leaves turn <brown> and <yellow> in the fall.
#> ... and 57 more이 예제에서 cols에는 문자와 숫자만 포함되어 있으므로 메타문자에 대해 걱정할 필요가 없습니다. 하지만 일반적으로 기존 문자열에서 패턴을 생성할 때는 str_escape()를 통해 실행하여 문자 그대로 일치하도록 하는 것이 현명합니다.
15.6.4 연습문제 (Exercises)
-
다음 각 과제에 대해 단일 정규 표현식과 여러
str_detect()호출의 조합을 모두 사용하여 해결해 보세요.-
x로 시작하거나 끝나는 모든words를 찾으세요. - 모음으로 시작하고 자음으로 끝나는 모든
words를 찾으세요. - 각기 다른 모음이 적어도 하나씩 포함된
words가 있나요?
-
“c 뒤가 아니면 e 앞에 i (i before e except after c)” 규칙에 대한 증거와 반대 증거를 찾는 패턴을 구성하세요.
colors()에는 “lightgray” 및 “darkblue”와 같은 여러 수식어가 포함되어 있습니다. 이러한 수식어를 어떻게 자동으로 식별할 수 있을까요? (수식된 색상을 감지한 다음 제거하는 방법을 생각해 보세요).기본 R(base R) 데이터셋을 찾는 정규 표현식을 만드세요.
data()함수의 특별한 사용을 통해 이러한 데이터셋 목록을 얻을 수 있습니다:data(package = "datasets")$results[, "Item"]. 많은 오래된 데이터셋은 개별 벡터입니다; 이들은 괄호 안에 그룹화 “데이터 프레임”의 이름을 포함하고 있으므로 이를 제거해야 합니다.
15.7 다른 곳에서의 정규 표현식 (Regular expressions in other places)
stringr 및 tidyr 함수에서와 마찬가지로 R에는 정규 표현식을 사용할 수 있는 다른 많은 곳이 있습니다. 다음 섹션에서는 더 넓은 tidyverse와 기본 R(base R)의 다른 유용한 함수들을 설명합니다.
15.7.1 tidyverse
정규 표현식을 사용하고 싶을 만한 다른 세 가지 특히 유용한 곳이 있습니다.
matches(pattern)은 이름이 제공된 패턴과 일치하는 모든 변수를 선택합니다. 이것은 변수를 선택하는 모든 tidyverse 함수(예:select(),rename_with(),across())에서 사용할 수 있는 “tidyselect” 함수입니다.pivot_longer()의names_pattern인수는separate_wider_regex()와 마찬가지로 정규 표현식 벡터를 받습니다. 복잡한 구조를 가진 변수 이름에서 데이터를 추출할 때 유용합니다.separate_longer_delim()및separate_wider_delim()의delim인수는 일반적으로 고정 문자열과 일치하지만,regex()를 사용하여 패턴과 일치하도록 만들 수 있습니다. 예를 들어, 선택적으로 공백이 뒤따르는 쉼표, 즉regex(", ?")와 일치시키려는 경우 유용합니다.
15.7.2 기본 R (Base R)
apropos(pattern)은 전역 환경에서 사용할 수 있는 객체 중 주어진 패턴과 일치하는 모든 객체를 검색합니다. 함수 이름이 잘 기억나지 않을 때 유용합니다:
apropos("replace")
#> [1] "%+replace%" "replace" "replace_na"
#> [4] "replace_theme" "setReplaceMethod" "str_replace"
#> [7] "str_replace_all" "str_replace_na" "theme_replace"list.files(path, pattern)은 path에 있는 파일 중 정규 표현식 pattern과 일치하는 모든 파일을 나열합니다. 예를 들어, 다음을 사용하여 현재 디렉터리의 모든 R Markdown 파일을 찾을 수 있습니다:
head(list.files(pattern = "\\.Rmd$"))
#> character(0)기본 R에서 사용하는 패턴 언어는 stringr에서 사용하는 것과 아주 약간 다르다는 점에 유의할 가치가 있습니다. 이는 stringr이 stringi 패키지 위에 구축되었고, stringi는 다시 ICU 엔진 위에 구축된 반면, 기본 R 함수는 perl = TRUE 설정 여부에 따라 TRE 엔진 또는 PCRE 엔진을 사용하기 때문입니다. 다행히 정규 표현식의 기초는 매우 잘 확립되어 있어 이 책에서 배울 패턴으로 작업할 때 변형을 거의 겪지 않을 것입니다. 복잡한 유니코드 문자 범위와 같은 고급 기능이나 (?…) 구문을 사용하는 특수 기능에 의존하기 시작할 때만 차이점을 인지하면 됩니다.
15.8 요약 (Summary)
모든 구두점 문자가 잠재적으로 의미로 과부하되어 있는 정규 표현식은 가장 간결한 언어 중 하나입니다. 처음에는 확실히 혼란스럽지만, 눈으로 읽고 뇌로 이해하도록 훈련하면 R과 다른 많은 곳에서 사용할 수 있는 강력한 기술을 잠금 해제하게 됩니다.
이 장에서는 가장 유용한 stringr 함수와 정규 표현식 언어의 가장 중요한 구성 요소를 학습하여 정규 표현식 마스터가 되기 위한 여정을 시작했습니다. 그리고 더 배울 수 있는 자료가 많이 있습니다.
시작하기 좋은 곳은 vignette("regular-expressions", package = "stringr")입니다: stringr이 지원하는 전체 구문 집합을 문서화하고 있습니다. 또 다른 유용한 참고 자료는 https://www.regular-expressions.info/입니다. R에 특화된 것은 아니지만, 정규 표현식의 가장 고급 기능과 내부 작동 방식에 대해 배우는 데 사용할 수 있습니다.
stringr이 Marek Gagolewski의 stringi 패키지 위에 구현되었다는 것을 아는 것도 좋습니다. stringr에서 필요한 기능을 수행하는 함수를 찾는 데 어려움을 겪고 있다면 stringi를 찾아보는 것을 두려워하지 마세요. stringi는 stringr과 동일한 규칙을 많이 따르기 때문에 매우 쉽게 익힐 수 있습니다.
다음 장에서는 문자열과 밀접하게 관련된 데이터 구조인 팩터(factors)에 대해 이야기하겠습니다. 팩터는 R에서 범주형 데이터, 즉 문자열 벡터로 식별되는 고정되고 알려진 가능한 값 집합을 가진 데이터를 나타내는 데 사용됩니다.
‘레젝스’(hard-g) 또는 ‘레직스’(soft-g)로 발음할 수 있습니다.↩︎
Section 15.4.1 에서 이러한 특수 의미를 이스케이프(escape)하는 방법을 배울 것입니다.↩︎
음,
\n을 제외한 모든 문자입니다.↩︎이것은 “x”가 포함된 이름의 비율을 제공합니다. x가 포함된 이름을 가진 아기의 비율을 원한다면 가중 평균을 수행해야 합니다.↩︎
실제 생활에서는 이렇게 이상한 것을 절대 볼 수 없을 거라고 안심시켜 드리고 싶지만, 불행히도 경력을 쌓다 보면 훨씬 더 이상한 것들을 보게 될 것입니다!↩︎
메타문자의 전체 집합은
.^$\|*+?{}[]()입니다.↩︎\d또는\s가 포함된 정규 표현식을 만들려면 문자열에 대해\를 이스케이프해야 하므로"\\d"또는"\\s"를 입력해야 함을 기억하세요.↩︎주로 이 책에서 행렬에 대해 논의하지 않기 때문입니다!↩︎
comments = TRUE는 여기서 사용하는 것처럼 원시 문자열과 결합할 때 특히 효과적입니다.↩︎