정규표현식 - 첫번째

2020-07-27

정규표현식(Regular Expression)

정규표현식은 문자열 데이터의 값이 아닌 패턴을 정의하는 식입니다. 주민등록번호를 생각해 봅시다.

123456-2000115

주민등록번호는 우리나라 국민들이 일정한 나이가 되면 나라로부터 부여받는 일종의 일련번호로 사람마다 다 다를 것입니다. 값은 모두 다르나, 다음과 같은 형식입니다.

“6개의 숫자로 구성한 문자”-“7개의 숫자로 구성한 문자”

이와 같이 문자열의 패턴을 표현한 식을 정규표현식이라고 합니다.

R에서 정규표현식

R은 데이터를 다루는 프로그래밍 환경으로 다양한 데이터 유형을 다루기 위한 함수를 제공하고 있으며, 문자로 이뤄진 데이터와 관련이 있는 정규표현식을 위한 함수 또한 제공하고 있습니다.

base 패키지 내의 함수 grep(), grepl(), regexpr(), gregexpr(), sub(), gsub(), regexec(), strsplit() 들은 정규표현식을 지원하는 함수로 POSIX의 표준안을 따르는 패턴을 사용하는 함수들 입니다.1

본 문서에서는 R 기본 패키지에서 사용하는 함수들 대신에 RStudio 개발팀이 제공하는 tidyverse에 포함되어 있는 stringr 패키지에서 제공하는 함수들을 이용하여 R에서 정규표현식을 사용하는 방법에 대해 알아보겠습니다.

stringr 패키지 사용을 위해 tidyverse 패키지를 작업공간과 연결하겠습니다.

library( tidyverse )

주어진 패턴을 포함하는 문자열

주민등록번호를 생각해 봅시다. 주민등록번호는 반드시 문자 -를 포함합니다. 문자열 내에 -이 포함된 경우 이를 추출하는 경우 다음과 같이 사용할 수 있습니다.

rno <- c("790101-1234517", "7901011234517")
str_extract(rno, "-")
## [1] "-" NA

함수 str_extract(x, pattern)는 주어진 문자열 벡터(x)의 각 원소가 패턴에 해당하는 문자를 갖고 있을 경우 이를 반환하고 없을 경우 NA를 반환하는 함수입니다.

단지 - 포함 여부만 확인할 때는 str_detect()를 사용합니다.

rno <- c("790101-1234517", "7901011234517")
str_detect(rno, "-")
## [1]  TRUE FALSE

함수 str_detect(x, pattern)는 주어진 문자열 벡터(x)의 각 원소가 패턴에 해당하는 문자를 포함하고 있을 경우 TRUE를 반환하고 없을 경우 FALSE를 반환하는 함수입니다.

다음의 경우를 생각해 봅시다. - 이후에 나오는 문자는 우리나라의 주민등록번호 구성상 현재 0부터 8까지 존재합니다. 즉 문자가 나올 수 있는 범위를 제한하는 경우입니다. 한 문자가 나올 수 있는 경우를 나타내는 표현으로 대괄호쌍([, ])을 이용합니다.

0부터 8중 임의의 한 글자를 나타내는 패턴은 [012345678]이 됩니다.

rno
## [1] "790101-1234517" "7901011234517"
str_detect(rno, "-[012345678]")
## [1]  TRUE FALSE
str_extract(rno, "-[012345678]")
## [1] "-1" NA

0부터 8까지 숫자를 모두 쓰려니 힘듭니다. 프로그래밍을 하는 사람들은 이런 걸 싫어하기에 한번에 표현할 수 없을까하는 고민을 당연히 하게 됩니다. 마침 012345678은 순서대로 그 값이 존재합니다. 그래서 저도 0부터 8까지라는 표현을 앞서 사용했습니다. 정규표현식에서도 이와 같은 방식을 사용합니다.

-는 대괄호쌍 내에서는 이런 범위를 나타내어 0부터 8을 나타내는 패턴은 [0-8]이 됩니다.

str_detect(rno, "-[0-8]")
## [1]  TRUE FALSE
str_extract(rno, "-[0-8]")
## [1] "-1" NA

이는 일반 문자에도 동일하게 적용됩니다.

다음은 노래 제목입니다. 다음의 제목 중에서 a 다음에 b, c, d 중 한 글자가 들어가는 제목을 찾아봅시다.

titles <- c("abracadabra", "don't start now")
titles
## [1] "abracadabra"     "don't start now"

먼저 개별 문자로 찾는 방식입니다.

str_detect(titles, "a[bcd]")
## [1]  TRUE FALSE
str_extract(titles, "a[bcd]")
## [1] "ab" NA

범위로 표현해 봅시다.

str_detect(titles, "a[b-d]")
## [1]  TRUE FALSE
str_extract(titles, "a[b-d]")
## [1] "ab" NA

이와 같이 대괄호쌍으로 지정하는 것을 문자열 클래스라고 합니다.

또한, 범위로 표현할 수 있는 것들중 그 유형을 특정할 수 있는 것이 있습니다. [0-9]는 숫자, [a-z]는 모든 소문자 등으로요…

그래서 정규표현식에서 이런 패턴을 지칭하는 사전에 정의된 문자열 클래스를 지원하고 있으며 자주 사용하는 것들로 다음과 같은 클래스가 있습니다.

정의된 클래스 설명
[:punct:] 임의의 구둣점(. , ! ? 외 키보드 상 특수문자 #@$ 등)
[:digit:] 임의의 숫자
[:lower:] 임의의 알파벳 소문자
[:upper:] 임의의 알파벳 대문자
[:alpha:] 임의의 알파벳
[:alnum:] 임의의 알파벳이나 숫자
[:space:] 임의의 공백문자 (빈칸, 탭, 엔터 등)
[:blank:] 빈칸 혹은 탭 중 하나

위의 경우외에도 다양한 클래스가 존재 합니다. (링크를 참고해 주세욘)

이스케이프 문자

만일 사칙연산 기호 중 하나를 찾기위해 [+-*/]를 패턴으로 사용한다면, 제대로 작동하지 않을 것입니다. 이는 -가 대괄호내에서 범위를 나타내도록 정했기 때문입니다. 이와같이 사전에 정의된 문자의 역할이 아닌 문자로써 필요한 경우 우리는 역슬래쉬(’', 글꼴에 따라 통화기호(원, 엔 등))를 이스케이프 문자로 사용합니다.

다음을 보겠습니다.

expression <- c("y = ax + b", "y = a^x")
expression
## [1] "y = ax + b" "y = a^x"

사칙연산을 포함하는 문자열을 찾기 위해 다음과 같이 합시다.

str_detect(expression, "[+-*/]")
## Error in stri_detect_regex(string, pattern, negate = negate, opts_regex = opts(pattern)): In a character range [x-y], x is greater than y. (U_REGEX_INVALID_RANGE)

보시는 바와 같이 R이 에러가 발생했다고 보고하고 있습니다. 이를 다음과 같이 바꿔봅시다.

  • 아래의 예에서 역슬래쉬를 두개 사용했습니다. 이는 R의 문자열 데이터에도 이스케이프 문자가 존재하여 데이터로써의 문자가 아닌 다른 기능을 나타낼 때 사용하는 것으로 역슬래쉬를 한번만 사용하면 문자열 데이터 내에서 -를 특수기호로 인식하기 때문입니다. 이를 방지하기 위해 문자로써 역슬래쉬를 사용하기 위해 두 번 겹쳐 사용했습니다.
  • 즉, 처음 사용한 역슬래쉬는 R 문자열에서 이스케이프 처리를 위해 사용하여 뒤에 나오는 역슬래쉬를 문자로써 인식하게 하여 정규표현식의 패턴에서 이를 이스케이프 기호로 인식하게 하기 위함입니다.
  • 이와 유사한 예로 Windows에서 폴더를 구분하는 문자는 역슬래쉬를 사용하므로 R에서 파일의 위치를 나타낼 때 Windows 표현 방법을 사용한다면 ".\\data\\week01.csv" 와 같이 표현합니다. 그러나 R에서는 운영체제에 상관없이 폴더 구분문자로 슬래쉬(/)를 사용할 수 있으므로 "./data/week01.csv"로 표현할 수 있습니다.
str_detect(expression, "[+\\-*/]")
## [1]  TRUE FALSE

문자의 반복

singers <- c("Mose Allison", "Richard Boone", "Louis Armstrong")

가수의 이름 중 두개의 동일한 문자가 연속되는 경우를 생각해 봅시다. 앞서 사전에 정의된 문자열 클래스를 이용하면 다음과 같을 것입니다.

str_detect(singers, "[:alpha:][:alpha:]")
## [1] TRUE TRUE TRUE

이 코드는 알파벳 두 개의 문자가 연속인 패턴을 찾는 것으로 잘 작동합니다. 하지만, 프로그래밍 언어를 만드는 사람들은 연속으로 동일한 것이 나오는 것을 싫어합니다.

다음을 봅시다.

str_detect(singers, "[:alpha:]{2}")
## [1] TRUE TRUE TRUE
str_detect(singers, "[:alpha:]{2,3}")
## [1] TRUE TRUE TRUE
str_detect(singers, "[:alpha:]{2,}")
## [1] TRUE TRUE TRUE

정규표현식에서 중괄호는 반복 횟수를 나타내며 최소값과 최대값을 갖습니다. 위의 코드로 부터 지정하는 방법을 살펴보면

  • 먼저 정확히 2번 나타나는 패턴입니다.
  • 최소 2번에서 최대 3번 나타나는 패턴입니다.
  • 2번 이상 나타나는 패턴입니다.

특수한 기호들

점(‘.’)

정규표현식에서 .은 임의의 한문자를 나타냅니다. 다음 예를 보겠습니다.

titles
## [1] "abracadabra"     "don't start now"
str_detect(titles, "a.r")
## [1]  TRUE FALSE
str_extract(titles, "a.r")
## [1] "abr" NA
str_extract_all(titles, "a.r")
## [[1]]
## [1] "abr" "abr"
## 
## [[2]]
## character(0)
str_extract_all(titles, "a.r", simplify=TRUE)
##      [,1]  [,2] 
## [1,] "abr" "abr"
## [2,] ""    ""
class( str_extract_all(titles, "a.r", simplify=TRUE) )
## [1] "matrix" "array"

위의 예는 문자 a 이후 임의의 한문자가 나타나고 문자 r이 나오는 모든 패턴에 대한 예입니다.

  • titles 벡터의 첫번째 원소에는 이런 패턴이 존재하고 두번째 원소에는 이런 패턴이 존재하지 않음을 알 수 있습니다.

  • 존재할 경우 어떤 문자가 이에 해당하는지 살펴보기 위해 str_extract() 함수를 사용하였으나, 이 함수는 처음 패턴에 해당하는 결과만 보여줍니다.

  • 만일 패턴을 만족하는 모든 문자열을 출력하기 위해 str_extract_all() 함수를 사용합니다. 함수 str_extract_all() 은 벡터의 각 원소별로 리스트의 요소를 구성하고 벡터 원소에서 찾아진 모든 결과들을 리스트의 원소(벡터 원소 순서별로)별로 벡터로 저장합니다.

    • stringr 패키지의 함수 중 이와같이 첫번째 패턴을 찾아서 지정한 행동을 하는 함수들과 함께 함수명에 _all 이 붙어 모든 일치하는 패턴에 주어진 행동을 하는 함수들이 존재합니다.
  • 함수 str_extract_all() 은 인수 simplify를 가질 수 있으며 기본값은 FALSE입니다. 만일 simplify인수에 TRUE를 전달하면, 벡터별로 찾은 모든 패턴의 수가 가장 큰 값을 열의 수로 하는 행렬(Matrix)의 형태 결과를 전달합니다.

반복과 관련한 기호들 : ?, +, *

앞서 반복에서 살펴본 것중 특수한 상황을 나타내는 기호가 있으며 기본으로 사용되는 세가지 기호는 다음과 같습니다.

기호 설명 반복 표현
? 앞의 문자가 없거나 한번 {0,1}
+ 앞의 문자가 1회 이상 반복 {1,}
* 앞의 문자가 0회 이상 반복. 즉, 아예 나타나지 않거나 나타났을 경우 여러번 반복하는 경우 {0,}

예를 통해 확인해 보겠습니다.

userid <- c("goplay", "go22play")
userid
## [1] "goplay"   "go22play"
str_detect(userid, "o[:digit:]?p")
## [1]  TRUE FALSE
str_detect(userid, "o[:digit:]+p")
## [1] FALSE  TRUE
str_detect(userid, "o[:digit:]*p")
## [1] TRUE TRUE

각 줄별로 설명을 드리면 다음과 같습니다.

  • o와 p 사이에 숫자가 안 나오거나 1회 나와야 하므로 숫자가 없는 첫번째 원소가 이 패턴에 해당합니다.
  • o와 p 사이에 숫자가 1회 이상 나와야 하므로 두번째 원소가 이 패턴에 해당합니다.
  • o와 p 사이에 숫자가 안 나오거나 여러번 나오는 패턴으로 두 원소 모두 이에 해당합니다.

그리고 이 반복 기호들은 서로 결합하여 다양한 의미로도 사용합니다.2

갈 길이 아직 많이 남았습니다. 잠시 쉬는 의미로 이번 글에서는 여기까지 설명드리겠습니다. 궁금한 것이 있으시면 아래 메일주소로 말씀주세요.

이스케이프문자와 문자열 클래스

이스케이프 문자 이후 몇 개의 문자는 문자열 클래스를 나타냅니다.

문자열 설명
\d 숫자에 대응
\s 임의의 공백문자(줄바꿈, 탭 등)에 대응
\p{property} 문자와 그 문자의 유니코드 속성에 대응
\w 임의의 단어에 대응
\b 임의의 단어 경계에 대응

위 문자열의 대문자들도 존재하며 소문자 이외를 나타냅니다. 다음의 예를 보겠습니다.

str <- c("Do you have 2 phones?", "전화기를 2대 갖고 계십니까?")

str_extract_all(str, "\\d")
## [[1]]
## [1] "2"
## 
## [[2]]
## [1] "2"
str_extract_all(str, "\\s")
## [[1]]
## [1] " " " " " " " "
## 
## [[2]]
## [1] " " " " " "
str_extract_all(str, "\\p{Uppercase}")
## [[1]]
## [1] "D"
## 
## [[2]]
## character(0)
str_extract_all(str, "\\w+")
## [[1]]
## [1] "Do"     "you"    "have"   "2"      "phones"
## 
## [[2]]
## [1] "전화기를" "2대"      "갖고"     "계십니까"
str_split(str, "\\W")
## [[1]]
## [1] "Do"     "you"    "have"   "2"      "phones" ""      
## 
## [[2]]
## [1] "전화기를" "2대"      "갖고"     "계십니까" ""
str_extract_all(str[1], "\\b")
## [[1]]
##  [1] "" "" "" "" "" "" "" "" "" ""
str_replace_all(str[1], "\\b", "_")
## [1] "_Do_ _you_ _have_ _2_ _phones_?"
str_replace_all(str[1], "\\B", "_")
## [1] "D_o y_o_u h_a_v_e 2 p_h_o_n_e_s?_"
  • \d\s는 위의 예제를 보시면 어떤 것인지 금방 확인하실 것입니다.
  • \p{...}의 경우 유니코드에서 지정한 속성(property)3을 중괄호안에 넣어 속성에 해당하는 문자열을 가져옵니다.
  • \w의 경우 반복 기호 +를 종종 같이 사용합니다. (반복기호 없이 사용하고 결과를 비교해 주세요). 만일 대문자 W를 사용한다면, 단어가 아닌 문자열을 뜻하고, 문자열을 분리하는 함수 str_split()과 함께 사용하여 단어가 아닌 문자로 단어를 구분하여 자르고 잘라진 단어들을 벡터로 저장합니다.
  • \b는 단어 주변을 찾는 것으로 본 예에서는 단어의 앞과 뒤 빈 문자열을 찾았습니다(첫문자 앞도 찾았습니다)
    • 함수 str_replace_all() 은 찾아진 패턴을 사용자가 원하는 문자열로 바꾸는 함수입니다.
    • 단어 경계의 문자를 밑줄 문자로 바꾼 예와 함께 대문자 B를 사용한 결과를 비교해 주시기 바랍니다.
    • 단어 경계가 아닌 것을 찾는 \B를 밑줄 문자로 바꾼 것을 확인할 수 있습니다.

갈 길이 아직 많이 남았습니다. 잠시 쉬는 의미로 이번 글에서는 여기까지 설명드리겠습니다. 궁금한 것이 있으시면 아래 메일주소로 말씀주세요.


  1. 정규표현식을 위한 표준은 크게 POSIX 방식과 프로그래밍 언어인 PERL로 부터 제안한 PCRE (Perl Compatible Regular Expressions)의 두 가지가 있습니다.

  2. RStudio 팀의 문서를 보시면 더욱 많은 내용이 있습니다. RSrtudio팀의 stringr 에서의 정규표현식 문서 중 반복

  3. Unicode Property

R - 기초stringscharacterregular_expression정규표현식문자열

R과 Python에서 데이터 프레임 (I)

R에서 날짜와 시간