2015-06-30

정규식 패턴: 잃어버린 텍스트을 찾아서

정규식은 위키에서는 다음과 같이 설명하고 있다.
정규 표현식(正規表現式, 영어: regular expression, 간단히 regexp 또는 regex) 또는 정규식(正規式)은 특정한 규칙을 가진 문자열의 집합을 표현하는 데 사용하는 형식 언어이다.

  다시 말해서 정규식은 특정한 규칙에 해당하는 문자열을 뽑아내는 작업이라고 설명할 수 있을 것 같다. 예를 들면 이메일 형식의 문자만 추출하거나, URL 형식의 문자만 추출하거나 또는 다른 원하는 규칙에 해당하는 문자열을 다른 문자열로 바꾸고 싶을 때도 정규식을 사용할 수 있다. 이러한 정규식은 파이썬, 자바 스크립트, PHP 등등 많은 곳에서 응용하여 사용하고 있다. 늘 상 사용하는 기능은 아니지만 꼭 필요한 순간은 예고 없이 찾아온다.
  개인적으로 정규식에 대해서 항상 어렵다는 생각을 가지고 있었다. 지금 돌이켜보면 정규식에 대한 막연한 편견이었던 것 같다. 작업을 할 때 특수 문자가 주는 어려운 이미지 때문에 직접 작성하기 보다는 다른 방법으로 우회해서 사용하거나 꼭 필요한 경우에는 예제를 찾아서 변경하여 사용했었다. 어느 것이나 그렇지만 예제를 찾는다는 것은 내게 꼭 맞는 정규식은 아니었다. 그 때 정규식에 대해서 하나하나 뜯어보기 시작했는데 마음 먹고 들여다 본 정규식은 생각보다 어렵지 않았다. 다시 공부한 걸 생각도 할 겸 나와 같은 상황에 빠진 사람들에 조금이나마 도움이 되지 않을까 해서 정리해 보았다.
기준은 PHP로 잡고 정리 했지만 큰 그림은 비슷하기 때문에 한 번 이해하면 어디에서든 쓸 수 있을 거라고 생각한다.

정규식 예제에 사용 할 범주
  • /예제로 사용한 정규식/
  • 정규식을 적용할 문자열
  • []: 정규식을 찾고난 결과 값

정규식의 기본 사용

/blah-blah/
양 쪽에 있는 슬래시(/) 안에 문자를 넣어서 사용한다. 슬래시 안의 문자들을 정규식 패턴으로 인식하고 그 안에 기술된 패턴대로 문자열을 찾기 시작한다. 예를 들어 a라는 글자를 찾고 싶으면 다음과 같이 입력하면 된다.

/a/

a ab abc

[a]
/aaa/

a ab aaa

[aaa]
기본 사용법은 익혔으니 그 안의 내용을 작성하는 방법을 알아보자. 너무 겁 먹지 않아도 된다. 신경 쓸 게 많아 보이지만 생각보다 간단하다. 이 다음부터 나오는 모든 사용법은 슬래시 안에 작성해 주어야 한다.

문장이 시작하거나 끝나는 지점

^blah : blah로 시작하는 패턴

/^start/

start begin!

[start]
전체 문장의 시작이 정확히 start라고 시작하는 패턴을 찾는다. 예를 들어 찾아야 할 텍스트가 everybody start begin!이라면 start로 시작하지 않기 때문에 규칙에 맞는 텍스트는 없을 것이다.

end$ : end로 끝나는 패턴

/end$/

The end

[end]
찾아야할 문자열 중에서 가장 마지막 문자가 end로 끝나야만 한다. 예를 들어 찾아야 할 텍스트가 The end.라면 점(.)이 있기 때문에 규칙에 맞는 텍스트는 없다.

A|B : A 또는 B

/hell|hi/

hell hi hello

[hell]
hello 또는 hi라는 문자열을 찾기 때문에 결과는 가장 처음에 나오는 hell을 찾을 것이다.

여기에서 왜 hi를 찾지 않는지 궁금할 것이다. 이 쯤 해서 플래그에 대해서 알아야 할 것 같다. 기본적으로 규칙을 작성하게 되면 가장 처음 추출되는 문자열 하나를 찾는다. 그러나 제시한 텍스트에서 규칙에 해당하는 문자열을 모두 찾고 싶다면? 그때 플래그를 넣어주면 된다.

옵션 플래그

옵션 플래그에는 여러 가지가 있다. 그러나 가장 많이 사용하는 3가지만 소개하도록 한다. 물론 여기서 소개하지 않았다고 해서 중요하지 않거나 유용하지 않은 플래그라는 것은 아니다. 플래그는 하나만 사용할 수 도 있고 다른 플래그와 조합하여 여러개를 사용할 수 있다.

g: 전체 지정

/hell|hi/g

hell hi hello

[hell, hi, hell]
g 플래그는 전체 문자열에서 작성한 규칙에 해당하는 문자열을 모두 찾아낸다. g 플래그를 붙이지 않으면 규칙에 맞는 가장 첫 번째 문자열만 찾고 책임을 다하지만 g 플래그를 붙이면 규칙에 해당하는 모든 문자열을 찾는다.

i: 대소문자 무시

/hell|hi/gi

HELL hell HI hi hello

[HELL, hell, HI, hi, hell]
대소문자 상관없이 hell에 해당하는 문자열은 모두 찾는다. 또한 g 플래그가 붙었기 때문에 대소문자 구분 없이 hell, HELL 모두 찾아낸다.

m: 여러 줄에 해당 내용을 지정

/hello$/m

HELL hell HI hi hello!
HELL hell HI hi hello
HELL hell HI hi hello

[hello]

/hello$/gm

HELL hell HI hi hello!
HELL hell HI hi hello
HELL hell HI hi hello

[hello, hello]
m의 경우에 헷갈릴 수도 있을 것 같기도 한데 각 줄에서 해당하는 규칙을 찾아낸다. 다시 말해서 ^ 또는 $ 문자는 전체 문자열의 가장 처음 또는 가장 마지막 규칙을 찾아내는데 m 플래그를 사용할 경우 각 줄에서 마지막이 hello로 끝나는 단어를 찾아낸다. 그래서 바로 위의 예제에서는 hello로 끝나는 두 번째와 세 번째 hello를 반환 한다. g 플래그를 붙이지 않은 첫 번째 예제에서는 각 줄에서 작성한 규칙에 해당하는 두 번째 줄의 hello 문자열을 찾고 끝난다.

문자 출력

지금부터는 본격적인 규칙 작성을 시작한다. 가장 많이 사용하고 또 자유자재로 작성해야 할 문자들이다. 다음에서 소개할 내용은 반드시 숙지하고 있어야 하는데 어쩌면 암기하려 노력하지 여러 번 사용하면서 자연스럽게 외워질 수도 있다.

. : 특수 문자 \n을 제외한 모든 문자 한 개

/h./g

HELL hell HI hi hello!
HELL hell HI hi hello

[he, hi, he, he, hi, he]

* : 이전 문자가 0번 이상 반복 (반복하지 않거나 1번 이상)

/d*/g

a dd ddd dddd ddddd aaaaaa

[ , ,dd, ,ddd, ,dddd, ,ddddd, ]
별표(*)가 단독으로 쓰일 경우 0번 이상 반복하는 것까지 찾기 때문에 사실상 공백까지도 결과 범위에 포함한다.

+ : 이전 문자가 1번 이상 반복

/a+/g

a dd ddd dddd ddddd aaaaaa

[a, aaaaaa]

? : 이전 문자가 존재하거나 하지 않거나

/https?/g

http:// https:// httpss://

[http, https]
물음표(?) 앞에 나오는 s가 없거나 있는 경우 모두 찾아낸다.

{n,n} : 숫자를 반드시 지정해야 하며 그 안의 수 만큼 반복

/d{3,4}/g

a dd ddd dddd ddddd aaaaaa

[ddd, dddd, dddd]
d가 3번 이상 4번 이하 반복하는 문자열을 찾기 때문에 3번 반복하는 3번째 ddd부터 5번째에 있는 dddd까지 반환한다.

문자 지정 또는 범위

해당하는 문자 지정

[] 문자열을 사용하면 []라고 감싼 부분이 하나의 문자로 인식 하고 그 안에 작성한 문자열이 해당하는 범위가 된다.

/[ac3]/g

1234
abcd

[3, a, c]
위의 예제에서 보듯 [] 안에 a, c, 3이라는 3개의 문자열이 있다. []는 해당 자리에 문자가 하나 들어간 다는 뜻인데 그 하나의 문자열이 a나 c 또는 3일 때 해당 규칙이 성립한다고 본다. (물론 g 플래그를 사용했기 때문에 해당하는 문자열을 전부 추출한다.)

- : 해당하는 문자 범위 지정

만약 이 안에 하이픈(-)으로 지정하면 범위로 정해진다. 예를 들면 [0-9]라고 지정하면 0부터 9까지의 숫자를 의미하고 [a-z]라고 하면 소문자 알파벳 a부터 z까지가 해당한다. 같은 방식으로 대문자 알파벳 범위도 지정 가능하다. 또한 범위를 결합해서 사용할 수도 있다. 다시 말해서 [0-9a-z]라고 지정하면 숫자 0부터 9까지와 a부터 z까지에 해당하는 문자열 하나를 말한다. 물론 이 상황에서 대문자는 포함하지 않는다.
결국 [0-9]는 모든 숫자를 의미하고, [a-z]는 모든 소문자 알파벳, [A-Z]는 모든 대문자 알파벳이고, 당연히 [a-zA-Z]는 모든 알파벳을 의미한다.
 
/[0-9]/g

1234
abcd

[1, 2, 3, 4]

^ : 해당하지 않는 문자 (또는 범위) 지정

물론 해당 범주에 속하지 않도록 지정하는 규칙도 있다. ^ 문자를 사용하면 되는데 범위를 설정할 때는 문자열의 시작이라는 뜻이 아니라 부정의 의미를 담고 있다. 다음의 예제로 이해하면 더 쉬울 것이다. 0-9를 부정 (^) 하는 뜻이므로 숫자가 아닌 한 문자열을 의미하는 정규식이다.

/[^0-9]/g

1234
abcd

[a, b, c, d]

자주 사용하는 특수 문자

정규식에서는 특수 문자를 표현하는 문자로 패턴을 나타내기도 한다. 그 패턴은 일반 문자열 앞에 백슬래시(\)를 붙여서 특수한 용도로 사용한다. HTML 태그를 아는 사람은 익숙한 내용일 수도 있다. 다음은 자주 사용하는 특수 문자 리스트이다.
  • \n : 새로운 줄 (줄바꿈 용도로 사용하는 엔터와 같음)
  • \t : 탭 문자
  • \b : 공백 문자 (띄어쓰기 용도로 사용하는 스페이스와 같음)
  • \d : 정수 ([0-9]와 같음)
  • \D : 정수가 아닌 문자열 ([^0-9]와 같음)
  • \s : 공백 문자 (\n, \r, \t 등등이 해당됨)

/[\d]/g

1234
abcd

[1, 2, 3, 4]
기본적으로 정규식은 문자 규칙을 작성하는 것이기 때문에 여러 문자를 조합해서 사용한다.
그러나 실제로 \b라는 문자를 찾고 싶을 수도 있다. 그때는 역슬래시(\)를 해당 문자 앞에 붙여준다. 예를 들어 \b라는 문자를 찾고 싶다면 \\b라고 입력하면 된다.

/\\b/g

\b \a \t \b

[\b, \b]

문자 그룹화: ()

찾아낸 문자를 교체한다던가 정규식에 포함되는 내용 중 특정 문자를 추출하고자 할 때는 그룹화해서 문자열을 추출한다. 만약 리스트 태그(<li>)를 추출하는데 그 중에서도 클래스 명이 name인 내용만 추출하고 싶다면 그룹화을 사용하여 작업할 수 있다.

/<li.+?class="name">(.+?)<\/li>/g

<ul>
<li class="name">파무침</li>
<li class="phone">000-0000-0000</li>
<li class="name">파절임</li>
<li class="phone">000-0000-0000</li>
</ul>

[파무침, 파절임]
PHP에서는 preg_replace 함수를 이용하여 문자열을 교체할 수도 있다. 다음은 PHP 공식 홈페이지에서 가져온 preg_replace 예제이다. 첫번째 괄호는 ${1}또는 $1로 치환되고 두번째 괄호, 세번째 괄호는 각각 ${2} 또는 $2, ${3} 또는 $3으로 치환하여 사용한다.

<?php
$string = 'April 15, 2003';
$pattern = '/(\w+) (\d+), (\d+)/i';
$replacement = '${1}1,$3';
echo preg_replace($pattern, $replacement, $string);
?>
April1,2003

탐욕스러운(Greedy), 게으른(Lazy, Non-Greedy)

  • greedy :+ * {n,}
  • lazy : +? *? {n,}?
정규식이 살아있는 사람이라면 기본적으로 욕심많은 사람이다. 그러나 때론 욕심이 많으면 일을 그르칠 수도 있기에 욕심을 자제할 때도 있어야 하는 법이다. 욕심 많은 이 사람을 자제하거나 성격을 바꿀 수 있는 마법의 도구가 있다. 차이점을 알아차린 사람도 있겠지만 탐욕스러운 패턴에서 ?를 붙이면 마음이 조금 덜 탐욕스러워진다. 쉽게 설명하기 위해 가장 처음 요소인 <html>을 추출하려고 하는 아래의 예시를 같이 보도록 하자.
/<.*>/

<html><head><title>제목</title></head>

[<html><head><title>제목</title></head>]
모든 문자가 없거나 반복되는 내용으로 <>로 닫히는 태그를 찾고 있다. 그러나 출력되는 결과는 예상과는 다르다. 괄호로 닫히는 것은 맞지만 우리가 원했던 것은 <html>코드였지만 마지막 >까지 모두 추출해버렸다. 이 탐욕스러운 정규식!

이제 이 탐욕스러운 마음을 조금 진정하도록 도와주자. 다음과 같이 물음표를 붙이면 된다.

/<.*?>/

<html><head><title>제목</title></head>

[<html>]
함께 살펴본 2가지의 예시와 같이 물음표 하나로 정규식의 마음을 쥐락펴락할 수 있다. 이 마법의 문자를 기억하고 황에 따라 조절하며 사용하기 바란다.

예제

이제 대부분의 정규식에 대해서 공부 해 봤으니 실제 예시를 통해서 정규식을 사용해 보도록 하자. 가장 기본적인 전화번호 추출 방식부터 생각해보자.

전화번호(휴대폰 번호)

쉬운 예시를 위해 국제 번호는 제외한 휴대폰 번호만 추출하도록 하자. 전화번호의 경우에는 다음과 같은 패턴을 가지고 있다. 각 자리에는 당연히 숫자만 허용할 수 있고 각 국번 사이에는 하이픈(-)으로 연결되어 있기에 숫자와 정확한 곳에 자리 잡은 하이픈이 필요하다. 그 내용은 다음과 같이 정리할 수 있다.
{숫자 3자리}-{숫자 3자리 또는 4자리}-{숫자 4자리}
요새는 많이 사용 하지 않지만 가운데 국번이 3자리인 휴대폰 번호도 존재한다. 최대한 모든 휴대폰 번호를 수용할 수 있는 패턴을 만들어야 오류 발생율이 적어지기 때문에 3자리도 같이 패턴으로 처리하도록 하자. 이러한 생각을 바탕으로 정규식은 다음과 같이 설계할 수 있다.
/[0-9]{3}-[0-9]{3, 4}-[0-9]{4}/g

010-0000-0000
010-1234-1234

[010-0000-0000, 010-1234-1234]

주소 (URL)

인터넷 주소의 경우에는 전화번호보다는 조금 더 복잡하다. 우선, 한글 도메인은 예시에서 제외하도록 하자. 실제로 한글 도메인이 존재하기 때문에 실 개발할 때 URL을 영문으로만 한정짓는 것은 현명하지 못한 선택일 수도 있다. 그러나 설명을 위한 예시이므로 무시하는 것으로 한다.
URL 주소의 경우에는 워낙 종류도 많고 설정할 수 있는 규칙도 다양하기 때문에 이 내용이 정답이라고 할 수는 없다. {프로토콜}{주소}로 이루어져 있으며 그 중에서 프로토콜의 경우에는 http:// 또는 https:// 로 시작할 수도 있다. 이 부분만 정규식으로 표현한다면 다음과 같다. 슬러시(/) 문자는 정규식에서 사용하는 문자이기 때문에 역슬래시(\)를 작성하지 않으면 오류가 난다.
https?:\/\/
s 문자가 있을 수도 있고 없을 수도 있기 때문에 물음표를 입력하여 표현하였다. 주소 부분은 점(.)이나 하이픈(-) 그리고 문자열로 이루어져 있다. 이 내용은 다음과 같이 표현할 수 있다.
[0-9a-z.-]+
이 두 개의 정규식을 이어서 완성해 보면 다음과 같다.

/https?:\/\/[0-9a-z.-]+\/?/g

https://test010.com/
http://test010.co.kr/
https://test010.kr

[https://test010.com/, http://test010.co.kr/, https://test010.kr]
더 정확하게 표현하고 싶다면 이렇게 할 수도 있다.

/https?:\/\/([0-9a-z.-]+).[a-z]\/?/g

https://test010.com/
http://test010.co.kr/
https://test010.kr

[https://test010.com/, http://test010.co.kr/, https://test010.kr]

마무리

어느 것이든 다 마찬가지지만 실제 해당 기술 자체 보다는 그 기술을 어떻게 응용하고 이용하느냐에 따라서 달라진다. 어쩌면 문자열을 추출하는 작업을 할 때 정규식이 아니라 다른 방식 (substr 함수를 사용한다던가 if문으로 분기로 처리한다던가) 으로 처리하는 것이 훨씬 나을 때가 있다. 그러나 분명 정규식을 사용했을 때 더 편리하거나 정확한 경우가 있다고 믿고 있다. 많은 방법을 알고 있을 수록 작업을 할 때 더 효율적으로 선택할 수 있을 것이다.
모든 정규식을 외울 필요는 없지만 어느 정도 기본적인 지식이 있다면 정규식 검증 사이트에서 테스트를 해볼 수도 있다. 개인적으로 regex101이라는 사이트에서 많은 도움을 받았으나 정규식을 검증해주는 사이트는 얼마든지 많이 있다.


참고:
  • https://ko.wikipedia.org/wiki/%EC%A0%95%EA%B7%9C_%ED%91%9C%ED%98%84%EC%8B%9D
  • https://wikidocs.net/1642
  • http://php.net/manual/en/function.preg-replace.php
  • https://regex101.com/
continue reading 정규식 패턴: 잃어버린 텍스트을 찾아서
Share This:    Facebook Twitter