이 문서의 제안은 주로 정규식의 가독성에 중점을 둡니다. 개발 중에 이러한 습관을 개발하면 디자인과 표현 구조를 더 명확하게 고려하여 버그와 코드 유지 관리를 줄이는 데 도움이 됩니다. 이 코드의 관리자는 본인입니다. 실제 사용에서 정규 표현식을 사용하여 이러한 경험을 직접 살펴보고 주의를 기울일 수 있습니다.
정규식은 쓰기도 어렵고, 읽기도 어렵고, 유지 관리도 어렵습니다. 예상치 못한 텍스트가 일치하지 않거나 유효한 텍스트가 누락되는 경우가 많습니다. 이러한 문제는 정규식의 성능과 기능으로 인해 발생합니다. 각 메타 문자의 기능과 뉘앙스의 조합으로 인해 지적 트릭을 사용하지 않고는 코드를 해석하는 것이 불가능해집니다.
많은 도구에는 정규식을 쉽게 읽고 작성할 수 있는 기능이 포함되어 있지만 매우 비관용적이기도 합니다. 많은 프로그래머에게 정규 표현식을 작성하는 것은 마법 같은 예술입니다. 그들은 자신이 알고 있는 특성을 고수하고 절대적으로 낙관적인 태도를 가지고 있습니다. 이 기사에서 논의한 다섯 가지 습관을 기꺼이 채택한다면 시행착오를 견디는 정규식을 디자인할 수 있을 것입니다.
이 기사에서는 Perl, PHP 및 Python 언어를 코드 예제로 사용하지만 이 기사의 조언은 거의 모든 대체 표현식(regex) 구현에 적용됩니다.
1. 공백과 주석을 사용하십시오.
대부분의 프로그래머에게는 정규식 환경에서 공백과 들여쓰기를 사용하는 것이 문제가 되지 않습니다. 만약 그렇게 하지 않는다면 동료는 물론 일반 사람들에게도 비웃음을 받을 것입니다. 거의 모든 사람들이 코드를 한 줄에 압축하면 읽고, 쓰고, 유지 관리하기가 어렵다는 것을 알고 있습니다. 정규식의 차이점은 무엇입니까?
대부분의 대체 표현식 도구에는 확장된 공백 기능이 있어 프로그래머가 정규식을 여러 줄로 확장하고 각 줄 끝에 주석을 추가할 수 있습니다. 왜 소수의 프로그래머만이 이 기능을 활용합니까? Perl 6의 정규식은 기본적으로 공간 확장 패턴을 사용합니다. 기본적으로 언어가 공간을 확장하도록 두지 말고 직접 활용하세요.
확장된 공백에 대해 기억해야 할 한 가지 방법은 정규식 엔진에 확장된 공백을 무시하도록 지시하는 것입니다. 이렇게 하면 공백을 일치시켜야 하는 경우 명시적으로 지정해야 합니다.
Perl 언어에서는 정규식 끝에 x를 추가하면 "m/foo bar/"는 다음 형식이 됩니다.
m/
푸
술집
/x
PHP 언어에서는 정규식 끝에 x를 추가하므로 ""/foo bar/""는 다음 형식이 됩니다.
"/
푸
술집
/x"
Python 언어에서는 패턴 수정 매개변수 "re.VERBOSE"를 전달하여 다음과 같이 컴파일된 함수를 얻습니다.
패턴 = r'''
푸
술집
'''
regex = re.compile(pattern, re.VERBOSE)가
더 복잡한 정규식을 처리하는 경우 공백과 주석이 더 중요해집니다. 다음 정규식이 미국의 전화번호를 일치시키는 데 사용된다고 가정합니다.
(?d{3})? ?d{3}[-.]d{4}
이 정규식은 다음과 같은 전화번호와 일치합니다. "( 314)555-4000", 이 정규식이 "314-555-4000" 또는 "555-4000"과 일치한다고 생각하시나요? 대답은 둘 다 일치하지 않는다는 것입니다. 이러한 코드 줄을 작성하면 단점과 설계 결과 자체가 숨겨집니다. 전화 지역 코드가 필요하지만 정규식에는 지역 코드와 접두사 사이에 구분 기호가 없습니다.
이 코드 줄을 여러 줄로 나누고 주석을 추가하면 단점이 드러나고 수정하기가 더 쉬워집니다.
Perl 언어에서는 다음 형식이어야 합니다.
/
(? # 선택적 괄호
d{3} # 필수 전화 지역 번호
)? # 선택적 괄호
[-s.]? # 구분 기호는 대시, 공백 또는 마침표일 수 있습니다.
d{3} # 세 자리 접두사
[-.] # 또 다른 구분 기호
d{4} # 4자리 전화번호
/x
이제 다시 작성된 정규식에는 지역 코드 뒤에 선택적 구분 기호가 있으므로 "314-555-4000"과 일치해야 하지만 지역 코드는 여전히 필요합니다. 전화 지역 번호를 선택 사항으로 만들어야 하는 또 다른 프로그래머는 이제 그것이 선택 사항이 아니라는 것을 금방 알 수 있으며, 약간만 변경하면 문제를 해결할 수 있습니다.
2.
작성 테스트에는 세 가지 수준의 테스트가 있습니다. 각 수준은 코드에 안정성을 추가합니다. 먼저 어떤 코드를 일치시켜야 하는지, 불일치를 처리할 수 있는지 신중하게 생각해야 합니다. 둘째, 데이터 인스턴스를 사용하여 정규식을 테스트해야 합니다. 마지막으로 공식적으로 테스트 패널을 통과해야 합니다.
무엇을 일치시킬지 결정하는 것은 실제로 잘못된 결과 일치와 올바른 결과 누락 사이의 균형을 찾는 것입니다. 정규식이 너무 엄격하면 일부 올바른 일치 항목이 누락되고, 너무 느슨하면 잘못된 일치 항목이 생성됩니다. 정규 표현식이 실제 코드에 적용되면 두 가지를 모두 눈치채지 못할 수도 있습니다. "800-555-4000 = -5355"와 일치하는 위의 전화번호 예를 생각해 보세요. 잘못된 일치 항목은 실제로 감지하기 어렵기 때문에 미리 계획을 세우고 잘 테스트하는 것이 중요합니다.
전화번호 예를 계속해서 설명하면, 웹 양식에서 전화번호를 확인하는 경우 어떤 형식의 10자리 숫자로도 만족스러울 수 있습니다. 그러나 많은 양의 텍스트에서 전화번호를 분리하려면 요구 사항을 충족하지 않는 잘못된 일치 항목을 신중하게 제외해야 할 수도 있습니다.
일치시키려는 데이터에 대해 생각할 때 몇 가지 사례 시나리오를 적어보세요. 사례 시나리오에 대해 정규식을 테스트하는 코드를 작성하십시오. 복잡한 정규 표현식의 경우 다음과 같은 특정 형식을 취할 수 있는 작은 프로그램을 작성하여 테스트하는 것이 가장 좋습니다.
Perl 언어:
#!/usr/bin/perl
my @tests = ( "314-555-4000",
"800-555-4400",
"(314)555-4000",
"314.555.4000",
"555-4000",
"aasdklfjklas",
"1234-123-12345"
);
내 $test (@tests) {
if ( $test =~m/
(? # 선택적 괄호
d{3} # 필수 전화 지역 번호
)? # 선택적 괄호
[-s.]? # 구분 기호는 대시, 공백 또는 마침표일 수 있습니다.
d{3} # 세 자리 접두사
[-s.] # 또 다른 구분 기호
d{4} # 4자리 전화번호
/x ) {
"$testn에서 일치함"을 인쇄합니다.
}
또 다른 {
"$testn에서 일치 실패"를 인쇄합니다.
}
}
PHP 언어:
<?php
$tests = 배열("314-555-4000",
"800-555-4400",
"(314)555-4000",
"314.555.4000",
"555-4000",
"aasdklfjklas",
"1234-123-12345" );
$정규식 = "/
(? # 선택적 괄호
d{3} # 필수 전화 지역 번호
)? # 선택적 괄호
[-s.]? # 구분 기호는 대시, 공백 또는 마침표일 수 있습니다.
d{3} # 세 자리 접두사
[-s.] # 또 다른 구분 기호
d{4} # 4자리 전화번호
/x";
foreach ($test를 $test로) {
if (preg_match($regex, $test)) {
echo "$test에서 일치함
;";
}
또 다른 {
echo "$test에서 일치 실패
;";
}
}
?>;
Python 언어:
import
retest = ["314-555-4000",
"800-555-4400",
"(314)555-4000",
"314.555.4000",
"555-4000",
"aasdklfjklas",
"1234-123-12345"
]
패턴 = r'''
(? # 선택적 괄호
d{3} # 필수 전화 지역 번호
)? # 선택적 괄호
[-s.]? # 구분 기호는 대시, 공백 또는 마침표일 수 있습니다.
d{3} # 세 자리 접두사
[-s.] # 또 다른 구분 기호
d{4} # 4자리 전화번호
'''
regex = re.compile( 패턴, re.VERBOSE ):
regex.match(테스트)인 경우:
"Matched on"을 인쇄하고, 테스트하고, "n"
또 다른:
print "Failed match on", test, "n"
테스트 코드를 실행하면 또 다른 문제가 드러납니다: "1234-123-12345"와 일치합니다.
이론적으로는 전체 애플리케이션에 대한 모든 테스트를 테스트 팀으로 통합해야 합니다. 아직 테스트 그룹이 없더라도 정규식 테스트는 그룹을 위한 좋은 기초가 될 것이며 지금이 시작하기에 좋은 시기입니다. 아직 생성하기에 적절한 시기가 아니더라도 각 수정 후에 정규식을 실행하고 테스트해야 합니다. 여기서 약간의 시간을 보내면 많은 문제를 해결할 수 있습니다.
3. 그룹 교번 연산
교번 연산 기호( )는 우선순위가 낮아 프로그래머가 의도한 것보다 더 많이 교번하는 경우가 많다는 의미입니다. 예를 들어 텍스트에서 이메일 주소를 추출하는 정규식은 다음과 같습니다.
^CC: To:(.*)
위의 시도는 올바르지 않지만 이 버그는 종종 발견되지 않습니다. 위 코드의 목적은 "CC:" 또는 "To:"로 시작하는 텍스트를 찾은 다음 이 줄 끝에서 이메일 주소를 추출하는 것입니다.
불행하게도 "To:"가 줄 중간에 나타나면 이 정규식은 "CC:"로 시작하는 줄을 캡처하지 않고 대신 여러 임의의 텍스트 조각을 추출합니다. 솔직히 말하면 정규식은 "CC:"로 시작하는 줄과 일치하지만 아무것도 캡처하지 않거나 "To:"가 포함된 줄과 일치하지만 나머지 줄을 캡처합니다. 일반적으로 이 정규식은 많은 수의 이메일 주소를 캡처하므로 아무도 버그를 알아차리지 못합니다.
실제 의도를 충족하려면 괄호를 추가하여 이를 명확하게 해야 합니다. 정규식은 다음과 같습니다.
(^CC:) (To:(.*))
실제 의도가 "로 시작하는 텍스트를 캡처하는 것입니다
.CC:" 또는 "To:" 줄의 나머지 부분이 있는 경우 올바른 정규식은 다음과 같습니다.
^(CC: To:)(.*)
이것은 그룹화하는 습관을 들이면 피할 수 있는 일반적인 불완전 일치 버그입니다. 교대 작업에 대한 이 오류입니다.
4. 느슨한 수량자를 사용하십시오.
많은 프로그래머는 "*?", "+?" 및 "??"와 같은 느슨한 수량자를 사용하면 표현을 더 쉽게 작성하고 이해할 수 있습니다.
완화된 수량자는 가능한 한 적은 양의 텍스트와 일치하므로 정확한 일치가 성공하는 데 도움이 됩니다. "foo(.*?)bar"라고 쓰면 수량자는 마지막이 아닌 "bar"를 처음 만날 때 일치를 중지합니다. "foo###bar+++bar"에서 "###"을 캡처하려는 경우 이는 중요합니다. 엄격한 수량자는 "###bar++ +"를 캡처합니다. ;) 이로 인해 많은 문제가 발생할 수 있습니다. 완화된 수량자를 사용하면 문자 유형을 조합하는 데 매우 적은 시간을 소비하여 새로운 정규식을 생성할 수 있습니다.
완화된 수량자는 텍스트를 캡처하려는 컨텍스트의 구조를 알고 있을 때 큰 가치가 있습니다.
5. 사용 가능한 구분 기호를 사용하십시오.
Perl 및 PHP 언어는 정규 표현식의 시작과 끝을 표시하기 위해 왼쪽 슬래시(/)를 사용하는 경우가 많습니다. Python 언어는 시작과 끝을 표시하기 위해 따옴표 세트를 사용합니다. Perl과 PHP에서 왼쪽 슬래시를 사용하려면 표현식에서 슬래시를 사용하지 않는 것이 좋습니다. Python에서 따옴표를 사용하는 경우 백슬래시()를 사용하지 않는 것이 좋습니다. 다른 구분 기호나 따옴표를 선택하면 정규식의 절반을 피할 수 있습니다. 이렇게 하면 표현식을 더 쉽게 읽을 수 있고 기호를 피하는 것을 잊어버려서 발생하는 잠재적인 버그를 줄일 수 있습니다.
Perl 및 PHP 언어에서는 숫자가 아닌 문자와 공백 문자를 구분 기호로 사용할 수 있습니다. 새 구분 기호로 전환하면 URL 또는 HTML 태그(예: "http://" 또는 "<br/>;")를 일치시킬 때 왼쪽 슬래시가 누락되는 것을 방지할 수 있습니다.
예를 들어, "/http://(S)*/"는 "#http://(S)*#"로 쓸 수 있습니다.
일반적인 구분 기호는 "#", "!" 및 " "입니다. 대괄호, 꺾쇠 괄호 또는 중괄호를 사용하는 경우 일치하도록 유지하세요. 다음은 일반적인 구분 기호의 몇 가지 예입니다.
#…# !…! {…} s … … (Perl에만 해당) s[…][…] (Perl에만 해당) s<…>;/…/ (Perl에만 해당)
Python에서는 정규 표현식이 먼저 문자열로 처리됩니다. 따옴표를 구분 기호로 사용하면 모든 백슬래시가 누락됩니다. 그러나 "r''" 문자열을 사용하면 이 문제를 피할 수 있습니다. "re.VERBOSE" 옵션에 세 개의 연속된 작은따옴표를 사용하면 개행 문자를 포함할 수 있습니다. 예를 들어 regex = "( file://w+)(//d +)"는 다음 형식으로 작성할 수 있습니다.
regex = r'''
(w+)
(d+)
'''