Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Ruby 정규 표현식 내에서 조건부 사용

Ruby 2.0이 2013년에 출시한 많은 새로운 기능 중에서 제가 가장 주목하지 않은 것은 새로운 정규식 엔진인 Onigmo였습니다. 결국 정규식은 정규식입니다. Ruby에서 정규식을 구현하는 방법에 신경을 써야 하는 이유는 무엇입니까?

알고 보니 Onigmo 정규식 엔진에는 정규 표현식 내부에 조건문을 사용하는 기능을 포함하여 몇 가지 깔끔한 트릭이 있습니다.

이 게시물에서 우리는 정규식 조건문에 대해 알아보고, Ruby 구현의 단점에 대해 배우고, Ruby의 한계를 해결하기 위한 몇 가지 트릭에 대해 논의할 것입니다. 시작하겠습니다!

그룹 및 캡처

정규식의 조건문을 이해하려면 먼저 그룹화 및 캡처를 이해해야 합니다.

미국 인용 목록이 있다고 상상해 보세요.

Fayetteville, AR
Seattle, WA

주 약어에서 도시 이름을 분리하려고 합니다. 이를 수행하는 한 가지 방법은 여러 경기를 수행하는 것입니다.

PLACE = "Fayetteville, AR"

# City: Match any char that's not a comma
PLACE.match(/[^,]+/) 
# => #<MatchData "Fayetteville">

# Separator: Match a comma and optional spaces
PLACE.match(/, */)
# => #<MatchData ", ">

# State: Match a 2-letter code at the end of the string. 
PLACE.match(/[A-Z]{2}$/) 
# => #<MatchData "AR">

이것은 작동하지만 너무 장황합니다. 그룹을 사용하면 하나의 정규식으로 도시와 주를 모두 캡처할 수 있습니다.

따라서 위의 정규식을 결합하고 각 섹션을 괄호로 묶습니다. 괄호는 정규 표현식에서 항목을 그룹화하는 방법입니다.

PLACE = "Fayetteville, AR"
m = PLACE.match(/([^,]+)(, *)([A-Z]{2})/) 
# => #<MatchData "Fayetteville, AR" 1:"Fayetteville" 2:", " 3:"AR">

보시다시피 위의 표현식은 도시와 주를 모두 캡처합니다. MatchData를 처리하여 액세스합니다. 배열처럼:

m[1]
# => "Fayetteville"
m[3]
# => "AR"

그룹화의 문제는 위에서 수행한 것처럼 캡처된 데이터가 배열에 저장된다는 것입니다. 배열에서 위치가 변경되면 코드를 업데이트해야 하거나 방금 버그를 도입한 것입니다.

예를 들어 ", " 문자. 따라서 정규 표현식의 해당 부분 주위에 있는 괄호를 제거합니다.

m = PLACE.match(/([^,]+), *([A-Z]{2})/) 
# => #<MatchData "Fayetteville, AR" 1:"Fayetteville" 2:"AR">

m[3]
# => nil

하지만 지금은 m[3] 더 이상 상태 - 버그 도시를 포함하지 않습니다.

명명된 그룹

정규식 그룹에 이름을 지정하여 훨씬 더 의미 있는 그룹으로 만들 수 있습니다. 구문은 방금 사용한 것과 매우 유사합니다. 정규식을 괄호로 묶고 다음과 같이 이름을 지정합니다.

/(?<groupname>regex)/

이것을 시/도 정규 표현식에 적용하면 다음을 얻습니다.

m = PLACE.match(/(?<city>[^,]+), *(?<state>[A-Z]{2})/)
# => #<MatchData "Fayetteville, AR" city:"Fayetteville" state:"AR">

그리고 MatchData를 처리하여 캡처된 데이터에 액세스할 수 있습니다. 해시처럼:

m[:city] 
# => "Fayetteville"

조건

정규식의 조건문은 /(?(A)X|Y)/ 형식을 취합니다. . 다음은 몇 가지 유효한 사용 방법입니다.

# If A is true, then evaluate the expression X, else evaluate Y
/(?(A)X|Y)/

# If A is true, then X
/(?(A)X)/

# If A is false, then Y
/(?(A)|Y)/

귀하의 상태에 대한 가장 일반적인 두 가지 옵션, A

  • 명명 또는 번호가 지정된 그룹이 캡처되었습니까?
  • 둘러보기가 참으로 평가됩니까?

사용 방법을 살펴보겠습니다.

그룹이 캡처되었습니까?

그룹이 있는지 확인하려면 ?(n)를 사용하세요. 여기서 n은 정수 또는 <>로 묶인 그룹 이름입니다. 또는 '' .

# Has group number 1 been captured?
/(?(1)foo|bar)/

# Has a group named "mygroup" been captured?
/(?(<mygroup>)foo|bar)/

예시

미국 전화 번호를 구문 분석한다고 상상해보십시오. 이 번호에는 1로 시작하지 않는 한 선택 사항인 3자리 지역 코드가 있습니다.

1-800-555-1212 # Valid
800-555-1212 # Valid
555-1212 # Valid

1-555-1212 # INVALID!!

숫자가 1로 시작하는 경우에만 조건부를 사용하여 지역 번호를 요구 사항으로 만들 수 있습니다.

# This regular expression looks complex, but it's made of simple pieces
# `^(1-)?` Does the string start with "1-"? If so, capture it as group 1
# `(?(1)` Was anything captured in group one?
# `\d{3}-` if so, do a required match of three digits and a dash (the area code)
# `|(\d{3}-)?` if not, do an optional match of three digits and a dash (area code)
# `\d{3}-\d{4}` match the rest of the phone number, which is always required.

re = /^(1-)?(?(1)\d{3}-|(\d{3}-)?)\d{3}-\d{4}/

"1-800-555-1212".match(re)
#=> #<MatchData "1-800-555-1212" 1:"1-" 2:nil>

"800-555-1212".match(re)
#=> #<MatchData "800-555-1212" 1:nil 2:"800-">

"555-1212".match(re)
#=> #<MatchData "555-1212" 1:nil 2:nil>

"1-555-1212".match(re)
=> nil

제한 사항

그룹 기반 조건문을 사용할 때의 한 가지 문제는 그룹 일치가 문자열에서 해당 문자를 "소비"한다는 것입니다. 그러면 해당 문자는 조건문에서 사용할 수 없습니다.

예를 들어, 다음 코드는 "USD"라는 텍스트가 있는 경우 100을 일치시키려고 시도하지만 일치하지 않습니다.

"100USD".match(/(USD)(?(1)\d+)/) # nil

Perl 및 일부 다른 언어에서는 조건문에 미리보기 문을 추가할 수 있습니다. 이렇게 하면 문자열의 아무 곳에서나 텍스트를 기반으로 조건부를 트리거할 수 있습니다. 하지만 Ruby에는 이 기능이 없으므로 약간의 창의력을 발휘해야 합니다.

둘러보기

다행히도 둘러보기 표현식을 남용하여 Ruby 정규식 조건의 한계를 해결할 수 있습니다.

둘러보기란 무엇입니까?

일반적으로 정규식 파서는 일치하는 항목을 찾기 위해 처음부터 끝까지 문자열을 단계별로 실행합니다. 워드 프로세서에서 커서를 왼쪽에서 오른쪽으로 움직이는 것과 같습니다.

미리보기 및 뒤보기 표현식은 약간 다르게 작동합니다. 문자를 사용하지 않고 문자열을 검사할 수 있습니다. 완료되면 커서는 처음에 있던 것과 똑같은 위치에 남습니다.

둘러보기에 대한 훌륭한 소개를 보려면 Rexegg의 뒤돌아보기를 마스터하기 위한 가이드를 확인하세요.

구문은 다음과 같습니다.

유형 구문
예측 (?=query) \d+(?= dollars) "100달러"에서 100과 일치
네거티브 전망 (?!query) \d+(?! dollars) 뒤에 "dollars"라는 단어가 없으면 100과 일치합니다.
뒤돌아보기 (?<=query) (?<=lucky )\d "럭키 7"에서 7과 일치
부정적인 시선 (?<!query) (?<!furious )\d "럭키 7"에서 7과 일치

조건을 향상시키기 위해 둘러보기를 남용

조건부에서는 이미 설정된 그룹의 존재만 쿼리할 수 있습니다. 일반적으로 이는 그룹의 콘텐츠가 사용되었으며 조건부에서 사용할 수 없음을 의미합니다.

그러나 미리보기를 사용하여 문자를 소모하지 않고 그룹을 설정할 수 있습니다! 아직 정신이 없으신가요?

작동하지 않는 이 코드를 기억하십니까?

"100USD".match(/(USD)(?(1)\d+)/) # nil

미리보기에서 그룹을 캡처하도록 수정하면 갑자기 잘 작동합니다.

"100USD".match(/(?=.*(USD))(?(1)\d+)/)
=> #<MatchData "100" 1:"USD">

해당 쿼리를 분석하고 무슨 일이 일어나고 있는지 살펴보겠습니다.

  • (?=.*(USD)) 미리보기를 사용하여 "USD"에 대한 텍스트를 스캔하고 그룹 1에 캡처합니다.
  • (?(1) 그룹 1이 있는 경우
  • \d+ 그런 다음 하나 이상의 숫자를 찾습니다.

꽤 깔끔하죠?