Home » Julia 정규식 다루기

Julia 정규식 다루기

Julia에서 정규식은 별도의 모듈을 반입하지 않고 사용할 수 있으며, 패턴 자체는 Base.RegExp 타입으로 표현된다. 정규식 패턴을 만들 때에는 r"..." 형태의 리터럴로 바로 정의할 수 있다. (대신, raw string문자열은 raw"..."이다.) 한번 생성한 Regexp 값은 .pattern 필드로 그 패턴을 다시 확인할 수 있다. 참고로 r" .... " 리터럴은 정규식 패턴을 좀 더 손쉽게 사용할 수 있게 하기 위해 백슬래시를 이스케이프해준다. 따라서 \d 등과 같은 이스케이프 문자 시퀀스에서 백슬래시는 두 번이 아니라 한 번만 쓰면 된다. (r"\d+")

정규식 객체 및 매치 결과

생성된 정규식 객체에 대해서 match() 함수를 통해서 문자열에서 매치하는 부분을 찾을 수 있다. (match(pat, str) 의 모양이다.) 이때 매치한 결과는 단순히 매치되는 문자열 뿐만 아니라, 오프셋이나 캡쳐된 그룹등의 부가 정보를 포함해야 할 것이다. 따라서 이 정보들로 구성된 RegexMatch 타입의 객체가 리턴되며, 정규식 매치 타입의 값에는 다음과 같은 속성이 있다.

  • m.match : 패턴에 의해 매치되는 영역 전체의 문자열. 만약 반복적으로 매치되는 패턴이라면 마지막 매치 결과의 정보를 보여준다.
  • m.offset : m.match의 값이 원본 문자열에서 시작하는 위치
  • m.captures : 패턴에 캡쳐 그룹이 포함되어 있을 때, 각 캡쳐 그룹의 내용
  • m.offsets : 각 캡쳐 그룹의 오프셋값

캡쳐 그룹이 있는 패턴의 경우, 각각의 매치 그룹은 m[1], m[2] 와 같은 식으로도 액세스할 수 있다. match() 함수에서 패턴에 일치하는 영역을 찾지 못한 경우에는 비어있는 RegexMatch 객체 대신 nothing이 리턴된다. 다음은 괄호안의 단어를 그룹으로 캡쳐하는 패턴을 사용해서 매치하는 예이다.

s = "Use match(pat, str) to get the matched substring from a string."
pat = r"\b(\w[A-Za-z0-9_]+)\(([^\)]*)\)"
m = match(p, s)
# => RegexMatch("match(pat, str)", 1="match", 2="pat, str")
m.match #=> "match(pat, str)"
m[1]    #=> "match
m[2]    #=> "pat, str"

검사하고자 하는 문자열에서 주어진 패턴에 매치되는 영역이 반복적으로 나타나는 경우에는 항상 첫번째 매치가 발생하는 범위까지만 검사한다. 그 이후 부분에서 추가적으로 검색하려면 match() 함수를 다시 호출하면서 세번째 인자로 시작할 인덱스를 넘겨주면, 그 위치부터 다시 매치를 하게 된다. 따라서 반복해서 매치되는 패턴은 다음과 같은 방식으로 체크할 수 있다.

pat = r"(?:a|b)ce"
s = "acebcedcabcacebacedace"
o = 1
while true
  m = match(pat, s, o)
  (m != nothing) || break
  println("$(m.match)")
  o = lastindex(m.match) + m.offset
end

이와 동일한 환경에서 사용할 수 있는 것이 eachmatch(pat, str) 이다. 이 함수는 매치되는 구간을 찾고 다시 매치 뒤쪽부터 탐색을 계속하여 문자열의 끝까지 탐색하여, RegexMatch의 이터레이터를 리턴한다. 따라서 위의 코드는 eachmatch() 함수를 사용하면 아래와 같이 좀 더 간단하게 표현할 수 있다.

for m in eachmatch(pat, s)
  println(m.match)
end

# 왠지 이런 코드로 바꿔쓰고 싶은 강박이 든다.
# println.(map(x->x.match, eachmatch(pat, s)))

정규식을 사용하여 문자열 치환하기

언어에 무관하게 정규식을 사용하는 주된 용도 중 하나는 문자열의 특정 부위를 다른 값으로 치환하는 것이다. 줄리아에서도 이는 예외가 아니므로, 문자열 치환을 위한 replace()에 대해서도 알아두어야 한다. 참고로 replace()함수는 여러 메소드를 가지고 있으며, 배열의 특정 원소를 변경하는데에도 사용될 수 있다.

  • replace(A, old_new::Pair...; count=)
  • replace(f, A; count=)
  • replace!(A, old_new::Pair...; count=)
  • replace!(f, A; count=)

줄리아 함수 이름에 대한 관습에서 !로 끝나지 않는 함수는 인자로 주어진 값을 변경하지 않는다. 인자로 넘겨진 값을 직접 변경하기 위한 replace!()버전도 있다.

이때 재밌는 점은, 바꿀 부분과 바뀔 부분을 구분된 인자로 전달하는 것이 아니라, 원래 연속열과 교체대상=>교체값으로 구성된 Pair를 넘겨주는 식으로 작동한다. replace() 함수를 사용하면 여러 변경을 한 번에 수행할 수 있다. Pair가 아닌 교체 동작을 별도로 함수로 정의해서 인자로 넘겨줄 수도 있다.

replace() 함수는 기본적으로 두 개의 문자열 짝을 통해서 문자열의 일부분을 치환한다. 만약 정규식 패턴으로 교체할 영역을 매치하려 한다면, 교체 대상 값은 일반 문자열이 아닌 “교체 문자열”이라는 특별한 문자열의 타입을 사용해야 한다. (Pair{RegExp, SubstitutionString{String}}) 교체 문자열은 s"...."의 리터럴을 사용해서 작성할 수 있다. 아래 예제는 정규식 패턴을 사용해서 검출한 영역 뒤에 ()를 붙여주는 동작을 보여준다.

s = "Use match to get the matched substring from a string."
pat = r"(match)"
replace(s, pat => s"\1()") |> println
# Use match() to get the matched substring from a string.

그외 문자열 관련 함수에서 정규식 사용

문자열에서 특정 부분열을 찾는 find*()류 함수 중에서도 몇 가지 함수는 부분 문자열 대신에 정규식을 인자로 받을 수 있다.

  • findfirst(p, s) : 패턴이 최초로 나타나는 영역을 찾고 그 구간의 Range를 리턴한다.
  • findnext(p, s, start=) : 특정 위치 이후로 패턴이 나타나는 영역을 찾는다.
  • findall(p, s) : 패턴이 매치되는 모든 영역의 Range를 찾는다.

참고로 이런 함수들에서 어떤 것이 Regex 객체를 인자로 받을 수 있는지 알아내는 방법을 소개한다. Juila에서는 같은 이름의 함수도 인자의 타입이나 개수에 따라서 여러 정의를 가질 수 있다. 이 때 각각의 구현을 ‘메소드’라 한다. methods(f) 함수는 주어진 함수의 모든 메소드를 찾아서 리턴한다. 이 결과를 사용해서 특정 타입을 지원하는 메소드가 있는지 검사할 수 있다.

usingRegex(f) = begin
  ms = repr.(methods(f) |> collect)
  filter(ms) do item
    occursin("regex", item)
  end
end
usingRegex(findall)
# 1-element Array{String,1}:
#  "findall(t::Union{Regex, AbstractString}, s::AbstractString; overlap) in Base at # regex.jl:361"
usingRegex(findprev)
# String[]

댓글 남기기