어제 정규표현식 조건문에 관한 글을 소개해 드렸는데요, 오늘은 루비에서 정규표현식으로 활용할 수 있는 또 다른 재미있고 실용적인 트릭들을 살펴보려 합니다.
정규표현식으로 문자열 나누기
문자열을 구분자(delimiter)로 분리하는 것은 누구나 익숙하실 겁니다:
"one,two".split(",")
# => ["one", "two"]
그런데 split 메서드에 정규표현식도 그대로 넘길 수 있다는 사실을 알고 계셨나요?
# `,`와 `-`를 모두 구분자로 사용
"one,two-three".split(/,|-/)
=> ["one", "two", "three"]
# 미국 화폐는 콤마를 천 단위 구분 기호로,
# 유로화는 공백을 사용하는 경우
"1,000USD".split /(?=.*(USD))(?(1),| )/
구분자까지 함께 캡처하기
여기서 잊지 못할 '파티 트릭' 하나를 소개합니다. 보통 문자열을 split 하면 구분자는 결과에서 사라집니다:
# 콤마가 증발해 버립니다!
"one,two".split(",")
# => ["one", "two"]
하지만 정규표현식을 사용하고 구분자를 캡처 그룹(capture group) 안에 넣으면, split이 구분자까지 함께 반환합니다.
"one,two-three".split(/(,|-)/)
=> ["one", ",", "two", "-", "three"]
이런 동작이 일어나는 이유는 split이 사실 각 캡처 그룹의 경계를 기준으로 문자열을 자르기 때문입니다.
split 활용의 극한
split을 조금 비틀어서 거의 match처럼 동작하게 만들 수도 있습니다. 아래 코드에서는 정규표현식 안에 4개의 그룹을 만들어 문자열을 4조각으로 분리했습니다.
"1-800-555-1212".split(/(1)-(\d{3})-(\d{3})-(\d{4})/)
=> ["", "1", "800", "555", "1212"]
결과 배열의 첫 번째 요소가 빈 문자열인 이유는, 정규표현식이 원본 문자열 전체와 매칭되었기 때문입니다.
전역 매칭(Global Matching)
기본적으로 정규표현식은 패턴을 한 번만 매칭합니다. 아래 코드에서 매칭 가능한 후보가 다섯 개나 있지만 결과는 딱 한 개뿐입니다.
"12345".match /\d/
=> #<MatchData "1">
펄(Perl) 같은 다른 언어라면 정규표현식에 'global' 플래그를 붙여 해결하지만, 루비에는 그런 옵션이 없습니다. 대신 String#scan 메서드가 있습니다.
scan 메서드는 모든 매칭 결과를 배열로 돌려줍니다:
"12345".scan /\d/
=> ["1", "2", "3", "4", "5"]
편리한 블록 문법도 지원합니다:
"12345".scan /\d/ do |i|
puts i
end
아쉽게도 문자열을 지연(lazy) 방식으로 스캔하는 방법은 없어 보입니다. 따라서 이 기법은 예컨대 500MB짜리 파일을 처리하는 용도로는 적합하지 않습니다.
그룹과 함께 스캔하기
여기까지 읽으셨다면 'scan에서 그룹을 쓰면 어떤 신기한 일이 벌어질까?' 하는 호기심이 생기실 텐데요.
안타깝게도 여기서의 동작은 완전히 예측 가능하고 심심합니다. 그룹을 사용하면 다차원 배열이 반환됩니다:
"hiho hiho".scan /(hi)(ho)/
=> [["hi", "ho"], ["hi", "ho"]]
단, 한 가지 특이한 엣지 케이스가 있습니다. 그룹을 사용하면 그룹 밖의 내용은 결과에 포함되지 않습니다.
"hiho hiho".scan /(hi)ho/
=> [["hi"], ["hi"]]
간결한 매칭 문법
정규표현식이 문자열과 일치하는지 확인하는 방법으로 =~ 연산자는 이미 잘 알고 계실 겁니다. 이 연산자는 매칭이 시작되는 문자의 인덱스를 반환합니다.
"hiho" =~ /hi/
# 0
"hiho" =~ /ho/
# 2
그런데 매칭 여부를 확인하는 또 다른 빠른 방법이 있습니다. 바로 === 연산자입니다.
/hi/ === "hiho"
# true
루비에서 a === b라고 쓰면 'b가 a가 정의한 집합에 속하는가?'를 묻는 것입니다. 즉, "'hiho'는 정규표현식 /hi/가 매칭하는 문자열 집합에 속하는가?"라고 해석할 수 있습니다.
=== 연산자는 루비의 case 문 내부에서 사용됩니다. 덕분에 case 문에서도 정규표현식을 활용할 수 있습니다.
case "hiho"
when /hi/
puts "match"
end
삼중 등호 연산자는 자신이 작성한 메서드가 정규표현식과 문자열을 모두 인자로 받도록 만들 때도 유용합니다.
예를 들어 에러가 발생했을 때 무시 목록에 있는 클래스가 아니라면 특정 코드를 실행한다고 가정해 봅시다. 아래 예제에서는 === 연산자를 사용해 사용자가 문자열이나 정규표현식 중 어느 쪽이든 지정할 수 있게 했습니다.
def ignored?(error)
@ignored_patterns.any? { |x| x === error.class.name }
end
마무리
이 외에도 루비와 레일스 곳곳에는 이런 저런 작은 트릭들이 수십 가지나 숨어 있습니다. 특히 마음에 드는 트릭이 있다면 언제든 알려주세요!