public bigdata

R4DS (R FOR DATASCIENCE) 11장 stringr로 하는 문자열 본문

R programming/R4DS (R for DataScience)

R4DS (R FOR DATASCIENCE) 11장 stringr로 하는 문자열

public bigdata 2020. 7. 19. 16:02
  • 문자열의 출력 표시는 문자열 자체와 같지 않다. 출력에서는 이스케이프가 보인다. 문자열의 원시 형태를 보려면 writeLines 함수를 사용한다.
x <- c("\"", "\\")
x
writeLines(x)
  • ?'"' 를 통해서 R에서 제공하는 특수문자 들을 보여준다.

11.2.2 문자열 결합

1) 둘 이상의 문자열을 결합할 때는 str_c를 사용한다.

str_c 함수 사용시 sep, collapse인자 두가지 모두 사용되는 경우 sep인자를 통해 각각의 결과가 먼저 만들어지고, collapse 인자를 통해서 각각의 결과가 하나의 결과로 합쳐진다.

> # 1.str_c
> # str_c는 벡터화되고 짧은 벡터가 긴 벡터와 길이가 같도록 자동으로 재사용한다.
> str_c("prefix-", c("a0", "a1", "a2"), "-suffix")
[1] "prefix-a0-suffix" "prefix-a1-suffix" "prefix-a2-suffix"

2) 문자열 벡터를 하나의 문자열로 합치려면 collapse를 사용한다.

str_c("prefix-", c("a0", "a1", "a2"), "-suffix", collapse = " ")

11.2.3 문자열 서브셋하기

  • 문자열의 일부는 str_sub를 통해 추출할 수 있다. start, end 인수를 취하고, 문자열이 짧은 경우에도 오류를 발생하지 않고 가능한 만큼 반환한다.
  • names와 같은 함수처럼 str_sub(x, 1, 2) <- "ap" 와 같이 문자열을 수정할 수도 있다.

11.2.4 로캘

각각의 언어는 대소문자 규칙이 달라서, 대소문자 변경도 생각보다 더 복잡하다. 이럴 때 로캘(locale)을 지정하여 사용하면 좋다고 한다.

터키어는 대문자도 "."이 붙어있는 대문자가 있다. (locale = "tr")

> str_to_upper(c("i", "I"), locale = "tr")
[1] "İ" "I"
  • 로캘은 두 글짜 또는 세 글자 줄임말인 ISO639언어 코드로 지정된다. 설정하고자 하는 언어의 ISO639코드를 모르는 경우에는 위키백과에 정리된 것을 참고하면 좋다.
  • 로캘이 비어있는 경우 운영체재에서 제공한 현재 로캘을 사용한다.
  • 베이스 R의 order, sort는 현재 로캘을 사용하여 정렬한다. 다른 환경에서도 동일한 결과를 원한다면 로캘을 명시할 수 있는 str_order, str_sort를 사용하면 된다.

로캘이 비어있는 경우 운영체재에서 제공한 현재 로캘을 사용한다.

11.2.5 연습문제

1) stringr을 사용하지 않는 코드에서 paste, paste0을 종종 볼 수 있다. 두 함수의 차이점은 무엇이고, 상응하는 stringr::함수는 무엇인가? 이 함수들은 NA를 다룰 때 어떻게 다른가?

> paste, paste0의 차이는 paste0는 공백을 하나도 없이 문자들을 결합한다. 

> paste에 상응하는 stringr 함수는 str_c(..., sep = " ")와 동일한 결과를 반환한다.

> paste0에 상응하는 stringr 함수는 str_c(...)이다. 인자 디폴트 값과 동일하다.

> NA를 다룰 때 어떻게 다른가? paste 함수는 NA를 문자 그대로 "NA"로 사용하고, str_c 함수는 NA가 섞인 입력은 결과로 NA를 유발한다.

> paste("prefix-", c("a", "b", "c"), NA, sep="sep")
[1] "prefix-sepasepNA" "prefix-sepbsepNA" "prefix-sepcsepNA"
> str_c("prefix-", c("a", "b", NA), "-suffix", sep="sep")
[1] "prefix-sepasep-suffix" "prefix-sepbsep-suffix" NA  

3) str_length, str_sub을 이용하여 문자열 중앙문자를 추출하라. 문자열에 짝수 개의 문자가 있다면 어떻게 하겠는가? 

> 해설집에서는 ceiling을 활용해서 위치를 찾아 추출하던데, 그 방식 보다는 나는 ifelse 함수를 통해서 홀수, 짝수일 때 동작을 달리해서 추출하는 것이 더 좋을 것 같다.

 

4) str_wrap의 기능은 무엇인가? 

> 긴 문자를 일정한 폭으로 유지한다. 

thanks_path <- file.path(R.home("doc"), "THANKS")
thanks <- str_c(readLines(thanks_path), collapse = "\n")
thanks <- word(thanks, 1, 3, fixed("\n\n"))
cat(str_wrap(thanks), "\n")

 

위 코드에서 사용한 함수

  • R.home : R의 홈 디렉토리 또는 특정 구성 요소의 경로를 반환받을 수 있다.
  • file.path : 디렉토리 또는 파일 주소를 뜻하는 경로를 만들어준다.
  • readLines : 파일의 텍스트를 한줄씩 읽어들여서 벡터로 저장한다.
  • word : 문장으로 부터 단어를 추출해내는 함수(디폴트로 단어 구분은 " "공백 하나이나. sep=fixed()로 변경가능)
  • str_wrap : 텍스트를 일정한 폭으로 줄바꿈 해주고, 왼쪽 여백 등의 내어쓰기를 만들어주나.. 정확한 기능은 이해가 안된다.
  • cat : 문자열을 출력해준다. 기존에는 \n같은 이스케이프 문자들도 그대로 저장이 되어 있는데 출력하면서 줄바꿈이 된다.

5) str_trim의 기능은 무엇인가? str_trim의 반대는 무엇인가?

> 공백제거, 반대 함수는 str_pad 이며 너비에 맞게 다른 문자를 추가해준다.

 

6) 예를 들어 벡터 c("a", "b", "c")를 문자열 a, b, c로 변환하는 함수를 작성하라, 길이가 0, 1, 2인 벡터일 경우 어떻게 해야 하는지에 대해 생각해보라. 

> 해설 참조

 

11.3 정규표현식을 이용한 패턴 매칭

R에는 정규표현식을 배우기 위한 함수 2가지가 있다. str_view, str_view_all

 

11.3.1 기초 매칭

1) str_view

## ex1 ##
# 문자 "an" 매칭
x <- c("apple", "banana", "pear")
str_view(x, "an")

## ex2 ##
# 줄바꿈을 제외한 임의의 문자와 매칭 하는 "."
str_view(x, ".a.")

ex1 결과
ex2 결과

2_1) 정규표현식 \(역슬래쉬)

정규식에서 .은 줄바꿈을 제외한 임의의 문자와 매칭이 되는 동작을 한다. 그런데 "."을 매칭을 하고 싶다면 역슬래쉬를 이용하면 된다. 즉. 정규표현식으로 \. 을 전달하면 된다. 

 

그런데 우리는 \.을 문자열로 전달해야 한다. "\." 이렇게 그러나 이렇게 전달을 하면 정규식뿐만 아니라 문자열에서도 동작을 이스케이프 하는데 \를 사용한다.

 

그렇기에 정규표현식 \.을 작성하기 위해서는 문자열 "\\."이 필요하다.

> dot <- "\\."
> writeLines(dot)
\.

2_2) 정규표현식에서 문자 \는 어떻게 표현해야 하는가

정규식에서 문자 "\"를 표현하려면 정규식 \\이 필요하다. 문자열에서 \\을 표현하려면 "\\\\"을 작성해야 정규식 \\이 전달이 된다.

> dot <- "\\\\"
> writeLines(dot)
\\

11.3.2 연습문제

1) 문자열 "\", "\\", "\\\"가 문자 \와 매칭되지 않는 이유를 설명하라.

> "\"는 문자열에서 이스케이프 문자이므로 문자열 자체가 생성이 되지 않는다 뒤의 "를 문자열 표현이 아닌 문자 그대로 만들기 때문

> "\\"는 첫번째 \가 뒤의 \를 이스케이프 해서 문자 \만 남는다. 문자 \는 정규식에 전달돼서 정규식의 이스케이프 문자로 해석되어 문자 "\"를 찾을 수 없다.

> "\\\"는 첫번째가 두번째를 문자로 만들고 세번째 \는 이스케이프 문자가 되어 뒤의 "를 문자로 만들어 문자열 생성이 되지 않으므로, 정규식에 전달하여 문자 \를 매칭할 수 없다.

 

2) 시퀀스 "'\를 어떻게 매칭하겠는가?

str_view("\"\'\\", pattern = "\"\'\\\\")

 

3) 정규표현식 \..\..\..은 어떤 패턴과 매칭되겠는가? 문자열로 어떻게 표현하겠는가?

str_view(c(".a.b.c", ".a.b", "....."), c("\\..\\..\\.."), match = TRUE)

> 결과는 ".a.b.c"만 매칭이 된다.

11.3.3 앵커

정규식을 문자열의 시작 또는 끝과 매칭할 수 있다.

text <- c("apple", "apple pie", "apple cake")
str_view(text, "^apple$")

결과

boundary에 관련된 내용도 나오는데 pass 

11.3.4 연습문제

1) 문자열 "$^$"를 어떻게 매칭하겠는가?

# "$^$"를 포함하는 경우
str_view("$^$", pattern = "\\$\\^\\$", match = TRUE)

# "$^$"를 정확하게 매칭되는 경우(즉. "$^$"만 있는 경우)
str_view("$^$", pattern = "^\\$\\^\\$$", match = TRUE)

2) stringr::words에 담긴 평범한 말뭉치에서 다음에 해당하는 단어들을 찾는 정규표현식을 구하라.

  • 'y'로 시작
  • 'x'로 끝남
  • 정확히 세 글자 (str_length를 사용하는 부정행위 x) --> \b 요거롤 써야 하나?
  • 7개 이상의 글자.

str_view 함수의 match 인자를 활용하여 매칭되는 단어만 보도록 하자

 

> 'y'로 시작

str_view(stringr::words, pattern = "^y", match = TRUE)

> 'x'로 끝남

str_view(stringr::words, pattern = "x$", match = TRUE)

> 정확히 세 글자

str_view_all(stringr::words, pattern = "^...$", match = TRUE)

> 7개 이상의 글자

# 7개 이상의 글자
str_view_all(stringr::words, pattern = ".......", match = TRUE)

# 정확하게 7글자
str_view_all(stringr::words, pattern = "^.......$", match = TRUE)

※ str_view, str_view_all 의 차이는 str_view_all의 경우 "apple banana"라는 문자열이 있을 때 apple 또는 banana라는 단어를 찾으라고 한 경우 apple을 먼저 찾아 조건을 만족하지만 뒤의 문자열까지 탐색해서 추가적인 매치가 되는지 찾는다. 

str_view("apple banana", pattern = "apple|banana", match = TRUE)

str_view_all("apple banana", pattern = "apple|banana", match = TRUE)

11.3.5 문자 클래스와 대체 구문

줄바꿈을 제외한 임의의 문자를 매칭하는 . 이외에도 유용한 도구가 있다.

  • \d는 임의의 숫자를 의미한다.
  • \s는 임의의 여백 문자(whitespace, 탭, 줄바꿈)을 의미한다.
  • [abc]는 a, b 또는 c를 의미한다.
  • [^abc]는 a, b, c를 제외한 문자를 의미한다.

※ 추가사항

  • abc|xyz 라는 정규식은 abc 또는 xyz를 매칭하라는 뜻인데 사용하다 보면 ab(c또는 x)yz라는 뜻인지 헷갈리기도 한다. 이럴 때 후자의 의미를 가지는 정규식은 다음과 같이 괄호를 활용하여 표현하면 좋다. ab(c|x)yz
  • 정규식 매칭에서 _all 함수와 기본 함수의 차이는 _all이 붙는 함수는 해당 정규식을 만족하는 것을 문자열 끝까지 모두 찾는다. 다만 정규식에는 영향을 주지 않는다. 같은 정규식이라면 _all이 붙는 함수나, 기본 함수나 정규식이 뜻하는 것은 동일하다.
  • 기본적으로 정규식을 매칭하는 함수들은 R에서 greedy한 방식인데, str_view("abc", "a[bc]+")인 경우 "abc"를 매칭한다. +라는 정규식 표현은 앞 규칙의 1이상 매칭되는 경우를 뜻하는데, 기본적으로 greedy 방식이므로 1이상의 최대로 매칭되도록 한다. str_view("abc", "a[bc]+?")이렇게 매칭하면 최소로 매칭하도록 지정할 수 있다.

11.3.6 연습문제

1) 다음에 해당하는 모든 단어를 찾는 정규표현식을 작성하라.

  • 모음으로 시작함
  • 자음만 포함함
  • ed로 끝나지만 eed로 끝나지 않음
  • ing 혹은 ize로 끝남

> 모음으로 시작하는 정규식

str_view(stringr::words, pattern = "^[AEIOUaeiou]")

# str_subset을 활용하면 해당 조건을 만족하는 값들만 벡터로 반환한다.
str_subset(stringr::words, "^[aeiou]")

> 자음만 포함하는 정규식

> str_subset(stringr::words, "[aeiou]", negate = TRUE)
[1] "by"  "dry" "fly" "mrs" "try" "why"

## 아래처럼 사용하면 모음이 아닌 것들을 포함하는 문자를 반환하기 때문에
## 모음을 제외하는 결과가 아니라, 모음이 아닌 자음을 포함하는 문자를 반환하게 돼서
## 결과적으로 모음을 포함하게 된다.
str_subset(stringr::words, "[^aeiou]")

> ed로 끝나지만 eed로는 끝나지 않음

## ed로 끝나지만 eed로 끝나면 안된다는 것은 ed로 끝나는 문자 앞에 e만 안오면 되는 것이다.
> str_subset(stringr::words, "[^e]ed$")
[1] "bed"     "hundred" "red" 


## 만약 'ed'라는 문자를 추가적으로 정규식으로 매칭하고 싶은 경우
## () 괄호 안을 보면 ^|[^e] 라는 정규식이 있는데, 정규식에서 ^는 []대괄호 밖에 있는 경우에는
## 문자의 시작을 알리는 앵커이고, [^e]는 e가 아닌 문자들을 뜻하기 때문에 
## 결과적으로 ^ed$ 라는 정규식이거나, [^e]ed$의 두가지 정규식을 표현할 수 있다.
## 나도 이런 사례는 처음봤는데 굉장히 유용하다.
str_subset(c("ed", stringr::words), "(^|[^e])ed$")
#> [1] "ed"      "bed"     "hundred" "red"

> ing 혹은 ize로 끝남(다양하게 표현이 가능하다)

> str_subset(stringr::words, "(ing|ize)$")
 [1] "bring"     "during"    "evening"   "king"     
 [5] "meaning"   "morning"   "organize"  "recognize"
 [9] "ring"      "sing"      "size"      "thing"    
 
 # 정규식에서 사용되는 특수한 목적의 문자인 $같은 것도 |를 통해서 or 조건이 되는게 뭔가 신기하다.
> str_subset(stringr::words, "(ing$|ize$)")
 [1] "bring"     "during"    "evening"   "king"     
 [5] "meaning"   "morning"   "organize"  "recognize"
 [9] "ring"      "sing"      "size"      "thing" 
 
>str_subset(stringr::words, "i(ng|se)$")
 [1] "advertise" "bring"     "during"    "evening"   "exercise"  "king"     
 [7] "meaning"   "morning"   "otherwise" "practise"  "raise"     "realise"  
 [13] "ring"      "rise"      "sing"      "surprise"  "thing"

2) 다음의 규칙을 데이터 기반으로 증명하라. 'c를 제외하고는 i가 e 앞에 온다'

무슨말인지 모르겠다. R4DS 해답을 그대로 첨부한다.

## 문제 : Empirically verify the rule “i” before e except after “c”.

length(str_subset(stringr::words, "(cei|[^c]ie)"))
#> [1] 14

length(str_subset(stringr::words, "(cie|[^c]ei)"))
#> [1] 3

3) 'q'다음은 항상 'u'인가?

# 매칭되는 결과가 없다
str_view(stringr::words, "q[^u]", match = TRUE)

4) 미국 영어가 아닌 영국 영어로 쓰여진 단어를 매칭하는 정규표현식을 작성하라.

> 몇몇 규칙을 정할 수는 있지만, 그것으로 충분하지 않기 때문에 영국식 사전이 필요하다.

 

5) 여러분의 나라에서 일반적으로 쓰이는 전화번호를 매칭하는 정규표현식을 작성하라

다양한 방식들이 존재한다.

str_view("010-6554-4613", "^[0-9]{3}-[0-9]{4}-[0-9]{4}$", match = TRUE)

str_view(x, "[0-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]")

str_view(x, "\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d")

11.3.7 반복

  • ? : 앞자리 문자가 0회 또는 1회
  • + : 앞자리 문자가 1회 이상
  • * : 앞자리 문자가 0회 이상
str_view(x, "CC?")
str_view(x, "CC+")
str_view(x, "C[LX]+")

str_view(x, "CC?")
str_view(x, "CC+")
str_view(x, "C[LX}+")

 

위 정규식처럼도 가능하지만, 매칭횟수를 정확하게 지정할 수도 있다.

  • {n} : 정확히 n회
  • {n,} : n회 이상
  • {,m} : 최대 m회
  • {n,m} : n과 m회 사이

주의할 점 위 정규식 사용할 때 ,(콤마)이전 이후 띄워쓰기 하면 안됨 {2,} 붙여써야 함.(R에서는/다른 언어는 모르겠음)

str_view(x, "C{2}")
str_view(x, "C{2,}")
str_view(x, "C{2,3}")

str_view(x, "C{2}")
str_view(x, "C{2,}")
str_view(x, "C{2,3}")

기본적으로 이런 매칭은 'greedy'매칭이다. 가능한 한 가장 긴 문자열과 매칭한다. 뒤에 ?를 넣으면 가장 짧은 문자열과 매칭된다. 즉. lazy(게으르게) 매칭된다. 정규표현식의 고급 기능이다.

str_view_all과 같은 _all이 들어가는 정규표현식 함수가 조건에 맞는 문자를 찾고 추가적으로 조건을 만족하는 문자가 있는지 확인하는 기능을 한다면, greedy 매칭은 

str_view(x, 'C{2,3}?')
str_view(x, 'C[LX]+?')

str_view(x, 'C{2,3}?')

 

str_view(x, 'C[LX]+?')

11.3.8 연습문제

1) ?, +, *와 같은 의미의 정규표현식을 {m, n}형식을 사용해 기술하라.

## ? ##
str_view("dasfdaadfs", pattern = "aa?")
str_view("dasfdaadfs", pattern = "aa{0,1}")

## + ##
str_view("sdfdfsfd", pattern = "aa+")
str_view("sdfdfsfd", pattern = "aa{1,}")

## * ##
str_view("sdfdfsfd", pattern = "aa*")
str_view("sdfdfsfd", pattern = "aa{0,}")

2) 다음의 정규식이 어떤 것과 매칭하는지 설명하라

  • ^.*$
  • "\\{.+\\}"
  • \d{4}-\d{2}-\d{2}
  • "\\\\{4}"

> ^.*$ : 줄바꿈을 제외한 문자가 0개 이상으로 시작하고 끝난다.

> "\\{.+\\}" : \{.+\}는 "{"로 시작하고 줄바꿈을 제외한 문자가 하나 이상 나타나고 "}"으로 끝난다. 예를 들면 "{asfsfd}" 이런 문자가 매칭된다.

 

> \d{4}-\d{2}-\d{2} : \d는 숫자를 의미한다. 숫자가 4개 나타나고, 숫자 2개, 숫자 2개 형식의 문자열이 매칭된다. 물론 정규식을 문자열로 전달하려면 "\\d{4}-\\d{2}-\\d{2}"로 전달해야 한다.

str_view("1234-12-12", pattern = "\\d{4}-\\d{2}-\\d{2}")

> "\\\\{4}" : "\\\\"이 정규식으로 \\으로 전달되고, 정규식\\{4}는 "\"가 4번 오는 "\\\\"문자를 매칭한다.

str_view("\\\\\\\\\\\\", pattern = "\\\\{4}")

3) 다음의 모든 단어를 찾는 정규표현식을 작성하라.

  • 세 개의 자음으로 시작
  • 세 개 이상의 모음이 연달아 있음
  • 두 개 이상의 모음-자음 쌍이 연달아 있음
## 영어의 모음은 a, e, i, o, u 총 5개 입니다. ##
str_view(stringr::words, "^[^aeiou]{3}", match = TRUE)

 

## 모음이 연달아 세 개 나오는 경우 ##
str_view(stringr::words, "[aeiou]{3,}", match = TRUE)

## 모음, 자음 세트가 2번 이상 반복되는 경우이므로 (모음, 자음){2,} 형태로 표현한다 ##
str_view(stringr::words, "([aeiou][^aeiou]){2,}", match = TRUE)

4) 다음의 초보자 정규표현식 십자말풀이를 풀어보라

링크

 

How to play Regex Crossword

Instructions how to play the Regex Crossword puzzle game, and what regular expressions are all about with example solution and patterns.

regexcrossword.com

11.3.9 그룹화와 역참조

역참조란 ()를 사용해 그룹화를 한 다음, ()에 의해서 일치된 정규식을 \1의 형태로 지칭하는 것이다.

str_view(stringr::words, "(..)\\1", match=TRUE)

헷갈리는 부분인데 책에서는 아주 조금 설명이 되어 있다. 위 그림을 보면 먼저 정규식 (..)이 매칭이 된다. 여기서 매칭된 문자는 "em"이다. 그런 다음 정규식에서 \\1 부분이 남아 있는데 이부분은 ()로 감싼 정규식의 번호이다. 여기서는 한 개뿐이니 \\1이다. \\1이 의미하는 것은 첫번째 그룹화된 정규식 표현에 의해서 매칭된 문자인 "em"을 한 번 더 매칭하는 것이다.(이래서 역참조라 지칭하는 듯)

그래서 "emem"이 매칭이 된다.

 

str_match 함수와 사용하면 유용하다고 하는데, 좀 더 살펴봐야 할듯

11.3.10 연습문제

1. 다음의 표현식이 어떤 것과 매칭되는지 설명하라

1) (.)\1\1 : 'aaa' 와 같은 똑같은 문자가 3개 오는 문자열

2) "(.)(.)\\2\\1" : 'abba'와 같이 앞 두글자가 대칭인 문자열

3) (..)\1 : 앞 두글자가 반복되는 문자 'abab'와 같은 문자열

4) "(.).\\1.\\1" : 1, 2, 3번째 글자가 동일한 문자열

5) "(.)(.)(.).*\\3\\2\\1" : 1,2,3,4 번째 글자가 오고 *(0개 이상의 어떠한 문자)가 오고 1,2,3번째 문자가 대칭으로 온다. "abccba"와 같은 (여기서는 4번째 .이 줄바꿈을 제외한 모든 문자이니 공백으로 적용되었다.)

 

2. 다음의 단어와 매칭하는 정규표현식을 작성하라

1) 같은 문자로 시작하고 끝남

# 내가 작성한 코드
> str_subset(words, "^(.).*\\1$")
 [1] "america"    "area"       "dad"       
 [4] "dead"       "depend"     "educate"   
 [7] "else"       "encourage"  "engine"    
[10] "europe"     "evidence"   "example"   
[13] "excuse"     "exercise"   "expense"   
[16] "experience" "eye"        "health"    
[19] "high"       "knock"      "level"     
[22] "local"      "nation"     "non"       
[25] "rather"     "refer"      "remember"  
[28] "serious"    "stairs"     "test"      
[31] "tonight"    "transport"  "treat"     
[34] "trust"      "window"     "yesterday" 

# 답지('a'가 추가로 매칭되어 있음)
str_subset(words, "^(.)((.*\\1$)|\\1?$)")
#>  [1] "a"          "america"    "area"       "dad"        "dead"      
#>  [6] "depend"     "educate"    "else"       "encourage"  "engine"    
#> [11] "europe"     "evidence"   "example"    "excuse"     "exercise"  
#> [16] "expense"    "experience" "eye"        "health"     "high"      
#> [21] "knock"      "level"      "local"      "nation"     "non"       
#> [26] "rather"     "refer"      "remember"   "serious"    "stairs"    
#> [31] "test"       "tonight"    "transport"  "treat"      "trust"     
#> [36] "window"     "yesterday"

2) 두 문자 반복이 있음 ('church'는 'ch'가 두번 반복됨)

str_subset(words, "([A-Za-z][A-Za-z]).*\\1")
#>  [1] "appropriate" "church"      "condition"   "decide"      "environment"
#>  [6] "london"      "paragraph"   "particular"  "photograph"  "prepare"    
#> [11] "pressure"    "remember"    "represent"   "require"     "sense"      
#> [16] "therefore"   "understand"  "whether"

3) 적어도 세 곳에서 반복되는 문자가 있음 ('eleven'은 'e'가 세 개)

str_subset(words, "([a-z]).*\\1.*\\1")
#>  [1] "appropriate" "available"   "believe"     "between"     "business"   
#>  [6] "degree"      "difference"  "discuss"     "eleven"      "environment"
#> [11] "evidence"    "exercise"    "expense"     "experience"  "individual" 
#> [16] "paragraph"   "receive"     "remember"    "represent"   "telephone"  
#> [21] "therefore"   "tomorrow"

11.4.1 매칭 탐지

str_detect와 filter 함수를 함께 사용하면 데이터프레임의 문자열에 조건을 걸어 데이터 필터링이 가능하다.

 

str_count 함수는 단순히 yes, no 대신 하나의 문자열에서 정규식이 몇번 매칭되는지 알려준다. str_count 함수는 mutate 함수와 함께 사용하여 문자열이 있는 컬럼에 각 조건에 해당하는 컬럼을 만들어 해당 조건을 만족하는 케이스가 몇번인지 나타낼 수 있다.

library(tidyverse)
library(stringr)

# t로 시작하는 단어의 개수
sum(str_detect(words, "^t"))

# t로 시작하는 단어의 비율
mean(str_detect(words, "^t")) * 100


## 모음을 포함하지 않는 것을 찾는 2가지 방법
# 모음이 있는 단어를 찾은 후 역을 취함
no_aeiou <- !str_detect(words, "[aeiou]")
words[no_aeiou]

# 비모음으로 구성된 단어를 찾음
aeiou <- str_detect(words, "^[^aeiou]+$")
words[aeiou]

# filter와 str_detect의 활용
df <- tibble(
  word = words,
  i = seq_along(words)
)
df %>% 
  filter(str_detect(word, "x$"))

11.4.2 연습문제

1. 다음 문제들을 두 가지 방식으로 각각 풀어보라. 하나의 정규표현식을 사용해보고, 또 여러 str_detect 호출을 결합해보라.

1) x로 시작하거나 끝나는 모든 단어를 찾아라

2) 모음으로 시작하고 자음으로 끝나는 모든 단어를 찾아라

3) 각기 다른 모음을 하나 이상씩 포함하는 단어가 있는가?

 

1) x로 시작하거나 끝나는 모든 단어를 찾아라

> words[str_detect(words, "^x|x$")]
[1] "box" "sex" "six" "tax"

2) 모음으로 시작하고 자음으로 끝나는 모든 단어를 찾아라

words[str_detect(words, "^[aeiou].*[^aeiou]$")]

3) 각기 다른 모음을 하나 이상씩 포함하는 단어가 있는가?

words[str_detect(words, "a") &
  str_detect(words, "e") &
  str_detect(words, "i") &
  str_detect(words, "o") &
  str_detect(words, "u")]

이렇게 하면 쉽게 찾을 수 있고 해설지를 보면 다른 방법을 제시하는데 이부분은 이해가 안돼서 첨부만 한다.

pattern <-
  cross(rerun(5, c("a", "e", "i", "o", "u")),
    .filter = function(...) {
      x <- as.character(unlist(list(...)))
      length(x) != length(unique(x))
    }
  ) %>%
  map_chr(~str_c(unlist(.x), collapse = ".*")) %>%
  str_c(collapse = "|")

str_subset("aseiouds", pattern)
#> [1] "aseiouds"

2. 어떤 단어가 가장 많은 모음을 갖는가? 어떤 단어가 모음의 비율이 가장 높은가?

> max1 <- max(str_count(words, "[aeiou]"))
> words[str_count(words, "[aeiou]") == max1]
[1] "appropriate" "associate"   "available"  
[4] "colleague"   "encourage"   "experience" 
[7] "individual"  "television" 

11.4.3 매칭 추출

library(tidyverse)
library(stringr)

# color_match 지정
color <- c("red", "orange", "yellow", "green", "blue", "purple")
color_match <- str_c(color, collapse = "|")

# str_subset으로 sentence 중 color 색이 매칭되는 문장만 걸러낸다.
# 그런 다음 str_extract로 매칭되는 색상을 추출한다.
has_color <- str_subset(sentences, color_match)
matche <- str_extract(has_color, color_match) # str_extract는 첫번째 매칭되는 결과만 가져온다.
> matche
  [1] "blue"   "blue"   "red"    "red"    "red"    "blue"   "yellow"
  [8] "red"    "red"    "green"  "red"    "red"    "blue"   "red"   ...

# str_extract_all로 하면 모두 매칭되는 것 모두 추출
# simplify인자는 가장 긴 것과 같은 길이로 확장된 행렬로 반환해준다.
matches <- str_extract_all(has_color, color_match, simplify = TRUE)
> matches
     [,1]     [,2] 
 [1,] "blue"   ""   
 [2,] "blue"   ""   
 [3,] "red"    ""   
 [4,] "red"    ""  

11.4.4 연습문제

1) 앞의 매칭 추출에서 매칭된 정규표현식이 색상이 아닌 'fickered'에도 매칭이 된 것을 해결하기 위해 정규식을 수정하라.

colour_match2 <- str_c("\\b(", str_c(colours, collapse = "|"), ")\\b")

> 위 코드를 보면 바운더리를 통해서 정규표현식 앞뒤로 단어의 경계를 뜻하는 \\b를 추가하여 "red", "orange"와 같이 앞뒤로 추가적인 단어가 붙지 않도록 했다. (추측하면, orange 또는 white orange라는 단어가 있다면 orange 앞뒤로 바운더리가 존재하고, white orange는 white 앞뒤로 하나씩 그리고 orange 앞뒤로 하나씩 총 4개의 바운더리가 존재할 것으로 생각된다.)

> 이렇게 바운더리를 사용한 이유는 문장에서 컬러를 뜻하는 단어를 포함하는 문장을 걸러내고 싶은데      ^red$ 이런식으로 매칭하면 매칭되는 문장이 없을 것이기 때문에 바운더리를 활용한 것으로 생각된다. 

 

2) 하버드 문장 데이터에서 다음을 추출하라

  • 각 문장의 첫 번째 단어
  • ing로 끝나는 모든 단어
  • 모든 복수형

> 각 문장의 첫 번째 단어(str_extract는 처음 매칭되는 단어들만 반환하는 것을 활용)

pattern <- "[A-Za-z][A-Za-z']*"
sentences_with_ing <- str_extract(sentences, pattern1)
sentences_with_ing

> ing으로 끝나는 모든 단어 추출

> pattern <- "\\b[A-Za-z]+ing\\b"
> sentences_with_ing <- str_detect(sentences, pattern)
> unique(unlist(str_extract_all(sentences[sentences_with_ing], pattern))) %>%
+   head()
[1] "spring"  "evening" "morning" "winding" "living"  "king" 

> 모든 복수형

해설지에 의하면 모든 복수형은 정확하게 찾을 수는 없다고 한다. 해당 언어의 형태학적 정보가 필요하다고 한다. 다만 'as', 'is', 'gas' 등의 문자를 제거하기 위해서 3개 이상의 문자가 오고 's'로 끝나는 단어를 식별하자. 단어를 식별하는 것이기 때문에 바운더리를 사용하여 단어를 식별한다. 단어의 경우 앞 뒤로 공백이 존재할 테니 바운더리가 존재한다? ex) "~~~ 단어 ~"

> unique(unlist(str_extract_all(sentences, "\\b[A-Za-z]{3,}s\\b"))) %>%
+   head()
[1] "planks" "days"   "bowls"  "lemons" "makes"  "hogs"  

11.4.5 그룹화 매칭

앞에서 배웠던 내용에서 연산 우선순위, 역참조 목적으로 괄호를 사용하는 것에 대해 언급했다. 이 외에 복잡한 매치의 일부를 추출하기 위해서도 괄호를 사용할 수 있다. 

 

"(정규식1 불라뷸라) (정규식2 불라불라)" 이런 정규식이 있다고 할때 각 괄호는 정규식 내에서도 규칙1, 규칙2를 의미한다. 아래 예제에서 어떻게 활용되는지 살펴보자

 

'a'또는 'the' 뒤에 오는 단어 찾기

## 데이터 준비 ##
## 먼저 정규식(a 또는 the 가 오고 한칸 띄운뒤에 공백이 아닌 문자가 1개 이상 오는 경우) ##
## 을 만족하는 벡터만 걸러낸다. ##
> noun <- "(a|the) ([^ ]+)"
> has_noun <- stringr::sentences %>% 
+   str_subset(noun) %>% 
+   head(10)


## 필터링 1(매칭되는 전체문자만 반환) ##
## 걸러낸 벡터에서 str_extract를 이용해 매칭되는 부분을 추출한다 ##
## str_extract는 첫 매칭만 확인해준다 ##
## str_extract_all을 사용해야 모든 매칭을 확인한다 ## 
> 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"  
 
 
 ## 필터링 2(전체 매칭, 개별 매칭들을 행렬로 반환) ##
 ## str_extract는 완전한 매칭만 제공한다. 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"  

## 필터링 3 ## 
## 데이터가 tibble인 경우에는 tidyr::extract를 사용한다. ##
## str_match와 비슷하게 동작하지만, 매치의 이름을 사용자가 지정할 수 있고 ##
## 그 각각의 새로운 열로 배치한다. ##
> tibble(sentence = stringr::sentences) %>% 
+   tidyr::extract(
+     sentence, c("article", "noun"), "(a|the) ([^ ]+)",
+     remove = FALSE)
# A tibble: 720 x 3
   sentence                                    article noun   
   <chr>                                       <chr>   <chr>  
 1 The birch canoe slid on the smooth planks.  the     smooth 
 2 Glue the sheet to the dark blue background. the     sheet  
 3 It's easy to tell the depth of a well.      the     depth  
 4 These days a chicken leg is a rare dish.    a       chicken
 5 Rice is often served in round bowls.        NA      NA     
 6 The juice of lemons makes fine punch.       NA      NA     
 7 The box was thrown beside the parked truck. the     parked 
 8 The hogs were fed chopped corn and garbage. NA      NA     
 9 Four hours of steady work faced us.         NA      NA     
10 Large size in stockings is hard to sell.    NA      NA     
# ... with 710 more rows

11.4.6 연습문제

1) 'one', 'two', 'three'등과 같은 '숫자'다음에 오는 모든 단어를 구하라. 숫자와 단어 모두를 추출하라

> #1 내가 한 방식
> data <- str_extract_all(sentences, pattern = "\\b(one|two|three|four|five|six|seven|eight|nine|ten)\\b +(\\w+)",
+                 simplify = FALSE) 
> data %>% unlist()
 [1] "seven books"   "two met"       "two factors"   "three lists"   "seven is"      "two when"      "ten inches"   
 [8] "one war"       "one button"    "six minutes"   "ten years"     "two shares"    "two distinct"  "five cents"   
[15] "two pins"      "five robins"   "four kinds"    "three story"   "three inches"  "six comes"     "three batches"
[22] "two leaves"   
> 
> #2 해설 방식 (이게 더 깔끔해 보임)
> #step1 : 매칭되는 데이터 필터링
> #step2 : 매칭되는 데이터 중에서 매칭되는 부분 추출
> numword <- "\\b(one|two|three|four|five|six|seven|eight|nine|ten)\\b +(\\w+)"
> sentences <- sentences[str_detect(sentences, numword)]
> str_extract_all(sentences, numword) %>% unlist
 [1] "seven books"   "two met"       "two factors"   "three lists"   "seven is"      "two when"      "ten inches"   
 [8] "one war"       "one button"    "six minutes"   "ten years"     "two shares"    "two distinct"  "five cents"   
[15] "two pins"      "five robins"   "four kinds"    "three story"   "three inches"  "six comes"     "three batches"
[22] "two leaves"   

2) 줄임말을 모두 찾아라, 아포스트로피(') 이전과 이후 조각을 분리하라.

먼저 setences에서 매칭되는 데이터들을 먼저 찾아서 필터링 한 뒤에 해당 데이터에서 매칭되는 부분을 추출해 내고 마지막으로 str_split을 통해서 아포스트로피 기준으로 앞 뒤로 분리하면 된다.

# step1 : str_detect함수를 이용해서 정규식이 매칭되는 데이터만 필터링
# step2 : str_extract함수를 이용해서 매칭되는 부분을 데이터로 추출
# step3 : 추출된 데이터에서 어포스트로피 기준으로 문자열 앞 뒤로 분리하기
contraction <- "([A-Za-z]+)'([A-Za-z]+)"
sentences[str_detect(sentences, contraction)] %>%
  str_extract(contraction) %>%
  str_split("'")
#> [[1]]
#> [1] "It" "s" 
#> 
#> [[2]]
#> [1] "man" "s"  
#> 
#> [[3]]
#> [1] "don" "t"  
#> 
#> [[4]]
#> [1] "store" "s"    
#> 
#> [[5]]
#> [1] "workmen" "s"      
#> 
#> [[6]]
#> [1] "Let" "s"  
#> 
#> [[7]]
#> [1] "sun" "s"  
#> 
#> [[8]]
#> [1] "child" "s"    
#> 
#> [[9]]
#> [1] "king" "s"   
#> 
#> [[10]]
#> [1] "It" "s" 
#> 
#> [[11]]
#> [1] "don" "t"  
#> 
#> [[12]]
#> [1] "queen" "s"    
#> 
#> [[13]]
#> [1] "don" "t"  
#> 
#> [[14]]
#> [1] "pirate" "s"     
#> 
#> [[15]]
#> [1] "neighbor" "s"

11.4.7 매칭 치환

> #1 단순한 용법
> x <- c("apple", "pear", "banana")
> str_replace(x,"[aeiou]", "-")
[1] "-pple"  "p-ar"   "b-nana"


> #2 단순한 용법
> x <- c("apple", "pear", "banana")
> str_replace_all(x,"[aeiou]", "-")
[1] "-ppl-"  "p--r"   "b-n-n-"


> #3 str_replace_all - 다중치환
> x <- c("1 house", "2 cars", "3 people")
> str_replace_all(x, c("1" = "one", "2" = "two", "3" = "three"))
[1] "one house"    "two cars"     "three people"


> #4 역참조 활용 - 두 번째와 세 번째 단어의 순서 바꾸기

# 원래 데이터 보기
> sentences %>% 
+   head(3)
[1] "The birch canoe slid on the smooth planks."  "Glue the sheet to the dark blue background."
[3] "It's easy to tell the depth of a well." 

# 변환된 데이터
> sentences %>% 
+   str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "\\1 \\3 \\2") %>% 
+   head(3)
[1] "The canoe birch slid on the smooth planks."  "Glue sheet the to the dark blue background."
[3] "It's to easy tell the depth of a well."   

11.4.8 연습문제

1) 문자열의 모든 슬래시를 역슬래시로 치환하라

str_replace_all("past/present/future", "/", "\\\\")
#> [1] "past\\present\\future"

> 역슬래시를 "\\\\"을 통해서 전달하는 이유는 해당 인자의 부분이 정규식 표현을 포함하는 방식이기 때문에 문자열에 "\\\\"을 전달해야 정규식 표현에서 \\이 되고 정규식 표현 \\은 문자 "\"을 의미하기 때문인 것으로 보인다.

 

2) replace_all을 사용하여 str_to_lower의 간단한 버전을 구현하라.

# str_replace_all - 다중치환 예제
replacements <- c("A" = "a", "B" = "b", "C" = "c", "D" = "d", "E" = "e",
                  "F" = "f", "G" = "g", "H" = "h", "I" = "i", "J" = "j", 
                  "K" = "k", "L" = "l", "M" = "m", "N" = "n", "O" = "o", 
                  "P" = "p", "Q" = "q", "R" = "r", "S" = "s", "T" = "t", 
                  "U" = "u", "V" = "v", "W" = "w", "X" = "x", "Y" = "y", 
                  "Z" = "z")
                  
lower_words <- str_replace_all(words, pattern = replacements)
head(lower_words)
#> [1] "a"        "able"     "about"    "absolute" "accept"   "account"

> 다중치환은 str_replace는 안되고 str_replace_all만 된다.

 

3) 단어의 첫 번째와 마지막 문자를 바꿔라. 여전히 단어가 되는 문자열은 무엇인가?

# 변환
swapped <- str_replace_all(words, "^([A-Za-z])(.*)([A-Za-z])$", "\\3\\2\\1")

# 교집합
intersect(swapped, words)
#>  [1] "a"          "america"    "area"       "dad"        "dead"      
#>  [6] "lead"       "read"       "depend"     "god"        "educate"   
#> [11] "else"       "encourage"  "engine"     "europe"     "evidence"  
#> [16] "example"    "excuse"     "exercise"   "expense"    "experience"
#> [21] "eye"        "dog"        "health"     "high"       "knock"     
#> [26] "deal"       "level"      "local"      "nation"     "on"        
#> [31] "non"        "no"         "rather"     "dear"       "refer"     
#> [36] "remember"   "serious"    "stairs"     "test"       "tonight"   
#> [41] "transport"  "treat"      "trust"      "window"     "yesterday"

> # 변환 부분을 보면 첫 번째와 마지막 문자를 바꾸기 위해서는 단어를 크게 세부분으로 나눠야 한다. 맨 처음 한글자 중간 그리고 마지막 글자 부분

> 한글자는 변경하지 않아도 되고 두글짜부터 첫번째 마지막 글자 바꾸는 것이 해당이 된다. 즉. 첫번째 마지막은 반드시 매칭이 되어야 한다. 그러기에 중간 부분은 (.*)으로 사용한다. 정규식에서 *은 앞의 문자 0개 이상이다.

11.4.9 문자열 분할

library(tidyverse)
library(stringr)

# 1.1 문자열을 조각으로 분할
# 반환되는 조각의 갯수가 다를 수 있으므로 리스트를 반환한다.
sentences %>% 
  head(5) %>% 
  str_split(" ")

# 1.2 조각들을 행렬로 반환 받기 (simplify = TRUE)
sentences %>% 
  head(5) %>% 
  str_split(" ", simplify = TRUE)

# 1.3 반환할 조각의 갯수 지정 가능
sentences %>% 
  head(5) %>% 
  str_split(" ", simplify = TRUE, n = 2)

# 1.4 boundary 함수를 이용하여 문자, 줄, 문장, 단어를 경계로 분할할 수 있다.
x <- "This is a sentence. This is another sentence."
str_split(x, boundary("word"))
str_split(x, boundary("sentence"))

# 2.길이가 1인 문자열 인덱싱 하는법
"a|b|c|d" %>% 
  str_split("\\|") %>% 
  .[[1]]

11.4.10 연습문제

1. "apples, pears, and bananas"와 같은 문자열을 개별 구성요소로 분할하라.

2. 왜 " "보다 boundary("word")로 분할하는 것이 좋은가?

> " " 로 분할하면 문자와 함께 사용되는 ,가 분리되지 않는다. "banana, apple"에서 ","가 분리되지 않는다. boundary("word")를 사용하면 문자와 함께 사용되는 경우인 can't에서 "'"이 분리되지 않는다.

3. 빈 문자열로 "" 분할하면 어떻게 되는가?

> 빈 문자열로 분할하면 문자 하나하나씩 분리됨

> # 연습문제 1
> x <- c("apples, pears, and bananas")
> str_split(x, ", +(and +)?")[[1]] # 이 부분 생각해내는 것도 중요
[1] "apples"  "pears"   "bananas"
> 
> # 연습문제 2
> sentence <- "The quick (\"brown\") fox can’t jump 32.3 feet, right?"
> str_split(sentence, " ")[[1]]
[1] "The"         "quick"       "(\"brown\")" "fox"         "can’t"      "jump"        "32.3"       
[8] "feet,"       "right?"     
> str_split(sentence, boundary("word"))[[1]]
[1] "The"    "quick"  "brown"  "fox"    "can’t" "jump"   "32.3"   "feet"   "right" 

11.4.11 매칭 찾기

str_locate, str_locate_all 함수를 활용해서 매칭되는 문자열의 시작, 종료 위치를 알 수 있다.

> # str_locate
> str_locate("apple, banana, pineapple", "apple")
     start end
[1,]     1   5


> # str_loacate_all
> str_locate_all("apple, banana, pineapple", "apple")
[[1]]
     start end
[1,]     1   5
[2,]    20  24