Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby 정규 표현식 완벽 가이드: 기초 문법부터 실전 활용까지

Ruby 정규 표현식(ruby regex)은 문자열 안에서 특정 패턴을 찾아내고, 그 결과를 추출해 추가 처리에 활용할 수 있게 해주는 강력한 도구입니다.

정규 표현식의 대표적인 활용 사례는 크게 두 가지입니다. 바로 유효성 검사(validation)파싱(parsing)입니다.

예를 들어, 이메일 주소를 생각해 봅시다. 정규 표현식을 사용하면 '올바른 이메일 주소의 형태'를 직접 정의할 수 있습니다. 즉, 프로그램이 유효한 이메일과 유효하지 않은 이메일을 스스로 구분할 수 있게 되는 것입니다.

Ruby 정규 표현식 완벽 가이드: 기초 문법부터 실전 활용까지

Ruby에서 정규 표현식은 두 개의 슬래시(/) 사이에 정의합니다. 다른 문법 요소와 구분하기 위함이며, 가장 단순한 표현식은 하나의 단어 또는 한 글자만 매칭할 수도 있습니다.

예시:

# 'like'라는 단어 찾기
"Do you like cats?" =~ /like/

매칭에 성공하면 해당 단어가 처음 등장하는 인덱스를 반환하고, 실패하면 nil을 반환합니다. 인덱스가 필요 없다면 String#include? 메서드를 사용하는 것이 더 간단합니다.

문자열이 정규 표현식과 일치하는지 확인하는 또 다른 방법은 match 메서드를 사용하는 것입니다.

if "Do you like cats?".match(/like/)
  puts "매치 발견!"
end

이제부터는 날짜, 전화번호, 이메일, URL 같은 것들을 매칭하고, 캡처하고, 치환할 수 있는 더 복잡한 패턴을 만드는 방법을 알아보겠습니다.

문자 클래스(Character Classes)

문자 클래스를 사용하면 매칭할 문자의 범위나 목록을 정의할 수 있습니다. 예를 들어 [aeiou]는 모든 모음과 일치합니다.

예제: 문자열에 모음이 포함되어 있는지 확인하기

def contains_vowel(str)
  str =~ /[aeiou]/
end

contains_vowel("test") # 1 반환
contains_vowel("sky")  # nil 반환

이 방법은 문자의 개수까지는 고려하지 않습니다. 개수를 다루는 방법은 곧 배우게 됩니다.

범위(Ranges)

범위를 사용하면 여러 글자나 숫자를 일일이 나열하지 않고도 매칭할 수 있습니다. 즉, [2-5][2345]와 동일한 의미입니다.

유용하게 쓰이는 범위들:

  • [0-9]: 0부터 9까지의 숫자와 일치
  • [a-z]: a부터 z까지의 소문자와 일치
  • [^a-z]: 부정(negated) 범위 — 소문자가 아닌 문자와 일치

예제: 문자열에 숫자가 포함되어 있는지 확인하기

def contains_number(str)
  str =~ /[0-9]/
end

contains_number("The year is 2015")  # 12 반환
contains_number("The cat is black")  # nil 반환

기억하세요: =~ 연산자의 반환값은 항상 인덱스 또는 nil입니다.

문자 범위를 지정할 때 유용한 축약 문법도 있습니다.

  • \w: [0-9a-zA-Z_]와 동일 (영숫자 + 언더스코어)
  • \d: [0-9]와 동일 (숫자)
  • \s: 공백 문자와 일치 (탭, 일반 공백, 줄바꿈)

각각의 부정 형태도 존재합니다.

  • \W: [0-9a-zA-Z_]에 속하지 않는 모든 문자
  • \D: 숫자가 아닌 모든 문자
  • \S: 공백이 아닌 모든 문자

점(.) 문자는 줄바꿈을 제외한 모든 문자와 일치합니다. 리터럴 점 자체를 매칭하려면 반드시 이스케이프(escape)해야 합니다.

예제: 특수 문자 이스케이프하기

# 이스케이프하지 않으면 어떤 문자든 일치함
"5a5".match(/\d.\d/)

# 이 경우에는 리터럴 점(.)만 일치함
"5a5".match(/\d\.\d/) # nil
"5.5".match(/\d\.\d/) # 매치 성공

수량자(Modifiers)

지금까지는 한 번에 한 글자만 매칭할 수 있었습니다. 여러 문자를 매칭하려면 패턴 수량자를 사용해야 합니다.

수량자 설명
+ 1개 이상
* 0개 이상
? 0개 또는 1개
{3,5} 3개 이상 5개 이하

지금까지 배운 내용을 조합하면 더 복잡한 정규 표현식을 만들 수 있습니다.

예제: IP 주소처럼 보이는지 확인하기

# 참고: 이 패턴은 999.999.999.999처럼 잘못된 IP도 매칭할 수 있습니다.
# 하지만 여기서는 형식만 확인한다고 가정합니다.

def ip_address?(str)
  # !!를 사용해 반환값을 불리언으로 변환
  !!(str =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)
end

ip_address?("192.168.1.1")  # true 반환
ip_address?("0000.0000")    # false 반환

정확한 문자열 매칭

정확히 일치하는지 확인하려면 다른 종류의 앵커(anchor)가 필요합니다. 예시를 통해 살펴보겠습니다.

# 문자열이 정확히 네 글자인지 확인하고 싶지만,
# 아래 코드는 네 글자를 초과해도 매칭됩니다.
"Regex are cool".match /\w{4}/

# 대신 '줄 시작(^)'과 '줄 끝($)' 앵커를 사용합니다.
"Regex are cool".match /^\w{4}$/

# 이번에는 매칭되지 않습니다. 사실 .size로 길이를 확인하는 게 더 간단하지만,
# 개념을 이해하는 데는 충분한 예제입니다.

줄바꿈(\n) 이후의 각 줄이 아니라 문자열 전체의 시작과 끝을 엄격하게 매칭하려면 ^$ 대신 \A\Z(또는 \z)를 사용해야 합니다.

캡처 그룹(Capture Groups)

캡처 그룹을 사용하면 매칭 결과의 일부를 저장해 두었다가 나중에 재사용할 수 있습니다. 캡처하려는 부분을 괄호로 감싸면 됩니다.

예제: 로그 파일 파싱하기

Line = Struct.new(:time, :type, :msg)
LOG_FORMAT = /(\d{2}:\d{2}) (\w+) (.*)/

def parse_line(line)
  line.match(LOG_FORMAT) { |m| Line.new(*m.captures) }
end

parse_line("12:41 INFO User has logged in.")
# 아래와 같은 객체가 생성됩니다:
#

이 예제에서는 =~ 대신 .match 메서드를 사용했습니다.

match 메서드는 매칭에 성공하면 MatchData 객체를, 실패하면 nil을 반환합니다. MatchData 클래스에는 유용한 메서드가 많으니 공식 문서를 참고해 보세요!

단순히 불리언 값(true/false)만 필요하다면 Ruby 2.4부터 제공되는 match? 메서드를 사용할 수 있습니다. MatchData 객체를 생성하지 않으므로 match보다 더 빠르다는 장점도 있습니다.

캡처된 데이터는 .captures 메서드로 접근하거나, MatchData 객체를 배열처럼 다루면 됩니다. 인덱스 0에는 전체 매칭 결과가 들어가고, 이후 인덱스에는 각 캡처 그룹의 결과가 순서대로 저장됩니다.

첫 번째 캡처 그룹에 접근하려면 다음과 같이 하면 됩니다.

m = "John 31".match /\w+ (\d+)/

m[1]
# "31"

캡처하지 않는 그룹(non-capturing group)도 사용할 수 있습니다. 표현식을 묶기만 할 뿐 저장하지 않으므로 성능상 이점이 있습니다. 또한 이름 있는 그룹(named group)을 사용하면 복잡한 표현식을 훨씬 읽기 쉽게 만들 수 있습니다.

문법 설명
(?:...) 캡처하지 않는 그룹
(?<foo>...) 이름 있는 그룹

예제: 이름 있는 그룹 사용하기

m = "David 30".match /(?<name>\w+) (?<age>\d+)/
m[:age]
# => "30"
m[:name]
# => "David"

이름 있는 그룹 역시 MatchData 객체를 반환하며, 이 객체를 통해 결과를 읽을 수 있습니다.

전방 탐색과 후방 탐색(Look Ahead & Look Behind)

이것은 좀 더 고급 기법으로, 모든 정규 표현식 엔진에서 지원하지는 않습니다. 다행히 Ruby의 정규 표현식 엔진은 이를 지원하므로, 활용 방법을 살펴보겠습니다.

탐색(lookaround)을 사용하면 특정 패턴 앞이나 뒤에 무엇이 있는지 '몰래 들여다볼' 수 있습니다.

이름 설명
(?=pat) 긍정형 전방 탐색(positive lookahead)
(?<=pat) 긍정형 후방 탐색(positive lookbehind)
(?!pat) 부정형 전방 탐색(negative lookahead)
(?<!pat) 부정형 후방 탐색(negative lookbehind)

예제: 최소 한 글자 이상의 문자 뒤에 숫자가 오는지 확인하기

def number_after_word?(str)
  !!(str =~ /(?<=\w) (\d+)/)
end

number_after_word?("Grade 99")

Ruby의 Regexp 클래스

Ruby의 정규 표현식은 Regexp 클래스의 인스턴스입니다. 대부분의 경우 이 클래스를 직접 다룰 일은 없지만, 알아두면 좋습니다.

puts /a/.class
# Regexp

활용 예로, 문자열로부터 정규 표현식을 생성할 수 있습니다.

regexp = Regexp.new("a")

정규 표현식을 만드는 또 다른 방법은 %r 표기법을 사용하는 것입니다.

regexp = %r{\w+}

정규 표현식 옵션

정규 표현식에 옵션을 설정하면 동작 방식을 바꿀 수 있습니다.

옵션 설명
i 대소문자 구분 없이 매칭(case insensitive)
m 점(.)이 줄바꿈 문자도 매칭
x 공백 무시 (가독성 향상)

옵션을 사용하려면 정규 표현식을 닫는 / 뒤에 해당 글자를 붙이면 됩니다.

사용 예:

"abc".match?(/[A-Z]/i)

긴 정규 표현식 보기 좋게 작성하기

복잡한 Ruby 정규 표현식은 읽기 어려워질 수 있습니다. 이럴 때 'x' 옵션을 사용하면 표현식을 여러 줄로 나눌 수 있고, 심지어 정규 표현식 안에 주석도 작성할 수 있습니다.

예제:

LOG_FORMAT = %r{
  (\d{2}:\d{2}) # 시간
  \s(\w+)       # 이벤트 타입
  \s(.*)        # 메시지
}x

Ruby 정규 표현식 종합 활용

정규 표현식은 다양한 Ruby 메서드와 함께 사용할 수 있습니다.

  • .split
  • .scan
  • .gsub
  • 그 외 많은 메서드들...

예제: .scan으로 문자열에서 모든 단어 추출하기

"this is some string".scan(/\w+/)
# => ["this", "is", "some", "string"]

예제: 문자열에서 모든 숫자 추출하기

"The year was 1492.".scan(/\d+/)
# => ["1492"]

예제: 문자열의 모든 단어 첫 글자 대문자로 변환하기

str = "lord of the rings"

str.gsub(/\w+/) { |w| w.capitalize }
# => "Lord Of The Rings"

예제: 이메일 주소 유효성 검사하기

email = "test@example.com"

!!email.match(/\A[\w.+-]+@\w+\.\w+\z/)

# true

마지막 예제에서는 !!를 사용해 결과를 불리언 값(true/false)으로 변환했습니다. Ruby 2.4 이상이라면 이 변환을 자동으로 처리해주고 더 빠른 match? 메서드를 사용하는 것도 좋은 선택입니다.

마무리

정규 표현식은 놀라울 정도로 강력하지만, 때로는 까다롭게 느껴질 수 있습니다. rubular.com 같은 도구를 활용하면 Ruby 정규 표현식을 더욱 인터랙티브하게 만들고 테스트할 수 있습니다. Rubular에는 유용한 Ruby 정규 표현식 치트 시트도 함께 제공되니 꼭 확인해 보세요. 이제 에디터를 열고 직접 코딩을 시작해 볼 차례입니다!

아, 그리고 이 글이 도움이 되었다면 주변 친구들에게 공유해서 더 많은 사람들이 함께 배울 수 있도록 해주세요.