(전공복습) 데이터과학 3. 정규표현식

차례

들어가기 전에

이 글은 컴퓨터학과 이중전공생으로서 배운 것들을 다시 한번 정리하고자 남기는 글입니다. 불완전한 기억 등의 이유로 오류가 있을 수 있으며, 참조 내지 이정표로만 사용해주세요.
본 게시글은 고려대학교의 데이터과학 강의를 기반으로 작성자의 추가적인 설명이 덧붙여진 글입니다.

문자열 다루기

우리는 많은 경우 문자열 데이터를 다루게 된다. pandas 데이터프레임에서 문자열 데이터를 다루기 위해서는 문자열 접근자를 사용하면 되는데, 이때 문자열 접근자의 인수에는 일반적인 문자열은 물론이요, 정규표현식을 활용할 수도 있다. 그러니 오늘은 강력하고도 간단(?)한 정규표현식에 대해 조금만 알아보자.
(정규표현식의 이론적인 측면은 추후 다루게 될 계산이론 강의를 참조하라.)

정규표현식(Regular Expression)

정규표현식(Regualr Expression), 짧게는 정규식(Regex)이란, 말 그대로 일종의 표현식(Expression)이다. 그중에서도, 특정한 종류의 문자열들을 나타내는 표현식인데, 주로 어떠한 규칙(패턴)을 만족하는 문자열을 표현한다. 정규표현식은 프로그래밍 언어가 아니라, 문자열의 패턴을 기술하기 위한 방법이다. 그 자체로 프로그래밍 언어가 아니라 단순히 패턴 매칭을 위한 기술 방법이기 때문에, 파이썬, 자바스크립트, 자바 등 다양한 프로그래밍 언어에 정규표현식을 검사하기 위한 함수가 들어있다.

정규표현식의 편리함을 실감하기 위한 예제를 하나 만들어보자. 휴대전화 번호를 11개의 숫자로 이루어지며 3개, 4개, 4개로 나누어 사이에 -가 구분자(Delimiter)로 들어갈 수 있는 문자열이라고 정의하자. 또한 편의를 위해 구분자는 각 자리마다 들어갈 수도 있고, 안 들어갈 수도 있다고 하자. 예를 들어 아래를 유효한 휴대전화 번호라고 할 수 있다.

01012345678
010-1234-5678
010-12345678
0101234-5678

그렇다면 어떤 문자열이 휴대전화 번호를 나타내는 문자열인지 검사하려면 어떻게 해야 할까? 조금만 생각해봐도 매우 복잡해진다. 예를 들어 직관적으로 생각해서 아래와 같은 코드가 가능하다.
(파이썬을 잘 모르는 경우 아래 코드가 낯설 수 있다. 아래 코드는 딱히 중요하지 않으니 이해하지 못한다면 그냥 넘겨도 된다.)

def is_phone_number(s: str) -> bool:
    max_index: int = 10
    has_first_delimiter: bool = False
    has_second_delimiter: bool = False
    first_delimiter_index: int = 3
    second_delimiter_index: int = 7
    for i, c in enumerate(s):
        if c.isdecimal():
            continue
        if i == first_delimiter_index:
            has_first_delimiter = True
        elif i == second_delimiter_index + has_first_delimiter:
            has_second_delimiter = True
        else:
            return False
    if i != max_index + has_first_delimiter + has_second_delimiter:
        return False
    return True

위의 코드는 비효율적이기도 하고, 이해하기도 힘들다. 단순히 11자리 휴대전화 번호를 체크하는 데에도 이러한 함수를 만들어야 한다면 문자열의 패턴을 다루는 일이 매우 고될 것이다. 그래서 정규표현식으로 휴대전화 번호의 규칙을 나타내보도록 하겠다.

^\d{3}-?\d{4}-?\d{3}$

정규표현식을 알지 못한다면, 보고 있어도 외계어로밖에 보이지 않을 것이다. 하지만 이 한 줄은, 위에서 언급한 휴대전화 번호의 조건을 모두 만족하는 패턴을 찾아내는 정규표현식이다. 이 정규표현식의 의미를 대략적으로 이해해보자.
(뒤에서 구체적으로 다룰 것이므로 그냥 그런가보다 하고 보도록 하자.)

  1. ^는 문자열의 시작을 의미한다.
  2. \d{3}은 숫자가 3개 등장함을 의미한다.
  3. -?는 문자열 -이 0개 또는 1개 등장함을 의미한다.
  4. \d{4}는 숫자가 4개 등장함을 의미한다.
  5. -?는 문자열 -이 0개 또는 1개 등장함을 의미한다.
  6. \d{3}은 숫자가 3개 등장함을 의미한다.
  7. $는 문자열의 끝을 의미한다.

이제 우리는 이러한 정규표현식의 규칙을 하나씩 살펴보도록 하겠다. 이때, regex101과 같은, 정규표현식을 검사할 수 있는 웹사이트를 활용하면 더 이해하기 쉽다. 또한 RegexOne에서 정규표현식을 직접 입력해보며 배울 수도 있다.

기초적인 문법

아래는 정규표현식의 기초적인 문법이다.
(구현에 따라 정규표현식의 문법이 약간 다른 경우가 있다. 이 경우 파이썬을 기준으로 생각한다.)

연산 우선순위 예시 패턴 매칭되는 문자열
연결 3 ABBAB ABBAB
OR 4 ABB\|AB ABBAB
클로저(반복) 2 AB* A, AB, ABB, …
괄호 1 (A\|B)(BA)* A, B,ABA, BBA, ABABA, …

하나씩 알아보자.
우선, 연결(Concatenation)이란 말 그대로 두 패턴의 연결이다. 연결에는 아무런 기호를 사용하지 않는다. AB라는 패턴은 정확히 문자열 A 뒤에 문자열 B가 있다는 것, 즉 그냥 문자열 AB를 의미힌다.
OR는 말 그대로 OR 연산이다. 두 패턴 중 어느 것이든 올 수 있다는 것을 의미한다. ABB|ABABBAB 둘 중 하나에만 속하면 매칭된다. 연결이 OR보다 우선되기 때문에, AB(B|A)BABB|AB는 다르다. 괄호를 사용해서 명확히 밝히면 ABB|AB(ABB)|(AB)인 셈이다.
클로저(Closure)는 쉽게 말해 반복이다. AB*에서 *가 바로 반복을 의미하는 기호인데, 이때, 0번의 반복도 반복에 속함에 유의하라. 즉, *기호가 의미하는 것은 바로 앞의 패턴이 없어도 되고(A), 1개만 있어도 되고(AB), 2개가 있어도 되고(ABB), …, 몇개든 상관 없다는 것을 의미한다. 클로저는 연결보다 우선순위가 높기 때문에, AB*(AB)*와 다르다. 괄호를 사용해서 명확히 밝히면 AB*A(B*)인 셈이다.
괄호(Parenthesis)는 일반적인 수식에서의 괄호의 의미를 가진다. 즉, 연산의 우선순위를 바꾸기 위해 사용한다. 예시의 (A|B)(BA)*를 뜯어보자. 우선 (A|B)A|B, 즉 A 또는 B를 나타내는 패턴이다. (BA)*BA가 0번, 1번, 2번, … 반복될 수 있다는 의미이다. 즉, 위 패턴을 풀어 설명하자면 A 또는 B로 시작하고, 그 뒤에 BA가 0번 이상 반복되는 패턴이라 할 수 있다. A, B, ABA, BBA, ABABA, BBABA 등이 해당 패턴에 맞는 문자열이 된다.

정규표현식 확장

위 문법만으로 사실 모든 정규표현식을 작성할 수 있지만, 그렇게 하면 코드가 너무 길어진다. 예를 들어 위에서 배운 기초 문법만으로 아까 등장한 휴대전화 번호의 패턴을 작성하면 아래와 같다.

(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)(-(0|1|2|3|4|5|6|7|8|9)|(0|1|2|3|4|5|6|7|8|9))(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)(-(0|1|2|3|4|5|6|7|8|9)|(0|1|2|3|4|5|6|7|8|9))(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)

끔찍한 결과를 얻게 되었다. 얼핏 보면 암호로 보일 정도이다. 따라서 실제로는 정규표현식을 사용하는 목적(간편함)을 위해, 몇가지 확장 문법을 사용한다. 사실 이 문법들은 너무 당연해서 확장으로 느껴지지도 않는다. 아래는 그 목록이다.

연산 예시 패턴 매칭되는 문자열
임의의 문자 AB. ABA, ABB, ABC, …
문자 집합 [a-zA-Z] a, b, …, A, B, …, Z
하나 이상 AB+ AB, ABB, ABBB, …
있거나 없거나 AB? A, AB
N번 반복 A[ABC]{3} AAAA, AAAB, AAAC, …, ACCC
N~M번 반복 A[ABC]{2,3} AAA, AAB, AAC, …, ACCC

.모든 문자를 의미하는 패턴이다. 물론 문자이므로 길이가 1인 하나의 문자에만 매칭된다. 단, 기본적으로 개행 문자(\n)에는 매칭되지 않는다. 만약 모든 문자가 아닌, 진짜 문자 .에만 매칭하고 싶다면 \.으로 이스케이프하면 된다.
대괄호([]) 안의 문자들은 문자 집합을 나타내는 데에 사용된다. 예를 들어, [abc]a, b, c에 전부 매칭될 수 있다. 문자의 범위를 설정할 수도 있다. [a-z]는 소문자 a부터 z를 의미하고, [가-힣]은 완성된 한글 문자 전체를 의미한다. 또한, 문자 집합이 ^로 시작하면, 문자 집합의 문자들을 제외한 모든 문자로 매칭된다. 예를 들어, [^a-zA-Z]는 알파벳이 아닌 모든 문자에 매칭된다.
+하나 이상의 반복에 매칭된다. 0개 이상의 반복을 매칭하는 *에서 0개인 경우를 제외한 매칭을 의미한다. 헷갈린다면 1*0=0이므로 *는 0개부터 매칭되고, 1+0=1이므로 +는 1개부터 매칭된다고 기억하자.
한편, ?는 이전 패턴이 0개 또는 1개일 때만 사용한다. 정확히 n개라면 {n}을 쓰면 되고, n개에서 m개 사이라면 {n, m}을 사용할 수 있다. 또한, 이 경우 n을 생략하면 0부터, m을 생략하면 무한개까지 반복해서 매칭이 가능하다. 정리하자면 이전 패턴을 반복하고 싶다면 개수에 따라 아래와 같이 사용하면 된다. 이러한 문자들을 수량 한정자(Quantifier)라 부른다.

패턴 최소 최대
* 0 무한
+ 1 무한
? 0 1
{n} n n
{n,m} n m
{,m} 0 m
{n,} n 무한

조금 더 알아두면 좋은 것들

여기에 더해 몇가지 추가적인 문법을 알아두자.
아래 내용 외에도 정규표현식의 세계는 매우 방대하다. 정규표현식을 잘 알아두면 쓸 곳이 정말 많은데, 심지어 지금 이 게시글을 작성하고 있는 vscode나 github 편집기에서 일괄 찾아 바꾸기를 할 때도 사용할 수 있고, pandas에서 문자열을 다루는 거의 모든 경우 정규표현식이 사용 가능하다.

특수 시퀀스(Special Sequence)

정규표현식에서 특정 범주의 문자를 나타내는 데에 사용할 수 있는 문자들이다. 전부 \로 시작하는 이스케이프 시퀀스의 형태를 가지고 있다는 공통점이 있으며, 대문자와 소문자의 작동이 반대라는 특징을 가진다. 몇가지 자주 쓰이는 특수 시퀀스를 알아보자.

연산 예시 패턴 매칭되는 문자열
숫자 \d 1, 2, …, 0
숫자 아님 \D 숫자를 제외한 문자
공백 \s ` , \t,\r`, …
공백 아님 \S 공백을 제외한 문자
영숫자 \w _, a, A, 1, …
영숫자 아님 \W 알파벳, 숫자, 언더스코어를 제외한 문자

이 외에도 다양한 특수 시퀀스가 존재하며, 일부 시퀀스는 설정이나 로캘 등의 변수에 따라 다른 값들을 포함하기도 한다.

문자열의 시작과 끝

문자열의 시작에 매칭하기 위해서는 ^를 사용한다. 문자열의 에 매칭하기 위해서는 $를 사용한다. 예를 들어 ^안녕은 문자열의 맨 앞에 있는 안녕에만 매칭된다.
기본적으로 정규표현식은 문자열 전체에 매칭하지 않더라도 매칭을 생성할 수 있다. 예를 들어, [a-zA-Z]+라는 정규표현식 패턴은 "내 이름은 Alex입니다."라는 문자열에서 Alex에 매칭된다. 만약 문자열 전체가 일치하는 경우에만 매칭이 이뤄지도록 하고 싶다면 패턴의 앞뒤로 ^$를 붙여줄 수 있다. 아래는 각 상황별 매칭의 결과이다.

정규표현식 안녕! 응, 안녕 귀사의 안녕을 기원합니다
안녕 O O O
^안녕 O X X
안녕$ X O X
^안녕$ X X X

Greediness, Laziness

기본적으로 정규표현식을 이용한 패턴 매칭은 그리디(Greedy)하다. 그말인즉슨, 패턴이 항상 가능한 최대한의 문자열을 매칭하려고 한다는 점이다. 예를 들어, HTML 문서의 태그를 정규표현식을 이용해 매칭하려고 한다고 하자.
(참고로, HTML은 절대 정규표현식으로 완벽하게 매칭할 수 없다. 자세한 내용은 한계 챕터를 참조하라.)

<span>안녕하세요.</span>

이 문서 안의 태그를 찾기 위해 <.+>라는 정규표현식을 사용하는 것은 퍽 합리적으로 보인다. 이 정규표현식을 해석하자면, 아래와 같다.

  1. <로 시작한다.
  2. .+, 즉 아무 문자에 1번 이상 매칭한다.
  3. >로 끝난다.

이러한 정규표현식의 의도<span>이나 </span>처럼 꺾쇠괄호로 둘러싸인 태그들을 전부 추출하는 것이겠지만, 실제 결과는 span 태그가 <span>안녕하세요.</span>로 통째로 매칭되는 것이다.
그 이유는, 정규표현식의 매칭이 그리디하기 때문이다. 아래 표를 살펴보자.

패턴 의도 결과
< < <
.+ span span>안녕하세요.</span
> > >

이처럼, .+가 최대한 많은 문자에 매칭되다 보니 예상치 못한 결과를 낳게 되는 것이다. 이러한 일을 방지하려면, 정규표현식의 패턴 매칭이 그리디하지 않고 게으르게(Lazy, 레이지) 처리하도록 만드는 방법이 필요한데, 바로 수량 한정자의 뒤에 ?를 붙이는 것이다. 아래를 참조하라.

패턴 최소 최대 레이지하게
* 0 무한 *?
+ 1 무한 +?
? 0 1 -
{n} n n -
{n,m} n m {n,m}?
{,m} 0 m {,m}?
{n,} n 무한 {n,}?

이렇게 뒤에 ?를 붙인 한정자는 가능한 매칭의 개수 자체는 변하지 않지만, 최소한으로 매칭을 이뤄지도록 만들어준다. 반대로 ?가 뒤에 없는 모든 한정자는 최대한 많은 매칭을 만들어준다.

파이썬에서

파이썬에서의 정규표현식은 re 모듈을 통해 사용할 수 있다. reRegular Expression의 줄임말로, 이 모듈의 함수로 문자열에서 정규식을 만족하는 패턴을 찾아내거나, 대체하는 등의 작업을 할 수 있다.

파이썬에서 정규식 패턴을 나타내는 문자열을 작성할 때는 r-string을 사용하는 것이 권장된다. r-string이란 r"\d+"와 같이 문자열이 시작하기 전, 따옴표 앞에 r이 붙은 문자열을 의미한다. 이러한 문자열은 Raw String으로 해석되어, 원래 파이썬의 이스케이프 시퀀스 기능을 무력화한다. 아래 예시를 참조하라.

print("안녕\t잘가")     # 안녕    잘가
print(r"안녕\t잘가")    # 안녕\t잘가
print("안녕\\t잘가")    # 안녕\t잘가

위 예시에서 확인할 수 있듯이, 기본적으로 파이썬은 이스케이프 시퀀스 기능을 지원하기 때문에 일부 이스케이프 문자가 다른 의미로 해석된다.
문제는, 정규표현식에서 등장한 특수 시퀀스 역시 \로 시작한다는 점이다. 만약 파이썬 문자열 "\d"를 정규표현식 패턴으로 사용한다면 파이썬은 이 패턴 문자열을 \d라는 이스케이프 시퀀스로 해석하게 된다.
따라서, \\와 같이 역슬래시를 2개 넣어서 이스케이프하거나, r-string을 사용해서 역슬래시로 시작하는 문자열을 그 자체로 해석하도록 설정해주어야 한다.

정규표현식의 한계

하지만, 정규표현식은 만능이 아니다. 몇 가지 단점을 알아보도록 하자.

우선, 정규표현식은 너무나 복잡하다. 분명 간단하게 패턴을 표현하기 위해 사용하는 문법이지만, 경우의 수가 늘어날수록 패턴이 복잡해지고 가독성도 극도로 떨어지게 된다. 예를 들어, 유효한 RFC822 이메일 주소를 나타내는 Perl 정규표현식은 아래와 같다.
매우 복잡한 정규표현식
이처럼, 조건이 복잡해짐에 따라 정규표현식은 사실상 이해할 수 없는 외계어가 되어버리고, 심각한 문제를 초래할 수 있다.

또한, 정규표현식은 절대 모든 종류의 패턴을 잡아낼 수 없다. 대표적으로 HTML 파서는 절대 정규식으로 만들 수 없다. 만약 성공한 사람이 있다면 얼른 논문이라도 한 편 출판하시길.
그 이유는 정규표현식은 정규언어(Regular Language)를 표현하기 위한 표현식이기 때문이다. 정규언어는 오직 하나의 상태(현재 상태)만 저장할 수 있다. 즉 정규표현식은 유한 오토마타와 동치인데, 스택이 없는 유한 오토마타는 태그 안에 태그가 중첩될 수 있는 HTML을 완벽하게 파싱할 수 없다.
(이게 무슨 소리인지는 별로 중요한 부분이 아니다. 자세한 것은 계산이론 강의 정리에서 다루게 되며, 데이터과학에 있어서 유한 오토마타나 정규언어는 상대적으로 덜 중요한 주제이다.)

정리

오늘은 정규표현식에 대해 알아보았다. 파이썬 공식 문서에서 정규표현식과 re 모듈의 매우 다양한 사용법에 대해 알 수 있다. 비록 정규표현식이 무엇이든 해결해주는 마법의 열쇠는 아니지만, 데이터를 전처리할 때 반드시 필요하게 될 것이다.

다음 시간에는 시각화(Visualization)에 대해 다루게 된다.

2024

맨 위로 이동 ↑

2023

세그먼트 트리

개요 선형적인 자료구조에서는 값에 접근하는 데에 \(O(1)\)이면 충분하지만, 대신 부분합을 구하는 데에는 \(O(N)\)이 필요하다. 그렇다면 이 자료구조를 이진 트리로 구성하면 어떨까? 값에 접근하는 데에 걸리는 시간이 \(O(\lg N)\)으로 늘어나지만 대신 부분합을 구하...

벨만-포드 알고리즘

개요 다익스트라 알고리즘과 함께 Single Sourse Shortest Path(SSSP) 문제를 푸는 알고리즘이다. 즉, 한 노드에서 다른 모든 노드로 가는 최단 경로를 구하는 알고리즘이다. 다익스트라 알고리즘보다 느리지만, 음수 가중치 간선이 있어도 작동하며, 음수 가중치 사...

다익스트라 알고리즘

개요 다익스트라 알고리즘은 Single Sourse Shortest Path(SSSP) 문제를 푸는 알고리즘 중 하나이다. 즉, 한 노드에서 다른 모든 노드로 가는 최단 경로를 구하는 알고리즘이다. 단, 다익스트라 알고리즘은 음수 가중치 엣지를 허용하지 않는다. 이 경우에는 벨만-...

맨 위로 이동 ↑