profile

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

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

PHP 정규식(PCRE)의 모든 것 - Escape sequences

유영재

Escape sequences

백 슬래시 문자는 여러 용도로 사용된다.

첫 번째, 영숫자가 아닌 문자가 나오면 문자가 가질 수 있는 특별한 의미가 없어진다.

이스케이프 문자로 백 슬래시를 사용하면 캐릭터 클래스의 내부 및 외부에 모두 적용된다.

예를 들어, "*" 문자를 일치시키려면 패턴에 "\*"을 쓴다. 이것은 \ 다음 문자가 메타 문자로 해석될지 여부와 관계없이 적용되므로 영숫자가 아닌 문자 앞에 "\"를 붙이면 문자 자체를 의미하는 것으로 항상 안전하다. 단, 백 슬래시와 일치시키려면 "\\"이라고 써야 한다.

' 또는 " 따옴표가 붙은 PHP 문자열은 백 슬래시가 특별한 의미를 가진다. 따라서 \가 일반 표현식 \\과 일치해야 한다면 "\\\\" 또는 '\\\\'가 PHP 코드에서 사용 되어야 한다.

패턴이 PCRE_EXTENDED 옵션으로 컴파일 된 경우 패턴의 공백(캐릭터 클래스 제외)과 캐릭터 클래스 외부의 "#" 사이의 문자와 다음 줄바꿈 문자는 무시된다. 백 슬래시를 사용하여 공백이나 "#" 문자를 패턴의 일부로 포함 할 수 있다.

두 번째, 백 슬래시는 시각적으로 패턴의 비인쇄 문자를 인코딩하는 방법을 제공한다.

패턴 종료를 의미하는 바이너리 제로(0)를 제외하고 비인쇄 문자의 모양에는 제한이 없지만 텍스트 편집으로 패턴을 작성하는 경우 일반적으로 이진 문자 보다 다음 이스케이프 시퀀스 중 하나를 사용하는 것이 더 쉽다.

다음은 그 예시다.

문자 설명
\a 알람, 즉 BEL 문자 (hex 07)
\cx "control-x", 여기서 x는 임의의 문자이다.
\e escape (hex 1B)
\f formfeed (hex 0C)
\n newline (hex 0A)
\p{xx} xx 속성을 가진 문자, 자세한 정보는 유니코드 속성을 참조
\P{xx} xx 속성이 없는 문자, 자세한 정보는 유니코드 속성을 참조
\r carriage return (hex 0D)
\t tab (hex 09)
\xhh 16진 코드 hh, 유니코드 문자 속성 참고
\ddd 8진 코드 ddd 또는 역 참조가 있는 문자

\cx : "control-x"를 나타낸다(ctrl키 + x). 변환과정은 다음과 같다. "x"가 소문자 인 경우 대문자로 변환된다. 그런 다음 문자의 6번 비트(16진수 40)가 반전된다. 따라서 "\cz"는 16진수 1A가 되고, "\c{"는 16진수 3B, "\c;"는 16진수 7B가 된다.
변환 과정은 아래의 코드를 보면 더 빠르게 이해가 될 것이다.

<?php
$tmp = ['z' => '1a', '{' => '3b', ';' => '7b'];
foreach ($tmp as $from => $expect) {
    $fromBit = sprintf('%07s', decbin(ord(strtoupper($from))));
    echo $fromBit . ' ' . "\n";

    $toBit = (!substr($fromBit, -7, 1) ? '1' : '0') . substr($fromBit, -6);

    echo $toBit . "\n";
    if (strcmp(dechex(bindec($toBit)), $expect) != 0) {
        throw new \Exception('변환 오류');
    }
    echo dechex(bindec($toBit)) . ' <- ' . $expect . "\n";
    echo $from . ' -> ' . chr(bindec($toBit)) . "\n";
    echo "\n";
}

// 1011010
// 0011010
// 1a <- 1a
// z -> 
//
// 1111011
// 0111011
// 3b <- 3b
// { -> ;
//
// 0111011
// 1111011
// 7b <- 7b
// ; -> {

\x : "\x" 뒤에 나오는 최대 두 개의 16진수를 읽는다(문자는 대문자 또는 소문자로 입력 할 수 있음). UTF-8 모드에서는 "\x{...}"가 허용되며 중괄호의 내용은 16진수 문자열이다. 주어진 16진수 코드 넘버는 UTF-8 문자로 해석된다. 원래 16진수 이스케이프 시퀀스인 \xhh가 127보다 큰 경우에는 2바이트 UTF-8 문자로 취급한다.

\0 : "\0" 뒤에 나오는 최대 두 개의 8진수를 읽는다. 두 자릿수 미만이면 존재하는 숫자만 사용된다. 따라서 시퀀스 "\0\x\07"은 BEL 문자(코드 값 7)가 뒤 따르는 두 개의 2진 0을 의미한다. 뒤에 나오는 문자 그 자체가 8진수 이면 처음 0 뒤에 두 자리를 입력해야 한다.

<?php
$str = 'test-' . chr(0) . chr(0) . chr(7);
echo ((preg_match('/\0\x\07/', $str)) ? 'matched' : 'not matched') . "\n";
// matched

$str = 'test-' . chr(0) . chr(7);
echo ((preg_match('/\0\x\07/', $str)) ? 'matched' : 'not matched') . "\n";
// not matched

백 슬래시 뒤에 0이 아닌 숫자가 오는 경우 처리가 복잡하다. PCRE는 캐릭터 클래스 외부에서 10진수로 그 이후의 모든 자릿수를 읽는다. 숫자가 10보다 작거나 표현식에서 이전에 캡처한 왼쪽 괄호가 많으면 전체 시퀀스가 ​​역 참조로 간주된다. 이것이 어떻게 작동하는지에 대한 설명은 괄호로 묶인 서브 패턴에 대한 설명 이후에 다시 한다.

캐릭터 클래스 안이나, 10진수가 9보다 크고 그보다 많은 패턴을 포착되지 않으면, PCRE는 백 슬래시 다음에 최대 세 개의 8진수를 다시 읽으며, 해당하는 8비트 값으로 하나의 바이트를 생성하고 이후의 숫자는 그 자체를 나타낸다. 다음의 예시를 살펴보자 :

문자 설명
\040 space(공백)의 다른 표현
\40 캡처 서브 패턴이 40개 미만인 경우 \040와 동일
\7 항상 역 참조(back reference)를 의미
\11 역 참조(back reference) 이거나 tab(탭)의 다른 표현
\011 tab(탭)의 다른 표현
\0113 탭 문자 뒤에 "3"
\113 8진수 113에 해당하는 코드 문자 (99개의 역 참조가있을 수 없으므로)
\377 완전히 1 비트로 구성된 바이트
\81 역 참조(back reference) 이거나 "8"과 "1"이 뒤 따르는 두 개의 2진 0
<?php
echo ((preg_match('/\0113/', chr(octdec(11)) . '3')) ? 'matched' : 'not matched') . "\n";
// matched

echo ((preg_match('/\113/', chr(octdec(113)))) ? 'matched' : 'not matched') . "\n";
// matched

echo ((preg_match('/\377/', chr(octdec(377)))) ? 'matched' : 'not matched') . "\n";
// matched

echo ((preg_match('/\81/', chr(0) . chr(0) . '81')) ? 'matched' : 'not matched') . "\n";
// matched

세 개 이상의 8진수는 읽히지 않으므로 100 이상의 8진수 값은 선행 0과 함께 사용하면 안된다.

단일 바이트 값을 정의하는 모든 시퀀스는 캐릭터 클래스의 내부 및 외부 모두에서 사용할 수 있다. 또한 캐릭터 클래스 내에서 시퀀스 "\b"는 백 스페이스 문자 (16진수 08)로 해석된다. 캐릭터 클래스 외부에는 다른 의미가 있다 (아래 참조).

<?php
echo ((preg_match('/[\b0-9]{2}/', chr(8) . '3')) ? 'matched' : 'not matched') . "\n";
// matched

세 번째, 일반 문자 유형을 지정하는 것이다.

문자 설명
\d 임의의 10진수
\D 10진수가 아닌 문자
\h 모든 수평 공백 문자 (PHP 5.2.4 이후)
\H 수평 공백 문자가 아닌 모든 문자 (PHP 5.2.4 이후)
\s 공백 문자
\S 공백이 아닌 모든 문자
\v 모든 수직 공백 문자 (PHP 5.2.4 이후)
\V 수직 공백 문자가 아닌 모든 문자 (PHP 5.2.4 이후)
\w 모든 "word" 문자
\W 모든 "non-word" 문자

각 이스케이프 시퀀스 조합은 완전한 문자 집합을 두 개의 분리된 집합으로 분할한다. 주어진 문자는 각 집합 중 하나에만 일치한다.

"word" 문자는 임의의 문자 또는 숫자 또는 밑줄 문자(_), 즉 Perl "word"의 일부가 될 수 있는 문자이다. 문자 및 숫자의 정의는 PCRE(Perl Compatible Regular Expressions)의 문자 테이블(PCRE의 "pcre_maketables.c" 소스 참조)에 의해 제어되며 locale 특정 일치가 발생하는 경우 달라질 수 있다. 예를 들어 "fr"(프랑스어) 로켈에서 128보다 큰 일부 문자 코드는 악센트 부호가 있는 문자에 사용되며 이 문자는 \w와 일치한다.

이러한 문자 유형 시퀀스는 캐릭터 클래스의 안과 밖 모두에서 나타날 수 있다. 그것들은 각각 적절한 유형의 한 문자와 일치한다. 현재 일치하는 지점이 제목 문자열의 끝에 있으면 해당 문자가 모두 일치하지 않으므로 모두 일치하지 않는다.

네 번째, 단순한 특정 어설션을 위한 것이다.

어설션은 대상 문자열의 문자를 사용하지 않고 일치하는 특정 지점에서 충족되어야 하는 조건을 지정한다. 보다 복잡한 어설션을 위한 서브 패턴의 사용은 아래에 설명되어 있다.

문자 설명
\b 단어(word) 경계
\B 단어(not a word) 경계가 아님
\A 목표 문자열 시작 (다중 행 모드와 무관)
\Z 목표 문자열 끝 또는 마지막 줄바꿈 (다중 행 모드와 무관)
\z 목표 문자열 끝 (다중 행 모드와 무관)
\G 주제어에서 첫 번째로 일치하는 위치

이러한 어설션캐릭터 클래스 안에서 사용할 수 없다. (단, "\b"는 캐릭터 클래스 내에서 백 스페이스 문자의 의미를 가진다).

"word" 경계는 현재 문자와 이전 문자가 모두 \w 또는 \W와 일치하지 않는 대상 문자열에서의 위치이다(즉, 하나의 글자가 \w와 일치하고 다른 글자가 \W와 일치 경우). 또는, 시작 또는 마지막 문자가 각각 \w와 일치하면 문자열의 시작 또는 끝이 경계가 된다.

<?php
echo ((preg_match('/(testable|string)/', 'AtestableA')) ? 'matched' : 'not matched') . "\n";
// matched

echo ((preg_match('/\b(testable|string)\b/', 'AtestableA')) ? 'matched' : 'not matched') . "\n";
// not matched

echo ((preg_match('/\b(testable|string)\b/', 'A testable A')) ? 'matched' : 'not matched') . "\n";
// matched

echo ((preg_match('/\B(testable|string)\B/', 'AtestableA')) ? 'matched' : 'not matched') . "\n";
// matched

echo ((preg_match('/\B(testable|string)\B/', 'A testable A')) ? 'matched' : 'not matched') . "\n";
// not matched

\A, \Z\z 어설션은 기존 곡절 부호(^) 및 달러 부호($)와 달리 옵션이 설정 되더라도 대상 문자열의 시작과 끝 부분에서만 일치한다. 이들은 PCRE_MULTILINE 또는 PCRE_DOLLAR_ENDONLY 옵션의 영향을 받지 않는다. \Z\z의 차이점은 \Z는 문자열의 마지막 문자 뿐만 아니라 문자열 끝에 있는 개행 문자 앞도 일치하지만 \z는 문자열 끝에만 일치한다는 것이다.

<?php
$str = <<<STR
A testable string E
B testable string F
C testable string G
D testable string H
STR;

preg_match_all('/\AB /m', $str, $matches);
print_r($matches);
//Array
//(
//  [0] => Array
//  (
//  )
//)

preg_match_all('/^B /m', $str, $matches);
print_r($matches);
//Array
//(
//  [0] => Array
//  (
//      [0] => B
//  )
//)

preg_match_all('/H\z/m', $str, $matches);
print_r($matches);
//Array
//(
//  [0] => Array
//  (
//  )
//)

preg_match_all('/G$/m', $str, $matches);
print_r($matches);
//Array
//(
//  [0] => Array
//  (
//      [0] => G
//  )
//)

$str .= "\n";   // \Z 와 \z의 차이를 확인하기 위해 개행문자 추가

preg_match_all('/H\z/m', $str, $matches);
print_r($matches);
//Array
//(
//  [0] => Array
//  (
//  )
//)

preg_match_all('/H\Z/m', $str, $matches);
print_r($matches);
//Array
//(
//  [0] => Array
//  (
//      [0] => H
//  )
//)

\G 어설션은 현재 일치하는 위치가 preg_match()의 offset 인수에 지정된 일치 항목의 시작 지점에 있는 경우에만 일치된다. offset 값이 0이 아닌 경우 \A와 다르다(Perl에서의 \G와 다르다).

<?php
preg_match('/\G1234/', 'test1234', $matches, 0, 3);
print_r($matches);
// offset 위치의 시작이 "t"이므로 일치되지 않음
//Array
//(
//)

preg_match('/\G1234/', 'test1234', $matches, 0, 4);
print_r($matches);
//Array
//(
//  [0] => 1234
//)

\Q\E는 패턴에서 regexp 메타 문자를 무시하는데 사용될 수 있다. 예를 들어 \w+\Q.$.\E$는 하나 이상의 단어 문자와 일치하며 리터럴 .$.이 따라 오며 문자열 끝에 고정된다. 간단히 말해 \Q로 시작해서 \E로 끝나는 내부의 글자는 리터럴로 인지한다는 것이다.

<?php
$str = 'asdf abc.$.';
preg_match('/\w+\Q.$.\E$/', $str, $matches);
print_r($matches);

// Array
// (
//     [0] => abc.$.
// )

$str = 'asdf abc.$x';
preg_match('/\w+\Q.$.\E$/', $str, $matches);
print_r($matches);

// Array
// (
// )

\K는 PHP 5.2.4부터 일치 시작을 초기화하는데 사용될 수 있다. 예를 들어, foo\Kbar 패턴은 "foobar"와 일치하지만 "bar"와도 일치한다고 보고한다. \K를 사용하면 캡처된 부분 문자열의 설정을 방해하지 않는다. 예를 들어, 패턴 (foo)\Kbar가 "foobar"와 일치하면 첫 번째 하위 문자열은 여전히 ​​"foo"로 설정된다.

<?php
$str = 'foobar';
preg_match('/foobar/', $str, $matches);
print_r($matches);

//Array
//(
//  [0] => foobar
//)

preg_match('/(foo)bar/', $str, $matches);
print_r($matches);

//Array
//(
//  [0] => foobar
//  [1] => foo
//)

preg_match('/foo\Kbar/', $str, $matches);
print_r($matches);

//Array
//(
//  [0] => bar
//)

preg_match('/(foo)\Kbar/', $str, $matches);
print_r($matches);

//Array
//(
//  [0] => bar
//  [1] => foo
//)

comments powered by Disqus