공백을 포함한 모든 문자에 대응하는 regex

2017-07-08, Sat

나는 html을 다른 라이브러리 없이 그냥 정규식으로 파싱하는 편을 주로 선호한다. 가뜩이나 패키지 매니저도 없어서 의존성 관리 귀찮은 C++인데 깨작깨작 돌리는 작은 프로젝트에 makefile 붙혀주기 귀찮은게 제일 큰 이유일 것이다.
html특성상 태그들을 .*?같은 형식으로 뭉텅이로 무시하고 넘기는 경우가 많은데 .이 처리해야 하는 임의의 문자들에 평범한 문자들에 탭, 스페이스, \n이 섞여서 들어오게 된다. 하지만 C++이 기본적으로 사용하는 ECMAScript의 문법에서 .은 \n에 매칭되지 않는다.

아무튼 정규식을 쓸 테니 해결은 해야겠는데, 첫번째 방법으로 플래그를 바꿀 수 있고 두번째 방법으로 정규식 문법을 바꿀 수 있고 마지막 방법으로 패턴을 바꿀 수 있다.

플래그를 바꿀 때 다른 언어에서 /regex_pattern/s, (?s)regex_pattern처럼 사용되는 single line 또는 dotall이라는 플래그는 아쉽게도 C++에 없다.
참고: 플래그 종류가 적혀 있는 cppreferece.com

사실 위의 링크에서 볼 수 있는 것처럼 C++의 regex이 구현하는 문법들 중 POSIX extended가 있고 실제로 newline에 충실히 매칭시켜주지만 그거 하나 하겠다고 모든 \w류와 *를 [[:alnum:]_]과 *?로 바꿔줄 순 없는 노릇이다. 예전에 C에서 <regex.h>가지고 파싱하다가 고생한 기억을 되살려보면 정말 추천하고 싶지 않다.

2-2번 방법이라 할 수 있는데, Boost의 정규식은 기본적으로 dot을 newline에 매칭시켜 준다. 하지만 Boost.regex는 header-only가 아니기 때문에 makefile이 필요한건 마찬가지고 makefile 대신 쓰는 컴파일&링킹 스크립트에 libboost_XXX를 추가해준다고 한들 컴파일 시간이 너무 늘어나서 사용하기가 힘들다.

마지막 3번 패턴을 바꾸는 방법이 남았다. .대신에 어떤 패턴을 쓰면 매칭될지에 대해 이미 SO에 많은 질문글이 올라와 있다. 그에 대한 답변도 다양해서 [\s\S], [^], (?:.|\n), (?:.|\r?\n) 들이 해결책으로 제시되었다.

하도 문법과 케이스가 복잡한 regex니까 전부 체크 해보는 편이 미래를 위해서는 좋다.

#include <iostream>
#include <regex>

void checkRegex(const std::string& str, std::regex re, const char re_name[])
{
    std::smatch sm;
    if (std::regex_match(str, sm, re))
        std::cout << "re " << re_name << " Match\n";
}
int main()
{
    const std::string str =
        R"(a <>
b)";

    std::regex re1{R"(a[\s\S]*?b)"};
    std::regex re2{R"(a[^]*?b)"};
    std::regex re3{R"(a(?:.|\r?\n)*?b)"};
    std::regex re4{R"(a.*?b)", std::regex_constants::basic};
    std::regex re5{R"(a.*?b)", std::regex_constants::extended};

    checkRegex(str, re1, R"([\s\S])");
    checkRegex(str, re2, R"([^])");
    checkRegex(str, re3, R"((?:.|\r?\n))");
    checkRegex(str, re4, R"(.*? POSIX basic)");
    checkRegex(str, re5, R"(.*? POSIX extended)");
}

여러 답안들이 실제 작동하는지 위의 코드로 체크해보았고 (clang C++14)

re [\s\S] Match
re [^] Match
re (?:.|\r?\n) Match
re .*? POSIX extended Match
를 얻었다. 매칭되는 4가지 중 문법이 유지되는 1-3 패턴 중에 [^]이 아마 성능이 제일 잘 뽑히지 않을까 라는 생각이 든다.

POSIX문법 을 찾아보고 있었는데 POSIX-basic과 POSIX-extended에 어떤 차이가 있어서 re4와 re5의 결과가 다른지 모르겠다. 9.3.49.4.4둘 다 NUL을 제외한 모든 문자에 매칭한다는데 왜 다르게 나오는 건지..
gnu쪽 문서에서 봐도 어떤 차이가 있는 건지 잘 모르겠다.