루아(Lua)와 정규표현식

Twitter icon류광, 2022-09-27 15:09
루아의 패턴 부합 기능 이야기 약간과 "진짜" 정규표현식을 위한 Lrexlib 소개 및 팁

루아의 패턴 부합 기능은 정규표현식이 아니다

루아 표준 라이브러리의 string.findstring.gsub 등이 지원하는 패턴 부합(pattern matching) 기능은 정규 표현식은 아니고 정규 표현식의 부분집합에 해당합니다.

예를 들어 루아 패턴 부합 기능은 대안 선택 |를 지원하지 않습니다. 한 예로 "dog""fog" 모두와 부합하는 패턴이 필요할 때 PCRE나 POSIX 정규표현식 등에서는 "dog|fog" 또는 "(d|f)og"로 하면 되지만 루아에서는 문자 부류(character class)를 이용해서 "[dg]og"로 표현해야 합니다. "dog|cat"처럼 이런 편법이 통하지 않는 경우에는 패턴 부합을 여러 번 적용하는 수밖에 없지요.

그밖에 루아의 패턴 부합은 비갈무리(non-capture) 그룹이나 미리 내다보기(look-ahead) 같은 고급(?) 정규식 기능들도 지원하지 않습니다.

루아 표준 라이브러리가 POSIX이나 PCRE 같은 본격적인 정규식을 지원하지 않는 이유는 "작고 빠른 구현"이라는 루아의 지향점 때문입니다. "Programming in Lua 제1판" 20.1절[1]을 보면 POSIX 정규 표현식의 전형적인 구현은 코드가 4,000줄이고 이는 루아 표준 라이브러리 전체보다 크다, 루아의 패턴 부합 기능은 500줄 정도이다, 라는 이야기가 나옵니다.

루아의 패턴 부합 기능이 번듯한 정규표현식보다는 훨씬 작지만, 그래도 충분히 강력하긴 합니다. 저는 여러 가지 텍스트 처리에서 루아를 잘 써먹고 있는데, 특히 번역을 위해 원서 HTML 파일이나 (La)TeX 파일을 파싱해서 마크다운 비슷한 구조적 텍스트로 변환하는 꽤 복잡한 작업도 루아로 처리하고 있습니다.

루아 패턴 부합만의 독특한 기능

루아 패턴 부합만의 독특한 기능이라면 괄호 짝 맞추기가 있는데요. 예를 들어

abc (def (ghi) 123 (456 (jkl))) mno (pqr)

같은 문자열에서 좌우 괄호들의 짝이 맞는 "(def (ghi) 123 (456 (jkl)))"를 추출한다고 하면, PCRE에서는 다음과 같이 복잡한 패턴이 필요하지만,[2]

\((?:[^)(]+|(?R))*+\)

루아에서는 %b()로 간단하게 표현할 수 있습니다. 여기서 ()는 정규표현식의 그룹을 지정하는 것이 아니고, 검출할 여는 괄호와 닫는 괄호를 나타내는 리터럴 문자입니다.[3] %b{}, %b<>, %b[] 등 다른 괄호들도 가능하고요.

정규표현식을 위한 Lrexlib 라이브러리

그렇긴 하지만 정규표현식의 고급 기능이 아쉬울 때도 많습니다. 다른 여러 언어처럼 표준 라이브러리에 없는 기능은 사용자 라이브러리로 보충하면 되는데요. 다양한 정규표현식 구현을 제공하는 확장 라이브러리로 Lrexlib가 있습니다.

Lrexlib는 다양한 '향(flavour)'으로 제공됩니다. 정규표현식에도 종류(향)가 여러 가지인데, 현재 Lrexlib는 PCRE, PCRE2, POSIX, oniguruma, TRE, GNU 정규표현식을 제공합니다. 각 향은 각자 다른 라이브러리로 존재하므로 원하는 것만 따로 설치해서 사용할 수 있습니다. 예를 들어 PCRE2를 위한 라이브러리는 lrexlib-PRCE2입니다.

Lrexlib 관련 팁 몇 가지

다음은 Lrexlib 관련 팁 몇 가지입니다.

이진 라이브러리 파일

운영체제+아키텍처별로 미리 빌드된 이진 파일을 제공하는 곳은 없는 것 같습니다(제보 부탁). 결국 소스 코드로부터 빌드해야 하는데, 관련 환경을 잘 꾸며 놓지 않았다면 luarocks install로 빌드하는 것보다 그냥 해당 깃허브 저장소에 있는 Makefile을 사용하는 게 더 수월할 수 있습니다. 특히 Windows 환경에서요.

UTF-8 한글 지원

패턴에서 한글 문자들을 그 자체로 개별 문자로 취급하려면, 예를 들어 모든 완성형 한글 낱자가 [가-힣] 패턴과 부합되게 하려면, 다음 예처럼 UTF-8 모드를 활성화해야 합니다.[4] UTF8만으로는 부족하고 UCP도 지정해야 한다는 점이 핵심입니다. 그리고 PCRE2 향에서는 이상하게도 UTF8이 아니라 UTF라는 점도 주의해야 합니다.

local F = rex.flags()
re = rex.new([[^[^\W\d_]+$]], F.UTF8 + F.UCP)

모드 수정자

예를 들어 패턴 부합 시 대소문자를 구분하지 않으려면, 자바스크립트 등에서는 /dog/i처럼 패턴 끝에 i라는 플래그를 지정하면 됩니다. 그런데 Lrexlib의 패턴 리터럴은 /패턴/플래그들 형태가 아니고 그냥 문자열 "패턴"입니다. 앞의 UTF-8 예처럼 rex.new 같은 함수를 호출할 때 따로 플래그를 지정하면 되긴 하지만(참고로 대소문자 구분 없는 부합을 위한 플래그는 CASELESS입니다), 좀 불편합니다. 특히, Art of Unix Programming에서 말하듯이 정규표현식은 하나의 미니 언어(minilanguage)이므로,[5] 성능 문제가 없는 한 패턴 자체로 모든 것을 말하는 게 자연스럽습니다.

다행히 정규표현식 엔진들은 플래그를 패턴 자체에서 지정하는 기능을 제공합니다. 예를 들어 대소문자를 구별하지 않는 부합은 다음 예처럼 (?i)라는 수정자를 패턴 안에 사용하면 됩니다.

> rex.match("Dogs and cats", "(?i)dog|cat")  -- 대문자가 있는 Dog를 찾음
Dog
> rex.match("Dogs and cats", "dog|cat")  --Dog는 찾지 못하고 cat을 찾음
cat

사용 가능한 수정자는 정규표현식 엔진마다 차이가 있습니다. https://www.regular-expressions.info/refmodifiers.html을 참고하세요.

표준 문자열 확장

앞에서 언급했듯이 한글 글자들을 바이트열이 아니라 온전한 하나의 글자로 취급하게 하려면 UTF-8 플래그들을 지정해 주어야 하는데, 좀 번거롭습니다. 또한 루아 패턴 부합에서는 string.match(str, '%d+')str:match'%d+'로 간결하게 표현할 수 있지만 Lrexlib의 메서드들은 그렇지 않습니다.

이 둘을 한꺼번에 해결하는 한 방법은 Lrexlib의 메서드를 호출하는 커스텀 메서드를 표준 string에 추가하는 것입니다. 예를 들어

local F = rex.flags()

local function xmatch(subj, patt, init, cf, ef, ...)
    cf = cf or (F.UTF + F.UCP) 
    return rex.match(subj, patt, init, cf, ef, ...)
end

rawset(string, 'xmatch', xmatch)

를 실행해 두면, 그때부터는

local korean_name = str:xmatch'이름:([가-힣]+)'

처럼 사용할 수 있습니다.


  1. 제1판은 루아 5.0 기준이지만 전체적인 지향점이나 설계 철학은 변하지 않았습니다. 

  2. 출처: https://stackoverflow.com/a/35271017/400336 

  3. 한편 %b%는 정규식의 \에 해당하는 특수문자인데요. "이것은 정규표현식이 아니다"를 강조하기 위해 패턴의 특수문자 자체를 다르게 선택했으리라 짐작합니다. b는 brace(중괄호)이나 bracket(대괄호)이겠고요. 

  4. 출처: https://github.com/rrthomas/lrexlib/issues/14#issuecomment-74521382 

  5. http://www.catb.org/esr/writings/taoup/html/ch08s02.html#regexps 

태그: 프로그래밍 Lua
comments powered by Disqus