profile

이 세상에 하나는 남기고 가자

세상에 필요한 소스코드 한줄 남기고 가자

PHP 정규식(PCRE)의 모든 것 - 어설션(Assertions)

유영재

Assertions (어설션, 단정)

어설션은 실제로 어떤 문자도 사용하지 않고 현재 일치 지점의 앞/뒤 문자에 대한 테스트이다. \b, \B, \A, \Z, \z, ^$로 코딩된 간단한 어설션은 앞서 설명했다. 더 복잡한 어서션은 서브 패턴으로 코딩 된다. 이 어설션은 대상 문자열의 현재 위치보다 앞을 검사하는 look-ahead와 뒤를 검사하는 look-behind 두 가지 종류가 있다.

어설션 서브 패턴은 현재 일치하는 위치가 변경되지 않는다는 점을 제외하면 일반적인 방법으로 일치한다.

Look-ahead 어설션은 (?= 긍정 어설션, (?!은 부정 어설션을 나타낸다. 예를 들어, \w+(?=;)는 단어 뒤에 세미콜론이 있는 단어를 일치하지만 세미콜론은 포함하지 않는다.

<?php
$str = 'abc;def';
preg_match('/\w+(?=;)/', $str, $matches);
print_r($matches);
// Array
// (
//  [0] => abc
// )

foo(?!bar)는 "bar"가 뒤에 붙지 않는 "foo"와 일치한다. 기억할 점은 유사한 패턴인 (?!foo)bar는 "foo"로 시작되지 않는 "bar" 항목을 찾지 않는다는 것이다. 다음 3개의 문자가 "bar"일 때 어설션 (?!foo)은 항상 TRUE 이므로 모든 항목을 찾는다. 아래의 예시를 참고하자.

<?php
$string = "fooXbar";
echo preg_match('/foo(?!bar)/', $string) . "\n";
// 1

$string = "foobar";
echo preg_match('/(?!foo)bar/', $string) . "\n";
// 1

원하는 결과를 얻으려면 look-behind 어설션이 필요하다.

look-behind 어설션은 (?<= 긍정 어설션, (?<!은 부정 어설션을 나타낸다. 예를 들어, (?<!foo)bar는 "foo"가 앞에 붙지 않는 "bar"의 항목을 찾는다. look-behind 어설션은 일치하는 모든 문자열이 고정 길이가 되도록 제한되지만, 여러 가지 선택이 있는 경우 모든 선택이 동일한 고정 길이를 가져야 하는 것은 아니다. 따라서 (?<=bullock|donkey)는 허용된다. 그러나 (?<!dogs?|cats?)는 컴파일 타임에 오류가 발생한다. 다른 길이의 문자열에 일치하는 분기는 look-behind 어설션의 최상위 수준에서만 허용된다. 이것은 모든 분기에서 동일한 길이를 요구하는 Perl 5.005와 비교되는 확장이다. (?<=ab(c|de))와 같은 어설션은 허용되지 않는다. 왜냐하면 단일 최상위 분기가 두 개의 다른 길이를 일치시킬 수 있기 때문이다. 그러나 두 개의 최상위 레벨 분기를 사용하도록 (?<=abc|abde)로 다시 작성하면 허용된다. look-behind 어설션의 구현은 각 선택에 따라 현재 위치를 고정폭 만큼 임시로 이동시킨 다음 일치시키려고 시도한다. 현재 위치 앞에 문자가 충분하지 않으면 일치가 실패한 것으로 간주된다. 한 번만 사용할 수 있는 서브 패턴과 관련된 look-behind는 특히 문자열 끝에 매칭하는데 유용 할 수 있다. once-only subpatterns에 대한 섹션에 예제가 있다.

여러 어설션이 연속해서 발생할 수 있다. 예를 들어, (?<=\d{3})(?<!999)foo는 "foo" 앞에 "999"가 아닌 세 자리를 찾는다. 각 어설션은 주제 문자열의 동일한 지점에서 독립적으로 적용된다. 먼저 앞의 세 문자가 모두 숫자임을 확인한 다음 동일한 세 문자가 "999"가 아닌지 확인한다. 이 패턴은 "foo"와 6 문자 앞의 문자는 일치하지 않는다. 첫 번째 문자는 숫자이고 마지막 세 문자는 "999"가 아니다. 예를 들어, "123abcfoo"와 일치하지 않는다((?<=\d{3}...)(?<!999)foo를 사용하면 일치한다).

<?php
$string = "123abcfoo";
echo preg_match('/(?<=\d{3})(?<!999)foo/', $string) . "\n";
// 0

echo preg_match('/(?<=\d{3}...)(?<!999)foo/', $string) . "\n";
// 1

이번에는 첫 번째 어설션은 앞의 여섯개 문자를 살펴보고 첫 번째 세 개가 숫자 임을 확인한 다음 두 번째 어설션은 앞의 세 문자가 "999"가 아님을 확인한다.

어설션은 임의의 조합으로 중첩 될 수 있다. 예를 들어 (?<=(?<!foo)bar)baz는 앞에 "foo"가 없는 "bar"가 앞에 오는 "baz"의 발생과 일치하지만 (?<=\d{3}...(?<!999))foo는 "foo"앞에 3 자리 숫자와 "999"가 아닌 3 자와 일치하는 다른 패턴이다.

어설션 서브 패턴은 서브 패턴을 캡쳐하지 않으며 반복되지 않을 수 있다. 동일한 것을 여러 번 선언하는 것은 의미가 없기 때문이다. 임의의 종류의 어설션에 서브 패턴 캡처가 포함되어 있으면 이러한 패턴은 전체 패턴에서 캡처하는 서브 패턴에 번호를 매기는 목적으로 계산된다. 그러나 부분 문자열 캡처는 긍정적인 어설션에 대해서만 수행된다. 왜냐하면 부정적인 어설션에 대해서는 의미가 없기 때문이다.

어설션은 최대 200개의 괄호로 묶은 서브 패턴으로 계산된다.


comments powered by Disqus