♥️7분 빠른 소식 전달해 드립니다♥️

[스크립트] 정규표현식 알아보기(2) 본문

IT

[스크립트] 정규표현식 알아보기(2)

핫한연예뉴스 2019. 8. 2. 12:45

정규식을 제어할 수 있는 12개의 구두 문자를 일명 메타문자라고 한다. 정규식이 메타문자를 글자 그대로 대조하게 하려면 각 메타문자 앞에 백슬래시를 넣어 메타문자들을 이스케이프 처리해야 한다.

 

$()*+.?[\^{|

나열한 메타문자에 종료 대괄호 ], 하이픈 -, 종료 중괄호 }는 빠져있다. \와 -은 이스케이프 처리되지 않은 [ 뒤에 올경우에만 메타문자로 인식되며 }는 이스케이프 처리되지 않은 { 뒤에 올경우에만 메타문자로 인식된다. 종료 과료 ), }, ]를 이스케이프 처리할 일은 전혀 없으므로 이것들은 메타문자로 분류되지 않는다. [와 ] 사이에 있는 블록에 대한 메타문자 규칙은 별도로 존재한다.

 

영어/숫자 이외의 모든 문자는 이스케이프 처리를 해도 정규식 동작이 달라지지 않는다. 반면에, 영어 문자나 숫자 문자를 이스케이프 처리하면 특별한 의미가 부여되거나 문법 에러가 발생한다. 하지만, 불필요한 백슬래시 남용으로 인해 정규식은 알아보기 힘들어진다.

 


Perl, PCRE, Java는 정규식 토큰 \Q와 \E를 지원한다. \Q는 백슬래시를 비롯해 \E 까지의 모든 메타문자의 의미를 숨긴다.

 

정규식은 기본적으로 대소문자를 구분하게 돼 있다. <regex>를 사용하면 Regex, REGEX, ReGeX 등이 아닌 regex가 일치된다. <regex>를 사용해서 전부 일치시키려면 대소문자 구분하지 않음 옵션을 활성화해야 한다. 모든 프로그래밍 언어에는 플래그나 속성이 존재하는데, 그걸 이용해서 정규식이 대소문자를 구분하지 않게 지정할 수 있다. 

 

정규식 외부에서 대소문자 구분하지 않음 옵션을 활성화할 수 없는 상황에서는 <(?i)regex>와 같이 모드 변경자 <(?i)>를 사용하여 정규식 내부에서 대소문자 구분하지 않음 옵션을 활성화하면된다. <(?i)>는 정규식의 이후 부분에 대소문자 구분하지 않음 옵션을 활성화하면, <(?-i)>는 정규식의 이후 부분에 대소문자 구분하지 않음 옵션을 비활성화한다. 마치 토글 스위치처럼 작동한다.

 

(?i)[A-F0-9]
(?i)[^A-F0-9]

외부 플래그를 통해 지정되든 정규식 내부에 모드 변경자를 통해 지정되든 간에, 대소문자 구분하지 않음 설정도 문자 클래스에 영향을 미친다. JavaScript도 같은 규칙을 따르지만, <(?i)>를 지원하지 않는다. JavaScript에서 정규식이 대소문자를 구분하지 않게 하려면 정규식을 작성할 때 /i 플래그를 지정하면 된다.

 


가장 흔히 사용되는 7개의 아스키 제어 문자는 이스케이프 시퀀스로 예약돼 있으므로, 이 문자들 앞에는 백슬래시가 붙는다. 여기엔 여러 프로그래밍 언어에서 문자열 리터럴에 사용되는 문법이 그대로 적용된다. 일반적인 비인쇄 문자와 그 문자들의 표기법은 다음과 같다.

 

표기 의미 16진 표기
\a 0x07
\e 이스케이프 0x1B
\f 폼 피드 0x0C
\n 라인피드 (개행) 0x0A
\r 캐리지 리턴 0x0D
\t 가로 탭 0x09
\v 세로 탭  

escape sequence란 컴퓨터와 주변장치의 상태 전환에 사용되는 문자로, 장치 제어라는 의미를 담아 컨트롤 시퀀스라고도 한다. 이스케이프 시퀀스는 이스케이프 문자를 사용하여 그 뒤에 나오는 문자들의 의미를 변경한다. 그러면 그 문자들은 데이터가 아니라 실행될 명령으로 해석된다. 이스케이프 문자는 대개 키보드의 Esc 키에 할당돼 있다.

 


c[ae]l[ae]nd[ae]r

[] 대괄호로 묶은 부분을 문자 클래스라고 한다. 문자 클래스는 안에 나열된 문자들 중 한 문자와 일치된다. 첫번째 정규식에 들어있는 세 개의 클래스는 a나 e 중 하나와 일치된다. 각 클래스는 서로 독립적이다. 이 정규식을 대상 문자열 calendar에 적용하면 첫 문자 클래스는 a와 두번째 문자 클래스는 e와 세번째 문자 클래스는 a와 일치된다.

 

문자 클래스 외부에서는 12개의 메타문자($()*+.?[\^{|)가 존재하지만, 문자 클래스 내부에서는 오직 4개의 문자 \, ^, -, ]만 특수 기능이 있다. Java나 .NET 스타일에서는 문자 클래스 내부에서 시작 대괄호 [도 메타문자로 인식되며, 그 외의 문자들은 문자 클래스 안에서 순수한 리터럴 문자로 인식된다. 정규식 <[$()*+.?{|]>는 대괄호 안의 9개 문자 중 하나와 일치된다.

 

백슬래시는 문자 클래스 외부에서와 마찬가지로 항상 그 뒤에 나오는 문자를 이스케이프 처리한다. 이스케이프 처리된 문자는 1개의 문자일 수도 있고 범위의 시작이나 끝일 수도 있다. 나머지 4개의 메타문자는 특정 위치에 있을때에만 각각의 특수 의미를 얻게 된다. 특수 의미가 부여되지 않게끔 배치하면, 그 문자들을 이스케이프 처리하지 않고도 문자 클래스 안에 리터럴 문자처럼 포함시킬수 있다.

 

표준을 엄격히 따르는 JavaScript 스타일을 사용하지만 않는다면 <[][^-]>을 가지고 편법을 사용할 수 있다. 그러나 <[\]\[\^\-]>와 같이 이 메타문자들을 언제나 이스케이프 처리하길 권한다. 메타문자를 이스케이프 처리하면 정규식을 알아보기가 쉬워진다.

 

영수 문자는 백슬래시로 이스케이프 처리할 수 없다. 그럴 경우, 에러가 발생하거나 정규식 토큰(정규식 내에서 특수 의미를 갖는 문자)이 생성된다. 영수 문자는 문자 클래스 내부에 사용할 수 있다. 이러한 모든 토큰은 백슬래시와 단음문자로 구성되며, 경우에 따라 뒤에 다른 문자들이 붙기도 한다. <[\r\n]>는 캐리지리턴이나 라인피드와 일치된다.

 

캐릿(^)이 시작 괄호 바로 뒤에 있으면 문자 클래스가 부정된다. 부정 문자 클래스는 그 클래스 안의 문자들 이외의 문자와 일치된다. 부정 문자 클래스에 개행문자를 넣지 않으면, 부정 문자 클래스는 개행문자와 일치된다. 

 

하이픈(-)은 두 문자 사이에 있을땐 범위를 형성한다. 범위는 하이픈의 앞의 문자, 하이픈 뒤의 문자, 그 둘 사이에 번호 순으로 배치된 모든 문자가 들어있는 문자 클래스로 구성된다. 범위는 두 숫자 사이에나 둘다 대문자나 소문자인 두 문자 사이에만 작성하는 것이 바람직 하다.

 


백슬래시와 한 개의 단음문자로 이뤄진 6개의 정규식 토큰은 약기 문자 클래스들을 형성한다. 이 백슬래시와 단음문자는 둘 다 문자 클래스 내부와 외부에 사용할 수 있다. <\d>와 <[\d]>는 둘 다 한자리 숫자와 일치된다. 각 약기 소문자에는 짝을 이루는 약기 대문자가 존재하는데, 이것은 각기 소문자와 정반대 의미를 갖는다. <\D>는 순자가 아닌 모든 문자와 일치되며 <[^\d]>와 결과가 같다.

 

<\w>는 단어 문자 1개와 일치된다. 단어 문자란 단어에 들어있을 수 있는 문자를 말한다. 단어 문자는 단음문자, 숫자, 언더바로 구성된다. 단음문자, 숫자, 언더바들이 선정된 이유는 일반적으로 프로그래밍 언어에서 식별자 안에 넣을수 있기 때문이다. <\W>는 단어 문자를 제외한 모든 문자와 일치된다. Java, JavaScript, PCRE, Ruby에서 <\w>는 <[a-z-A-Z0-9_]>와 언제나 결과가 같다. 

 

<\s>는 모든 공백 문자와 일치된다. 공백 문자에는 빈칸, 탭, 개행문자가 포함된다. .NET, Perl, JavaScript에서 <\s>는 유니코드 표준에 공백 문자로 정의돼 있는 모든 문자와도 일치된다. JavaScript는 <\s>에 유니코드를 사용하지만 <\d>와 <\w>에는 아스키를 사용한다. <\S>는 <\s>와 일치되지 않는 모든 문자와 일치된다.

 

<\b>를 추가하면 비일관성이 한층 심해진다. <\b>는 약기 문자 클래스가 아니라 단어 경계다. <\w>가 유니코드를 지원하면 <\b>는 유니코드를 지원하고 <\w>가 아스키 전요이면 <\b>도 아스키 전용일거라 예상하겠지만, 언제나 그런 것은 아니다. 

 


(1) .NET 문자 클래스의 차집합

[a-zA-Z0-9-[g-zG-Z]]

위 정규식은 완곡한 방법으로 16진 문자 한 개와 일치된다. 외부 문자 클래스가 모든 영숫자 문자에 일치된 후, 내부 문자 클래스에 들어있는 g부터 z까지 범위의 단음문자가 제외된다. 이 내부 문자 클래스는 <[class-[subtract]]> 형태로 반드시 앞에 하이픈을 붙여서 외부 클래스 끝에 넣어야 한다.

 

문자 클래스의 차집합(subtraction)은 유니코드 속성, 구간, 스크립트를 사용해 작업할 때 특히 요긴하다. 예컨대 <\p{IsThai}>는 태국어 스크립트 범위의 모든 문자와 일치된다. <\p{N}>은 Number 속성이 없는 모든 문자와 일치된다. 이 둘을 차집합으로 연결한 <[\p{IsThai}-[\p{N}]]>은 태국숫자 10개 중 하나와 일치된다.

 

 

(2) Java 문자 클래스의 차집합

[a-f[A-F][0-9]]
[a-f[A-F[0-9]]]
[\w&&[a-fA-F0-9\s]]

Java 스타일에서는 한 문자 클래스를 다른 문자 클래스 안에 넣을 수 있다. 이렇게 내부 클래스를 일대일로 넣으면, 두 클래스의 합집합이 형성된다. 안에 넣을 수 있는 문자 클래스 수에는 제한이 없다. 위 정규식들은 모두 결과가 같다. 세번째 정규식은 가장 난해한 정규식이다. 외부문자 클래스는 모든 단어 문자와 일치된다. 결과 클래스는 둘의 교집합으로, 오직 16진수와 일치된다. 기본 클래스는 공백 문자와 일치되지 않고 하위 클래스는 <[g-zG-Z_]>와 일치되지 않기 때문에, 이것들은 최종 문자 클래스에서 제외되고 공통적으로 일치되는 16진수만 남게 된다.

 

[a-zA-Z0-9&&[^g-zG-Z]]

위의 정규식은 완곡한 방식으로 16진수 문자 한 개와 일치된다. 외부 문자 클래스가 모든 영수 문자와 일치된 이후, 내부 클래스로 인해 g부터 z까지의 단음문자가 제외된다. 이 내부 클래스는 <[class&&[^subtract]]>처럼 반드시 부정 문자 클래스여야 하며 앞에는 두개의 앰퍼샌드를 뭍여야 한다.

 

문자 클래스 교집합과 차집합은 유니코드 속성, 블록, 스크립트를 작업할 때 특히 자주 사용된다. <[\p{InThai}&&[\p{N}]]>은 10개의 태국 숫자 중 하나와 일치된다.

 


(1) 개행 문자를 제외한 모든 문자

마침표의 의미는 항상 모든 단일문자와 일치시킨다는 것이었다. 그러나 모든 문자란 게 정확히 무슨 의미인지 그 정의에 대해 혼란이 있다. 제일 오래된 정규식 툴들은 여러 파일을 한 행씩 행 단위로 처리했기 때문에, 애초에 대상 텍스트에 개행문자가 포함될 여지가 없었다. 일반적인 프로그래밍 언어들은 사용자가 집어넣은 개행문자의 수와 상관없이 대상 텍스트를 통째로 처리한다. 행단위 처리를 원할 경우, 대상 텍스트를 행 배열로 쪼갠 후 그 배열 내의 각 행에 정규식을 적용하는 코드를 작성해야 한다. 

 

JavaScript에는 '마침표는 개행문자와 일치' 옵션이 존재하지 않는다. <\s>는 모든 공백 문자와 일치된다. 반면에 <\S>는 <\s>와 일치되지 않는 모든 문자와 일치된다. 이 둘을 결합한 <[\s\S]>는 개행문자를 비롯한 모든 문자를 포함하는 문자 클래스가 된다. <[\d\D]>나 <\w\W>도 결과는 같다.

 

(2) 마침표 오용

마침표는 정규식 기능들 중 잘못 사용되는 빈도가 가장 높다. 날짜를 대조하는 방법으로 <\d\d.\d\d.\d\d>를 작성하는 것은 좋지 못하다. 99/99/99와도 일치되고, 12345678과도 일치된다. 유효한 날짜에만 일치되는 알맞은 정규식이 존재하긴 하지만, 마침표를 더 적절한 문자 클래스로 대체하기는 쉽다. <\d\d[/.\-]\d\d[/.\-]\d\d>라고 작성하면 슬래시, 마침표, 하이픈이 날짜 구분자로 사용된다. 물론 99/99/99가 일치되는 문제가 남아있기는 하다.

 

마침표는 모든 문자에 일치시킬 때만 사용하고, 그 외의 모든 상황에서는 문자 클래스나 부정 문자 클래스를 사용하는 것을 권장한다.

 

<(?s)>는 .NET, Java, PCRE, Perl, Python에서 '마침표는 개행문자와 일치' 모드로 변경해주는 모드 변경자다. s는 '싱글 라인' 모드를 나타낸다. 

 


정규식 토큰 ^, $, \A, \Z, \z를 일명 앵커라고 한다. 이 앵커들은 문자와 일치되는 것이 아니라, 사실상 정규식 패턴을 특정 위치에 고정시켜서 그 위치에 일치하도록 한다.

 

대상 텍스트 시작과 개행문자 사이, 두 개행문자 사이, 개행문자와 대상 텍스트 끝 사이 부분을 일명 행이라고 한다. 대상 텍스트에 개행문자가 없으면 전체 대상 텍스트가 한 행으로 간주된다. 

 

 

(1) 대상 텍스트 시작

앵커 <\A>는 항상 첫문자 앞에서 대상 텍스트 맨 처음을 대조한다. \A는 오로지 그 위치에 있는 문자만을 대조한다. 대상 텍스트가 원하는 패턴으로 시작되는지 검사하려면 \A를 정규식 맨 앞에 넣으면 된다. A는 반드시 대문자여야 한다.

 

JavaScript에서는 <\A>가 지원되지 않는다. 앵커 <^>는 '^와 $는 개행문자 위치에서 일치' 옵션을 설정하지 않으면 <\A>와 결과가 같다. JavaScript를 사용하지 않는 사람은 <^>을 사용하지 말고 <\A>를 사용할 것을 권한다. 왜냐하면 <\A>의 의미는 옵션과 상관없이 항상 같아서 정규식 옵션을 설정할 때 혼동하거나 실수할 일이 없기 때문이다.

 

(2) 대상 텍스트 끝

앵커 <\Z>와 <\z> 항상 마지막 문자 뒤에서 대상 텍스트의 맨 끝에 일치된다. 대상 텍스트가 원하는 패턴으로 끝나는지 검사하려면 정규식 끝에 <\Z>와 <\z>를 넣으면 된다.

 

.NET, Java, PCRE, Perl, Ruby에서는 <\Z>와 <\z>가 둘 다 지원되지만, Python에서는 <\Z>만 지원된다. JavaScript에서는 <\Z>와 <\z> 둘 다 지원되지 않는다.

 

<\Z>와 <\z>의 차이는 대상 텍스트의 마지막 문자가 개행문자일때 드러난다. <\Z>는 마지막 텍스트에서 개행문자를 포함하지 않고, <\z>는 개행문자를 포함한다.

 

JavaScript를 사용하지 않을땐 <$>를 사용하지 말고 항상 <\Z>를 사용하는 것이 바람직하다. <\Z>의 의미는 어떤 상황에서도 바뀌지 않으므로 정규식 옵션 설정에 있어서 혼동과 실수를 피할 수 있다.

 

(3) 행 시작

<^>는 <\A>와 마찬가지로 대상 텍스트의 시작에서만 일치된다. Ruby에서만 <^>이 항상 행의 맨 앞에서 일치된다. 나머지 모든 스타일에서는  '캐럿과 달러는 개행문자 위치에서 일치' 옵션을 설정해야 한다. 이 옵션을 일반적으로 '멀티라인(multiline)' 모드라고 부른다.

 

멀티라인 모드를 싱글라인 모드와 혼동하지 않는것이 좋다. '싱글 라인' 모드는 '마침표는 개행문자와 일치' 모드의 별칭이다. '멀티라인' 모드는 캐릿과 달러기호에만 영향을 주고, '싱글 라인' 모드는 마침표에만 영향을 준다. '싱글 라인' 모드와 '멀티라인' 모드를 종시에 설정하는 것도 가능하다. 두 옵션 모두 기본적으로 해제 상태이다. 

 

옵션들을 바르게 지정하면 <^>은 대상 텍스트 각 행의 시작 위치에서 일치된다. 엄밀히 말하면, <^>은 언제나 파일의 맨 첫문자 앞에서 대조하며 대상 텍스트의 각 개행문자 뒤에서 대조한다. <^>은 항상 <\n> 뒤에서 대조하므로 <\n^>에 들어있는 캐릿은 불필요하게 중복 사용된 것이다.

 

(4) 행 끝

기본적으로 <$>는 <\Z>와 마찬가지로 대상 텍스트의 끝이나 마지막 개행문자 앞에서만 일치된다. 그러나 유일하게 Ruby에서는 <$>가 항상 각 행의 끝에서 일치된다. 나머지 모든 스타일에서 캐릿과 달러가 개행문자 위치에서 일치되게 하려면 '멀티라인' 옵션을 설정해야 한다.

 

옵션 설정을 제대로 했다면 <$>는 대상 텍스트 각 행의 끝에서 일치된다. (각 행의 끝이기도 하므로 당연히 텍스트 맨 마지막 문자 뒤에서도 일치된다) <$>는 항상 <\n> 앞에서 일치되므로 <$\n>에 들어있는 달러는 불필요한 중복이다.

 

(5) 제로 길이 일치부 

정규식은 하나 이상의 앵커만으로도 구성될 수 있다. 그런 정규식은 각 앵커의 대조 가능 위치에 있는 제로 길이 일치부를 검색한다. 여러 앵커를 합치면 그 앵커들은 모두 정규식이 일치시킬 바로 그 위치를 대조해야 한다.

 

그러한 정규식을 검색치환에 사용할 수도 있다. 전체 대상 텍스트 앞/뒤에 어떤 문자를 덧붙이려면 <\A>나 <\Z>를 사용해서 치환하면 된다. '^과 $는 개행문자 위치에서 일치' 모드에서 대상 텍스트의 각 행 앞/뒤에 어떤 문자를 덧붙이려면 <^>이나 <$>를 사용해서 치환하면 된다.

 

빈 행이나 누락된 입력을 검사하려면 두 앵커를 결합하면 된다. <\A\Z>는 한 개의 라인피드가 들어있는 문자열이나 빈 문자열과 일치되지만, <\A\z>는 빈 문자열하고만 일치된다. '^과 $는 개행문자 위치에서 일치' 모드에서 <^$>는 대상 텍스트 안의 빈행 모두와 일치된다. 

 

 

정규식 외부에서 '^과 $는 개행문자 위치에서 일치' 모드를 설정할 수 없을때는 정규식 앞에 모드 변경자를 넣으면 된다.

 

<(?m)>은 .NET, Java, PCRE, Perl, Python의 '^과 $는 개행문자 위치에서 일치' 모드에서 사용가능한 모드 변경자다. m은 '멀티라인' 모드를 나타내는데, 유독 Perl에서만 '^과 $는 개행문자 위치에서 일치'라고 표시되어 혼란이 있다.

 

사용하는 스타일이 JavaScript가 아닐때는 정규식 안에 캐릿과 달러를 행에만 사용하는 것이 좋다. 정규식 앞에 <(?-m)>을 붙이지 않으려면 <\A>와 <\Z>를 사용해서 정규식을 파일 맨 처음이나 끝에 고정시켜야 한다.

 


(1) 단어 경계

정규식 토큰 <\b>를 일명 단어 경계라고 한다. <\b>는 단어의 맨앞이나 맨뒤에서 일치된다. <\b>는 단독적으로 제로 길이 일치부를 찾아낸다. <\b>는 앞 절에서 설명했던 토큰들과 마찬가지로 앵커다.

 

<\b>는 다음의 위치에서 일치된다.

  1. 첫문자가 단어 문자일 경우, 첫문자 앞에서
  2. 끝 문자가 단어 문자일 경우, 끝 문자 뒤에서
  3. 두 문자 중 하나만 단어 문자일 경우, 그 두 문자 사이에서

<\bx>와 <!\b>에 들어있는 <\b>는 단어의 시작 위치에서만 일치되며, <x\b>와 <\b!>에 들어있는 <\b>는 단어의 끝 위치에서만 일치된다. <x\bx>와 <!\b!>는 단어 앞이나 뒤 어디에서도 일치되지 않는다.

 

정규식을 사용해서 '완전한 단어만' 검색을 수행하려면 <\bcat\b>와 같이 해당 단어 앞뒤에 단어 경계를 넣으면 된다. 앞의 <\b>로 인해 문자열 맨 앞이나 비 단어 문자 뒤에 <c>가 있어야 일치되며, 뒤의 <\b>로 인해 문자열 맨 뒤나 비 단어 문자 앞에 <t>가 있어야 일치된다.

 

개행문자(캐리지리턴, 라인피드)는 비 단어 문자다. <\b>는 단어 문자가 개행문자 바로 뒤에 있을땐 그 개행문자 뒤에서 일치되고, 단어 문자가 개행문자 바로 앞에 있을땐 그 개행문자 앞에서 일치된다. <\b>는 '멀티라인' 모드나 <(?m)>에 영향을 받지 않는데, 이게 바로 앞에서 '멀티라인' 모드를 '^과 $는 개행문자 위치에서 일치' 모드와 같다고 설명했던 이유 중 하나다.

 

(2) 단어 비 경계

<\B>는 대상 텍스트에서 <\b>가 대조하지 않는 모든 위치에 일치된다.

  1. 첫 문자가 비 단어 문자일 경우, 첫 문자 앞에서
  2. 끝 문자가 비 단어 문자일 경우, 끝 문자 뒤에서
  3. 두 단어 문자 사이에서
  4. 두 비 단어 문자 사이에서
  5. 비 문자열에서

<\Bcat\B>는 staccato의 cat과는 일치되지만, My cat is brown, category, bobcat의 cat과는 일치되지 않는다. My cat is brown을 제외하고 staccato, category, bobcat이 검색되게 하고 싶으며, <\Bcat>과 <cat\B>를 <\Bcat|cat\B>로 합친 alternation 형태의 정규식을 사용해야 한다. 

 


단어 문자란 단어를 구성할 수 있는 문자를 뜻한다. 대부분의 스타일들은 <\b>와 <\B>를 지원하지만, 스타일마다 단어 문자로 인식되는 문자들이 서로 다르다.

 

Java 스타일에서는 <\w>는 아스키 문자에만 일치되는 반면, <\b>는 모든 스크립트에서 유니코드에도 일치된다. Javadptj <\b\w\b>는 모든 언어에서 비 단어 문자인 알파벳, 숫자, 언더바 중 한 문자와 일치된다. <\b>가 유니코드를 지원하므로 <\bkowka\b>는 cat을 뜻하는 러시아어 단어와 정확히 일치되지만, <\w>가 아스키만 지원하므로 <\w+>는 러시아어 단어와 일치되지 않는다.

 


유니코드 번호(code point)는 유니코드 문자 데이터베이스 내의 한 엔트리다. 문자는 생각하기에 따라 의미가 달라지지만 유니코드 번호는 그렇지 않다. 화면에 한 문자로 표시되는 내용을 일명 유니코드에서의 자소라고 한다. 유니코드 번호 U+2122는 정규식 스타일에 따라 <\u2122>나 <\x{2122}>를 사용해서 대조할 수 있다. <\u> 구문은 정확히 4자리 16진수만을 인식하므로 U+0000부터 U+FFFF까지의 유니코드 번호에만 <\u> 구문을 사용할 수 있다. 그에 반해 <\x> 구문은 자릿수에 상관없이 모든 16진수를 인식할수 있어 전체 유니코드 번호에 사용할 수 있다. U+00E0를 대조하려면 <\x{E0}>나 <\x{00E0}>을 사용하면 된다. 유니코드 번호는 문자 클래스 내부와 외부에 모두 사용할 수 있다.

 

<\P>는 <\p>의 부정이다. <\P{Ll}>은 Ll 속성이 들어있지 않은 하나의 유니코드 번호와 일치된다. <\P{L}>은 아무 'letter' 속성도 들어있지 않은 하나의 유니코드 번호와 일치된다. 

 

유니코드 문자 데이터베이스에는 모든 유니코드 번호가 블록으로 나뉘어 들어있다. 각 블록은 한 범위의 유니코드 번호들로 구성된다. 유니코드 블록은 연속적인 유니코드 번호들의 한 범위를 말한다. 많은 블록에 유니코드 스크립트 이름과 유니코드 카테고리의 이름이 있는데, 블록 이름은 단지 주된 용도를 나타낼 뿐이다.

 

미할당 번호를 제외하고, 한 개의 유니코드 스크립트에는 한 개의 유니코드 번호가 속한다. 미할당 유니코드 번호는 어느 스크립트에도 속하지 않는다.

 

유니코드 설계자는 기호와 기본 문자를 분리하는 유니코드 방식 이외에도 추가로 널리 사용되는 구 문자 세트들과 일대일 대응이 존재한다면 편리할 것으로 생각했다. 이런 이유로 특수 조합 문자들이 유력한 구 문자 세트들에는 존재하지 않는 것이다.

 


파이프 기호(|)는 정규식을 여러 개의 후보(선택적 일치 대상)로 분리하는 다자택일 연산자다. <Mary|Jane|Sue>는 각각 일치 시도를 통해 Mary나 Jane이나 Sue 중 하나와 일치된다. 대조할 때마다 한번에 한개의 이름만 일치되지만, 매번 다른 이름이 일치될 수 있다.

 

정규식 기준 엔진은 정규식이 장독되게끔 해주는 소프트웨어에 불과하다. 정규식 기준이란 대상 텍스트의 문자마다 정규식의 모든 후보를 대조한 후 대상 텍스트의 그 다음 문자를 대조하는 것을 뜻한다.

 

<Mary|Jane|Sue>를 Mary, Jane, and Sue went to Mary's house에 적용하면, 일치부가 Mary가 문자열의 맨 앞에서 즉시 발견된다. 문자열 뒷부분에 마저 적용하면(예: 텍스트 에디터의 '다음 찾기' 버튼의 기능) 이 정규식 기준 엔진은 문자열의 첫 콤마 위치에서 <Mary>의 일치를 시도하지만 실패한다. 그 다음 정규식은 같은 위치에서 <Jane>의 일치를 시도하지만 역시 실패한다. 그 다음 똑같이 콤마 위치에서 <Sue>의 일치를 시도하지만 마찬가지로 실패한다. 정규식 기준 엔진은 이 과정이 끝난 후에야 문자열의 다음 문자 위치로 간다. 첫 위치에서 시작하여 3개의 후보가 똑같이 일치에 실패한다.

 

이제 시작 위치가 J이므로 첫번재 후보 <Mary>는 일치에 실패한다. 그 뒤의 두번째 후보 <Jane>은 J에서 시작하여 일치가 시도되며 결국 Jane과 일치된다. 정규식 기준 엔진은 매칭에 성공한다.

 

정규식 안의 후보 순서는 중요치 한다. 정규식은 최좌측 일치부를 찾아낸다. 이 정규식은 대상 텍스트를 좌에서 우로 탐지하면서, 각 단계마다 정규식 안의 모든 후보를 대조하고, 후보들 중 어느 것이든 일치되면 그 일치 텍스트의 첫 위치에서 성공한다. 문자열의 나머지 뒷부분도 마저 검색하면 Sue가 발견된다. 네번째 검색을 수행하면 Mary가 한번 더 발견된다. 정규식 기준 엔진에게 다섯번째 검색을 수행하도록 명령하면, 3개의 후보 중 어느것도 나머지 부분인 house 문자열과 일치되지 않으므로 일치에 실패한다.

 

정규식에 들어있는 후보들의 순서가 중요한 경우는, 오직 두 개의 후보가 문자열의 같은 위치에서 일치될 가능성이 있을 때뿐이다. 정규식 <Jane|Janet>에는 텍스트 Her name is Janet 안의 같은 위치를 대조하는 두 개의 후보가 들어있다. <Jane>이 Her name is Janet에 들어있는 Janet이라는 단어와 일치된다는 사실은 부분적으로는 중요하다.

 

정규식 기준 엔진과 다른 텍스트 기준 엔진도 존재한다. 텍스트 기준 엔진은 대상 텍스트 안의 각 문자 위치를 한번만 대조하는 반면, 정규식 기준 엔진은 각 문자를 여러번 대조한다는 핵심적인 차이가 있다. 따라서 속도는 텍스트 기준 엔진이 훨씬 빠르지만 수학적 관점에서의 정규식만 지원한다는 단점이 있다. 그래서 화려한 기능의 Perl 스타일 정규식은 오로지 정규식 기준 엔진으로만 구현할 수 있다.

 

정규식 기준의 정규식 엔진은 <Jane|Janet>이 Her name is Janet의 Jane과 일치된다. 정규식 기준 엔진은 대상 텍스트를 좌에서 우로 탐지하면서 최좌측 일치부를 찾아낼 뿐 아니라, 정규식 안에 들어있는 후보들도 좌에서 우로 탐지한다. 정규식 기준 엔진은 일치되는 후보를 발견함과 동시에 정지한다.

 

<Jane|Janet>이 Her name is Janet의 J에 다다르면 첫번째 후보인 <Jane>이 일치된다. 두번째 후보는 시도되지 않는다. 엔진에게 제2의 일치부를 찾도록 명령해봤자 대상 텍스트 우측에는 t밖에 없으므로 그 t 위치에서 Jane이나 Janet 둘 다 일치되지 않는다.

 

Janet이 탐지되는 것을 Jane이 방해하지 않게 하는 방법은 두 가지다. 첫번째 방법은 <Janet|Jane>처럼 길이가 더 긴 후보를 앞에 배치하는 것이다. 두번째 방법은 일치시킬 대상의 정황을 명확하게 지정하는 것이다. 정규식은 단어를 처리하지 못해도 단어 경계를 처리할 수는 있다.

 

<\bJane\b|\bJanet\b>와 <\bJanet\b|\bJane\b> 중 어느 정규식을 사용하든 원하는 결과를 도출할 수 있다. 이로써 후보의 순서는 상관없게 된다.

 


일치를 완전한 단어로 한정해서 Mary나 Jane이나 Sue와 일치되게끔 정규식을 개선하는데, 각 후보가 아니라 정규식 전체를 한 쌍의 단어 경계로 감싸서 그룹화를 이용하자.

 

정규식에서 후보들 중 어느 것을 제외시키려면 그 후보들을 그룹화해야 한다. 그룹으로 묶으려면 괄호를 사용하면 된다. 괄호는 여느 대부분의 프로그래밍 언어에서와 마찬가지로 모든 정규식 연산자 중 우선순위가 제일 높다. 그렇게 괄호로 묶으면 <\b(Mary|Jane|Sue)\b>의 후보는 두 단어 경계사이의 Mary, Jane, Sue로 인식된다.

 

이 정규식 엔진이 대상 텍스트 Her name is Janet에 들어있는 Janter의 J와 만나면 첫번째 단어 경계가 일치된 후 그룹으로 들어간다. 그룹 안의 첫 후보인 <Mary>는 실패하고 두번재 후보인 <Jane>은 성공한 후 정규식 엔진은 그룹을 빠져나온다. 그러나 두번재 단어 경계 <\b>는 대상 텍스트 끝에 있는 e와 t 사이 위치에서 일치에 실패한다. J에서 시작한 전체적인 일치시도는 실패하게 된다.

 

한 쌍의 괄호는 단순 그룹이 아니라 캡처 그룹이다. Mary-Jane-Sue 정규식의 경우, 캡처 그룹은 전체 정규식 일치부와 같아서 그다지 쓸모가 없다. 캡처 그룹은 <\b(\d\d\d\d)-(\d\d)-(\d\d)\b>처럼 정규식의 일부분에 해당할 때만 쓸모가 있다.

 

이 정규식은 yyyy-mm-dd 형식의 날짜와 일치되며, 정규식 <\b\d\d\d\d-\d\d-\d\d\b>와 완전히 같다. 이 정규식은 후보나 반복을 전혀 사용하지 않기 때문에 괄호의 그룹화 기능은 필요하지 않다. 오직 캡처 기능이 중요한 것이다.

 

정규식 <\b(\d\d\d\d)-(\d\d)-(\d\d)\b>에서 캡처 그룹이 3개 들어있다. 그룹들은 좌에서 우로 세면서 차례대로 번호가 매겨진다. <(\d\d\d\d)>가 1번 그룹, <(\d\d)>가 2번 그룹, <(\d\d)>가 3번 그룹이다.

 

일치되는 과정에서, 이 정규식 엔진은 종료 괄호를 만나 그룹을 빠져나올때 캡처 그룹을 통해 일치된 텍스트 부분을 저장한다. 이 정규식이 2008-05-24와 일치되는 순간 2008은 첫번째 캡처그룹에 저장되고 05는 두번째 캡쳐그룹에, 24는 세번째 캡쳐 그룹에 저장된다.

 

캡처 텍스트 사용 방법

  1. 캡처된 텍스트를 같은 정규식 일치부 안에서 다시 일치시키는 방법
  2. 검색치환을 수행할때 캡처 텍스트를 치환 텍스트에 삽입하는 방법
  3. 정규식 일치부의 일부를 사용하는 애플리케이션 작성

 

캡처 그룹을 사용하는 대신에 다음과 같이 비캡처 그룹을 사용할 수도 있다.

\b(?:Mary|Jane|Sue)\b

비 캡처 그룹은 세 개의 문자 (?: 로 시작해서 )로 종료된다. 비 ㅐㅂ처 그룹의 그룹화 기능은 캡처 그룹과 똑같지만, 비 캡처 그룹은 아무것도 캡처하지 않는다. 캡처 그룹수를 알아내기 위해 캡처 그룹의 시작 괄호를 셀 때, 비 캡처 그룹의 괄호는 제외된다. 이것이 비캡처 그룹의 중요한 장점이다. 매겨진 캡처 그룹 참조들을 엉망으로 만들지 않고 기존 정규식에 비 캡처 그룹을 넣을 수 있다.

 

비 캡처 그룹의 또다른 장점은 성능이다. 재참조부를 특정 그룹에 사용하지 않거나 치환 텍스트에 그 재참조부를 다시 삽입하거나 그 일치부를 소스코드 안으로 가져오는 경우에는, 캡처 그룹이 불필요한 오버헤드를 삽입하는데 비 캡처 그룹을 사용하면 이 오버헤드를 제거할 수 있다. 복잡한 루프 안이나 방대한 데이터에 정규식을 사용하는 것이 아니라면 사실상 이 차이는 거의 체감하기는 어렵다.

 


\b\d\d(\d\d)-\1-\1\b

앞에서 이미 일치된 텍스트를 정규식 안에서 나중에 다시 대조하려면 그 앞 텍스트를 캡처해야 한다. 캡처하는 방법은 캡처 그룹을 이용하면 된다. 그런 다음 backreference를 사용하면 그 캡처된 텍스트를 정규식의 아무 곳에서나 대조할 수 있다. 백슬래시 뒤에 1~9 사이의 한자리 숫자가 붙은 처음 9개의 그룹을 참조할 수 있으며 10~99 사이의 그룹이라면 <\10>~<\99>를 사용하면 된다.

 

정규식 <\b\d\d(\d\d)-\1-\1\b>가 2008-08-08을 만나면 맨앞의 <\d\d>는 20과 일치된다. 그런 다음 정규식 엔진은 캡처 그룹으로 들어가면서 그 캡처 그룹과 만난 대상 텍스트 안의 위치를 기억해둔다.

 

캡처 그룹 안의 <\d\d>가 08과 일치된 후, 정규식 엔진은 캡처 그룹의 종료 괄호를 만난다. 이 지점에서 캡처 부분의 일치부 08이 캡처 그룹 1에 저장된다.

 

그 다음 토큰인 하이픈은, 글자 그대로 일치된 다음 재참조부를 만난다. 이 수낙ㄴ 정규식 엔진은 첫번째 캡처 그룹은 08의 내용을 검사한다. 정규식 엔진은 이 텍스트를 글자 그대로 대조한다. 만일 정규식 설정이 대소문자 구분하지 않음이라는 이 캡처 텍스트는 이런식으로 대조되어 재참조부가 일치에 성공한다. 끝으로, 단어 경계는 대상 텍스트의 끝과 일치되면서 전체 2008-08-08이 검색된다. 이때도 캡처 그룹에는 여전히 08이 저장돼 있다.

 

정규식 엔진은 처음부터 끝까지 진행하므로 캡처 괄호를 재참조부 앞에 넣어야 한다. 정규식 <\b\d\d\1-\1-(\d\d)\b>는 절대로 어느것과도 일치될 수 없다. 캡처 그룹 앞에서 만나는 재참조부는 아직 아무것도 캡처하지 않은 상태다. JavaScript를 사용할때만 제외하고는, 재참조부가 아직 일치 시도도 하지 않은 그룹을 참조한다면 그 재참조부는 항상 실패한다.

 

(1) 명명 캡처 그룹

\b(?<year>\d\d\d\d)-(?<month>\d\d)-(?<day>\d\d)\b
\b(?'year'\d\d\d\d)-(?'month'\d\d)-(?'day'\d\d)\b
\b(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d)\b

 

(2) 명명 재참조부

\b\d\d(?<magic>\d\d)-\k<magic>-\k<magic>\b
\b\d\d(?'magic'\d\d)-\k'magic'-\k'magic'\b
\b\d\d(?P<magic>\d\d)-(?P=magic)-(?P=magic)\b

yyyy-mm-dd 형식의 날짜와 일치되고 년, 월, 일을 각각 캡처하는 정규식을 작성하자. 각각의 값들을 가지고 일치를 처리하는 코드 안에서 작업을 쉽게할 목적으로, 캡처된 텍스트에 직관적인 이름 'year', 'month', 'day'를 지정하는 것도 가능하다.

 


정규식의 일부분을 특정 횟수로 반복하기

 

(1) 고정 반복

\b\d{100}\b

n이 양수일때 정량자 <{n}>은 앞의 정규식 토큰을 n회 반복한다. <\b\d{100}\b>에 들어있는 <\d{100}>은 100자리 문자열과 일치된다. 이것은 <\d>를 100개 작성하는 것과 같다.

 

<{1}>은 정량자가 없을때와 마찬가지로 앞의 토큰을 1회 반복한다. <ab{1}c>는 <abc>와 같다.

<{0}>은 앞의 토큰을 0회 반복하는데, 사실은 정규식에서 그 토큰이 삭제된다. <ab{0}c>는 <ac>와 같다.

 

(2) -1 가변 반복

\b[a-z0-9]{1,8}\b

가변적으로 반복시켜야 할땐 정량자 <{n,m}>을 사용한다. 여기서 n은 양수이고 m은 보다 큰 수다. <\b[a-f0-9]{1,8}\b>는 1~8자리 16진수와 일치된다. 가변 반복 위치부터 후보들이 일치되는 순서가 적용되기 시작한다.

 

(2) -2 무한 반복

n이 양수인 정량자 <{n,}>은 무한 반복에 사용된다. 사실상 무한 반복은 가변 반복에서 상한만 존재하지 않을 뿐이다. <\d{1,}>은 한자릿 수 이상과 일치되면 <\d+>와 같다. 정규식 토큰 뒤에 붙은 플러스 기호는 정량자가 아니며 '하나 이상'을 의미한다.

 

<\d{0,}>은 영 자릿수 이상과 일치되며 <\d*>와 같다. 애스터리스크는 항상 '영 이상'을 의미하며 <{0,}>과 애스터리스크는 무한 반복시킴과 동시에 앞 토큰을 옵션화한다.

 

(3) 토큰 옵션화

가변 반복을 사용하되 n을 0으로 지정하면, 결과적으로 정량자 앞의 토큰을 옵션화할 수 있다. <h{0,1}>은 <h>를 1회 대조하거나 대조하지 않는다. 만약 h가 존재하지 않으면 <h{0,1}>은 일치부의 길이가 0이 된다. <h{0,1}>만을 정규식으로 하면 대상 텍스트에 들어있는 h를 제외한 각 문자 앞에 제로 길이 일치부가 검색된다. 각 h는 한 개의 문자(h)가 일치된다.

 

<h?>는 <h{0,1}>과 같다. 유효하고 완전한 정규식 토큰 뒤에 붙은 물음표는 정량자가 아니며 '영 이상'을 의미한다.

 

(4) 그룹 반복시키기

\b\d*\.\d+(e\d+)?

그룹의 종료 괄호 뒤에 정량자를 넣으면 전체 그룹이 반복된다. <(?:abc){3}>은 <abcabcabc>와 같다.

 

정량자는 중첩이 가능하다. <(e\d+)?>는 한 자릿수 이상의 숫자가 뒤에 있는 e와 일치되거나 제로길이 일치부가 된다. 부동소수점 정규식에서 이것은 선택적인 멱지수다.

 

캡처 그룹은 반복이 가능하다. 캡처 그룹의 일치부는 정규식 엔진이 해당 그룹을 빠져나올 때마다 캡처되며, 이때 기존에 그 그룹에 일치됐던 모든 텍스트를 덮어쓰게 된다. <(\d\d){3}>은 두자릿수, 네자릿수, 여섯자리수 중 하나와 일치된다. 이 정규식이 123456과 일치된다고 가정할 때, 캡처 그룹은 56을 저장하게 된다. 왜냐하면 그룹의 최종 반복 시에 56이 저장되기 때문이다. 그룹에 일치된 나머지 두 일치부 12와 34는 덮어씌워져서 건질 수 없다.

 

<(\d\d){3}>이 캡처하는 텍스트는 <\d\d\d\d(\d\d)>와 같다. 캡처 그룹이 달랑 뒤의 두 자릿수만 캡처하는게 아니라 두자릿수, 네자릿수, 여섯자릿수를 전부 캡처하게 하려면, 캡처 그룹을 반복시키지 말고 <((?:\d\d){3})>처럼 캡처 그룹을 정량자를 묶어야 한다. 아니면 <((\d\d){3})>처럼 두 개의 캡처 그룹을 사용해도 됐을 것이다. 정규식 <((\d\d){3})>은 123456과 일치되는데, <\1>에는 123456이 저장되고 <\2>에는 56이 저장된다.




Comments