메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

PHP와 자바스크립트에서 정규 표현식 사용하기

한빛미디어

|

2010-03-24

|

by HANBIT

27,895

제공 : 한빛 네트워크
저자 : 로빈 닉슨(Robin Nixon)
역자 : 이대엽
원문 : Using Regular Expressions in PHP and JavaScript

정규 표현식(regular expression)은 다른 여러 언어와 마찬가지로 자바스크립트와 PHP에서도 지원한다. 정규 표현식을 쓰면 가장 강력한 패턴일치(pattern-matching) 알고리즘을 단 한 줄의 표현식으로 작성할 수 있다.

모든 정규 표현식은 반드시 슬래시(/)로 감싸야 한다. 이 슬래시 안에서는 특정 문자가 특별한 의미를 지니며, 이러한 문자를 메타문자(metacharacter)라 한다. 가령 별표(*)는 쉘이나 윈도우 명령 프롬프트에서 쓸 때와 의미가 비슷하다(하지만 의미가 완전히 같지는 않다). 별표는 "부합하려는 텍스트에 바로 앞의 문자가 0개 이상 존재할지도 모른다."라는 뜻이다.

메타문자를 통한 일치

"Le Guin"라는 이름을 찾고 싶고 누군가가 이 이름을 쓸 때 띄어쓰기를 했는지 확신할 수 없다고 가정하자. 텍스트가 이상하게 놓여 있을 수도 있으므로(가령 누군가가 텍스트에 오른쪽 정렬을 위해 별도의 공백을 추가했을 지도 모른다), 다음과 같은 줄을 찾아야 할 수도 있다.
The difficulty of classifying Le Guin"s works
그러므로 "LeGuin"뿐 아니라 임의 개수의 공백으로 나뉜 "Le"와 "Guin"도 일치해야 한다. 해결책으로 아래처럼 공백 뒤에 별표를 붙이면 된다.
/Le *Guin/
줄에는 "Le Guin"라는 이름 말고도 여러 가지가 있지만 그래도 괜찮다. 정규 표현식이 해당 줄의 일부분과 일치하면 검사 함수는 참 값을 반환한다. 줄에 "Le Guin"만 포함돼 있는지 확인하려면 어떻게 해야 할까? 나중에 이를 확인하는 방법을 보여주겠다.

항상 공백이 적어도 하나 이상 있다는 사실을 알고 있다고 가정하자. 그런 경우에는 플러스(+) 기호를 쓸 수도 있는데, 이것은 앞에 나오는 문자가 최소한 하나 이상은 존재해야 한다는 뜻이다.
/Le +Guin/
퍼지 문자 일치

점(.)은 특히 유용한데, 이 기호로 줄 바꿈 문자를 제외한 모든 문자와 일치할 수 있다. 텍스트에서 "<"로 시작해서 ">"로 끝나는 HTML 태그를 찾고 있다고 가정하자. 간단하게 다음과 같이 하면 된다.
/<.*>/
점은 어느 문자와도 일치하며 별표(*)는 그러한 일치를 0개 이상의 문자로 확장한다. 따라서 이 패턴은 "< 와 > 사이에 놓인 모든 것(설령 아무것도 없더라도)과 일치한다"라는 뜻이며, <>, ,
등과 일치할 것이다. 하지만 빈 경우인 <>와는 일치하고 싶지 않다면 아래처럼 * 대신 + 기호를 쓰면 된다.
/<.+>/
플러스 기호는 점을 1개 이상의 문자와 일치하는 것으로 확장하여 "<와 >사이에 적어도 하나 이상의 문자가 존재하기만 하면 그 사이에 놓인 모든 것과 일치한다."라는 뜻이 된다. 따라서 이 패턴은 ,

을 비롯해 아래처럼 태그 사이에 속성이 있는 경우까지 일치할 것이다.

하지만 아쉽게도 플러스 기호는 줄의 마지막 >까지 계속해서 일치하므로 다음과 같이,

Introduction

일치된 결과에 1개 이상의 태그가 있을지도 모른다. 나중에 더 나은 해결책을 보여주겠다.

점 문자 자체, 즉 (.)를 일치하고 싶다면 문자 앞에 역슬래시()를 집어넣어 이스케이프 처리를 하면 된다. 이렇게 하지 않으면 점 문자가 메타문자로 작용해 모든 문자와 일치할 것이다. 예를 들어 부동 소수점 수인 "5.0"를 일치하고 싶다고 가정하자. 정규 표현식은 다음과 같다.
/5.0/
역슬래시는 역슬래시 자체를 포함해서(텍스트에서 역슬래시를 일치하는 경우) 모든 메타문자를 이스케이프 처리한다. 하지만 나중에 어떻게 역슬래시가 때때로 그 다음에 오는 문자에 특별한 의미를 부여하는지 살펴보겠다.

방금 부동 소수점 수를 일치해 보았다. 아마 "5."이나 "5.0"도 마찬가지로 모두 부동 소수점 수로서 의미가 같기 때문에 일치하고 싶을 것이다. 또한 "5.00"이나 "5.000" 등도 임의 개수의 0은 허용되므로 일치하고 싶을 것이다. 이러한 일치는 앞서 살펴본 바와 마찬가지로 패턴에 별표를 추가하면 된다.
/5.0*/
괄호를 이용한 그룹화

이번에는 킬로(kilo), 메가(mega), 기가(giga), 테라(tera)와 같은 단위의 증가에 따른 누승을 일치하고 싶다고 가정하자. 즉, 다음과 같은 내용을 일치하고 싶은 것이다.
1,000
1,000,000
1,000,000,000
1,000,000,000,000
...
여기서도 플러스 기호가 효과가 있지만 ",000"라는 문자열을 그룹화해서 플러스 기호가 해당 문자열을 한 묶음으로 일치하게 해야 한다. 정규 표현식은 다음과 같다.
/1(,000)+ /
괄호는 "플러스 기호와 같은 무언가를 적용할 때 이것을 한 묶음으로 간주한다"라는 뜻이다. 따라서 텍스트에는 반드시 1이 나온 다음, 콤마에 이어 0이 세 개 따라붙는 묶음이 하나 이상 와야 하므로 "1,00,000"와 "1,000,00"은 일치하지 않는다.

+ 문자 다음에 오는 공백은 공백을 만났을 때 반드시 일치가 끝나야 함을 가리킨다. 공백이 없으면 "1,000,00"은 첫 "1,000"만 일치하고 나머지 ,00은 무시해서 올바르게 일치되지 않을 것이다. 나중에 오는 공백을 지정해야 숫자의 오른쪽 끝까지 일치가 계속 진행될 것이다.

문자 클래스

간혹 다소 불분명(fuzzy)하지만 점을 써야 할 만큼 광범위하지는 않은 것을 일치하고 싶을 때도 있다. 불분명하다는 건 정규 표현식의 가장 큰 강점인데, 정규 표현식에서는 정확함과 모호함을 원하는 만큼 조절할 수 있다.

퍼지 일치(fuzzy matching)를 지원하는 핵심 기능 중 하나는 바로 대괄호인 []이다. 대괄호는 점처럼 단일 문자를 일치하지만 대괄호 안에는 일치할 수 있는 대상의 목록을 집어넣을 수 있다. 텍스트에서 대괄호에 지정한 문자가 나타나면 해당 텍스트는 일치된 것이다. 예를 들어 미국식 철자인 "gray"와 영국식 철자인 "grey"를 모두 일치하고 싶다면 다음과 같이 패턴을 지정하면 된다.
/gr[ae]y/
일치 대상 텍스트에서는 gr이 나온 후에 a이나 e가 나올 수 있다. 하지만 둘 중 하나만 나와야 하는데, 대괄호에 무엇을 집어넣든 정확히 한 문자와 일치하기 때문이다. 대괄호 안에 든 문자 그룹을 문자 클래스(character class)라 한다.

범위 나타내기

대괄호 안에서는 하이픈(-)을 써서 범위를 나타낼 수 있다. 흔히 하는 작업 중 하나는 숫자 하나를 일치하는 것인데, 다음과 같은 방법으로 범위를 지정해서 일치할 수 있다.
/[0-9]/
숫자는 정규 표현식에서 공통적인 항목이라 숫자 표현을 위해 d라는 문자를 제공한다. 따라서 대괄호로 감싼 정규 표현식에 다음과 같이 d를 써서 숫자를 일치할 수 있다.
/d/
부정

대괄호에서 쓸 수 있는 또 하나의 중요한 기능은 문자 클래스의 부정이다. 여는 괄호 다음에 캐럿(^)을 지정해서 문자 클래스 전체를 뒤집을 수 있다. 이 말은 "다음에 나오는 것을 제외하고 어떠한 문자와도 일치한다."라는 뜻이다. "Yahoo" 다음에 느낌표가 없는 문자열을 찾는다고 가정해 보자. (이 회사의 공식 명칭에는 느낌표가 있다!) 이 경우 다음과 같은 패턴을 쓰면 된다.
/Yahoo[^!]/
단일 문자(느낌표)로 구성돼 있는 문자 클래스를 ^로 시작해서 뒤집었다. 이 방법은 실제로 문제를 해결하는 가장 좋은 방법은 아니다. 예를 들면 "Yahoo"가 한 줄의 끝에 나오면 이 방법은 통하지 않는다. 그 까닭은 해당 문자열 다음에 아무것도 나오지 않아 대괄호가 아무런 문자도 일치하지 못하기 때문이다. 더 나은 해법은 부정형 전방참조(negative look-ahead, 이어서 나오는 텍스트가 조건에 일치하지 않으면 일치함)를 쓰는 것이지만, 해당 주제는 기사의 범위를 벗어나므로 여기서 다루지는 않겠다.

더 복잡한 예제

문자 클래스와 부정을 이해하면 이제 HTML 태그의 일치 문제를 해결할 더 나은 해법을 시도해 볼 준비가 됐다. 이 해법은 단일 태그의 끝을 지나가지 않지만 을 비롯해 아래처럼 속성이 포함된 태그도 일치한다.

해법은 다음과 같다.
/<[^>]+>/
이 정규 표현식이 아무렇게 입력한 것처럼 보여도 완벽히 유효하면서도 매우 유용한 표현식이다. 이 정규 표현식을 여러 부분으로 쪼개보자. 각 요소는 다음과 같다.
  • / - 정규 표현식을 나타내는 여는 슬래시.
  • < - HTML 태그의 여는 괄호. 이 괄호는 일치 대상이며, 메타문자가 아니다.
  • [^>] - 문자 클래스. ^>는 "닫는 괄호를 제외한 모든 문자와 일치한다"라는 뜻이다.
  • + - 이전 패턴인 [^>]와 일치하는 문자가 하나라도 있으면 임의 개수의 문자와 일치한다.
  • > - HTML 태그의 닫는 괄호. 이 문자는 일치 대상이다.
  • / - 정규 표현식의 끝을 나타내는 닫는 슬래시.
이번에는 흔히 쓰는 정규 표현식을 하나 살펴보자.
/[^a-zA-Z0-9_]/
위 정규 표현식에는 또 다른 두 가지 중요한 메타문자가 있다. 두 메타문자는 정규 표현식이 특정 위치에 나타나게 하여 정규 표현식의 위치를 지정한다. 캐럿(^)이 정규 표현식이 시작할 때 나타나면, 해당 표현식은 한 줄이 시작하는 위치에 나와야 한다. 그렇지 않으면 일치하지 않는다. 이와 비슷하게 달러 기호($)가 정규 표현식의 끝에 나타나면 해당 표현식은 한 줄의 끝에 나와야 한다.

이제 기초적인 정규 표현식 맛보기는 다음과 같은 질문에 답하면서 마무리하겠다. 한 줄에 정규 표현식 외에는 아무것도 없게 만들고 싶다면? 한 줄에 "Le Guin" 말고는 아무것도 없게 만들고 싶다면? 이전 정규 표현식에 처음과 끝을 나타내는 부분을 추가하면 이렇게 할 수 있다.
/^Le *Guin$/
메타문자 요약

다음은 정규 표현식에서 사용 가능한 메타문자를 보여준다.
  • / - 정규 표현식을 시작하고 끝낸다.
  • . - 줄 바꿈 문자를 제외한 임의의 한 문자와 일치한다.
  • element* - 요소와 0번 이상 일치한다.
  • element+ - 요소와 1번 이상 일치한다.
  • element? - 요소와 일치하지 않거나 한 번 일치한다.
  • [characters] - 대괄호 안에 포함된 한 문자와 일치한다.
  • [^characters] - 대괄호 안에 포함되어 있지 않은 한 문자와 일치한다.
  • (regex) - regex를 그룹으로 간주하여 갯수를 세거나, 다음에 오는 *, +, ?에 적용한다.
  • left|right - left나 right와 일치한다.
  • l-r - l과 r사이의 문자 범위와 일치한다(대괄호 안에 있을 때만)
  • ^ - 일치하는 패턴이 문자열의 처음에 있어야 함
  • $ - 일치하는 패턴이 문자열의 끝에 있어야 함
  •  - 단어의 경계와 일치한다.
  • B - 단어의 경계가 아닌 문자와 일치한다.
  • d - 숫자 문자 하나와 일치한다.
  • D - 숫자 문자가 아닌 문자와 일치한다.
  • - 줄 바꿈 문자와 일치한다.
  • s - 공백 문자와 일치한다.
  • S - 공백 문자가 아닌 문자와 일치한다.
  • - 탭 문자와 일치한다.
  • w - 단어 문자(a-z, A-Z, 0-9, _)와 일치한다
  • W - 비단어 문자(a-z, A-Z, 0-9, _이 아닌 문자)와 일치한다
  • x - x(x가 메타문자이더라도 x를 쓰고자 할 때 유용함)
  • {n} - 정확히 n번 일치한다.
  • {n,} - n번이나 그 이상 일치한다.
  • {min,max} - 최소 min, 최대 max 번 일치한다.
위에서 설명한 내용을 바탕으로 /[^a-zA-Z0-9_]/라는 정규 표현식을 다시 한번 살펴보면, 이 표현식을 /[^w]/으로 줄여 쓸 수 있음을 알 수 있다. 이는 단일 메타문자인 w(소문자 w)가 a-Z, A-Z, 0-9, _를 나타내기 때문이다.

사실 앞의 정규 표현식을 더 명확하게 표현할 수도 있는데, 메타문자 W (대문자 W)가 a-Z, A-Z, 0-9, _를 제외한 모든 문자를 나타내기 때문이다. 따라서 ^ 메타문자를 빼고 단순히 /[W]/로 쓸 수도 있다. 이 정규 표현식이 어떻게 동작하는지 알려주기 위해 아래에 다양한 표현식과 그와 일치하는 패턴을 나열하였다.
  • r - The quick brown에서 첫 r
  • rec[ei][ei]ve - receive나 recieve (receeve나 reciive도 일치함)
  • rec[ei]{2}ve - receive나 recieve (receeve나 reciive도 일치함)
  • rec(ei)|(ie)ve - receive나 recieve (receeve나 reciive는 일치하지 않음)
  • cat - I like cats and dogs에서 cat 단어
  • cat|dog - I like cats and dogs에서 cat이나 dog라는 단어
  • . - . (.가 메타문자이므로 가 필요함)
  • 5.0* - 5., 5.0, 5.00, 5.000, 등
  • a-f - a, b, c, d, e, f 중 문자 하나
  • cats$ - My cats are friendly cats에서 마지막 cats
  • ^my - my cats are my pets에서 첫 my만
  • d{2,3} - 둘 또는 세 개의 숫자 (00에서 999까지)
  • 7(,000)+ - 7,000, 7,000,000, 7,000,000,000, 7,000,000,000,000 등
  • [w]+ - 하나 이상의 문자로 이루어진 단어
  • [w]{5} - 임의의 5글자로 이루어진 단어
일반적인 변경자

정규 표현식에서는 몇 가지 변경자(modifier)를 사용할 수 있다.
  • /g - "전역" 일치를 활성화. 바꾸기(replace) 기능을 쓸 경우 이 변경자를 쓰면 첫 번째 일치 결과만 바꾸는 것이 아니라 모든 일치 결과를 바꾼다.
  • /i - 정규 표현식이 대소문자를 구별하지 않음. 따라서 /[a-zA-Z]/ 대신 /[a-z]/i나 [A-Z]/i를 지정하면 된다.
  • /m - 캐럿(^)과 달러 기호($)가 대상 문자열 안의 줄 바꿈 전과 후와 일치하는 경우, 여러 줄 모드(multi-line mode)를 활성화. 보통 여러 줄 문자열에서 ^는 문자열의 시작과 일치하고, $는 문자열의 끝과 일치한다.
예를 들어, /cats/g라는 표현식은 "I like cats and cats like me"라는 문장에서 "cats"가 나타나는 곳과 모두 일치할 것이다. 이와 비슷하게, /dogs/gi라는 패턴은 "Dogs like other dogs"라는 문장에서 "dogs"("Dogs"와 "dogs")가 나타나는 곳과 모두 일치하는데, 이는 이러한 변경자를 함께 쓸 수 있기 때문이다.

자바스크립트에서 정규 표현식 사용하기

자바스크립트에서는 주로 test(이미 본 적이 있을 것이다)와 replace라는 메서드에서 정규 표현식을 쓸 것이다. test 메서드가 단지 전달한 인자와 정규 표현식의 일치 여부를 알려주는 데 반해, replace 메서드는 일치하는 텍스트를 대체할 문자열을 나타내는 두 번째 매개변수를 취한다. 대부분의 함수와 비슷하게 replace 메서드는 새로운 문자열을 반환값으로 생성하므로 입력된 문자열을 변경하지 않는다.

두 메서드를 비교하는 차원에서 아래 문장을 살펴보자. 아래 문장은 "cats"라는 단어가 전달한 문자열의 어딘가에 적어도 한번 나타나면 true를 반환한다.
document.write(/cats/i.test("Cats are fun. I like cats."))
하지만 다음 문장은 "cats"가 나타나는 곳을 "dogs"로 바꾸고 그 결과를 출력한다. 검색은 전역적으로(/g) 이루어져 모든 단어가 출현하는 곳을 찾고, 대소문자를 구별하지 않으므로(/i) 대문자로 쓴 "Cats"도 찾는다.
document.write("Cats are fun. I like cats.".replace(/cats/gi,"dogs"))
위 문장을 실행하면 replace의 한계를 확인할 수 있다. replace는 문자열을 정확히 사용하려고 하는 문자열로 바꾸기 때문에 첫 단어인 "Cats"는 "Dogs"가 아닌 "dogs"로 바뀐다.

PHP에서 정규 표현식 사용하기

PHP에서 쓸 법한 가장 널리 쓰는 정규 표현식 함수는 preg_match, preg_match_all, preg_replace이다.

어떤 문자열에서 대소문자를 구별하지 않고 "cats"라는 단어가 나타나는지 검사하려면 아래처럼 preg_match를 쓰면 된다.
$n = preg_match("/cats/i", "Cats are fun. I like cats.");
PHP에서는 1을 TRUE로, 0을 FALSE로 사용하므로 이전 문장에서는 $n이 1로 설정된다. 첫 인자는 정규 표현식이고 둘째 인자는 일치할 텍스트이다. 하지만 preg_match는 실제로 더 강력하고 복잡한데, 이 함수는 일치할 텍스트를 나타내는 셋째 인자를 받기 때문이다.
$n = preg_match("/cats/i", "Cats are fun. I like cats.", $match);
echo "$n Matches: $match[0]";
셋째 인자는 배열(여기서는 $match)이다. 이 함수는 일치하는 텍스트를 첫 요소로 넣으므로 만약 일치가 성공하면 $match[0]에서 일치된 텍스트를 확인할 수 있다. 이 예제에서는 출력 결과를 통해 일치된 텍스트가 대문자로 시작함을 알 수 있다.
1 Matches: Cats
일치 위치를 모두 확인하고 싶다면 아래처럼 preg_match_all 함수를 쓰면 된다.
$n = preg_match_all("/cats/i", "Cats are fun. I like cats.", $match);
echo "$n Matches: ";
for ($j=0 ; $j < $n ; ++$j) echo $match[0][$j]." ";
전과 마찬가지로 $match를 함수에 전달했으며, $match[0]에는 일치된 결과가 할당되는데, 이번에는 2차원 배열의 부배열(sub-array)로 할당된다. 이 예제에서는 부배열을 보여주기 위해 for 루프를 이용해서 배열을 순회하였다.

문자열의 일부를 바꾸고 싶으면 아래처럼 preg_replace를 쓰면 된다. 이 예제는 대소문자를 구별하지 않고 "cats"가 나타나는 곳을 모두 "dogs"으로 바꾼다.
echo preg_replace("/cats/i", "dogs", "Cats are fun. I like cats.");
TAG :
댓글 입력
자료실