[C++] 정규표현식, regex

[C++] 정규표현식, regex

이번 글에서는 C++에서 정규표현식(Regular Expression)을 사용하는 방법에 대해 알아보겠습니다.

정규표현식의 사전적 의미로는 ‘특정한 규칙을 가진 문자열의 집합을 표현하는 데 사용하는 형식 언어’ 입니다.
보통 프로그래밍에서는 문자열에서 패턴의 매칭 여부나, 검색, 문자열을 변경하는데 사용됩니다.

아래는 대표적으로 사용되는 정규표현식의 문법 목록입니다.

문법설명
.임의의 한 문자
^문자열 시작
$문자열 끝
*앞의 문자가 0번 이상 반복
+앞의 문자가 1번 이상 반복
?앞의 문자가 0번 또는 1번 나타남
{n}앞의 문자가 n번 나타남
{n,}의 문자가 n번 이상 나타남
{n, m}앞의 문자가 n번 이상 m번 이하 나타남
[]괄호 안에 있는 문자 중 하나를 매치
[-]문자 범위. [a-d]일 경우 a, b, c, d 중 하나를 매치
[^]괄호 안에 있는 문자 이외의 것과 매치
()Grouping(그룹핑)
|or 연산.
\d숫자와 매치, [0-9]와 동일
\D숫자가 아닌 것과 매치, [^0-9]와 동일
\w문자와 숫자 매치, [a-zA-Z0-9]와 동일
\W문자와 숫자가 아닌것과 매치, [^a-zA-Z0-9]와 동일
\s공백문자와 매치, [\t\n\r\f\v]와 동일
\S공백문자가 아닌 것과 매치, [^\t\n\r\f\v]와 동일

위 문법으로 정규표현식을 만들면 됩니다.

regex_match

정규표현식을 생성하여 매칭하는 방법에 대해 알아보겠습니다. 어느 문자열에서 정해진 패턴이 매치 되는지 안되는지 알아볼 때 regex_match 함수를 사용합니다.

예를 들어 어느 프로그램에서 아래 패턴과 같은 이름을 가지는 로그 파일을 매일 저장한다고 가정합니다.

  • (년)-(월)-(일).log
  • 년도는 4자리 숫자
  • 월은 2자리 숫자
  • 일을 2자리 숫자

위 패턴에 매칭하는 문자열은 아래 같은 문자열이여야 합니다.

  • 2023-12-01.log
  • 2023-12-02.log
  • 2023-12-04.log

어쨌든 위와 같은 패턴으로 정규표현식을 만들면 아래와 같습니다.

std::regex regex_filename_log(R"(\d{4}-\d{2}-\d{2}[.]log)");

아래는 위 패턴을 가지고 regex_match 함수를 사용한 예 입니다.

std::regex regex_filename_log(R"(\d{4}-\d{2}-\d{2}[.]log)");

// match 예제 1
std::string strFilename = "2023-04-15.txt"; // 매치 안됨. log가 아님
if (std::regex_match(strFilename, regex_filename_log))
    std::cout << "Matched" << std::endl;
else
    std::cout << "Not matched" << std::endl;

strFilename = "23-04-15.log"; // 매치 안됨. 년이 4글자가 아님
if (std::regex_match(strFilename, regex_filename_log))
    std::cout << "Matched" << std::endl;
else
    std::cout << "Not matched" << std::endl;

// match 예제 2
strFilename = "2023-01-10.log"; // 매치 됨
if (std::regex_match(strFilename, regex_filename_log))
    std::cout << "Matched" << std::endl;
else
    std::cout << "Not matched" << std::endl;

위 코드를 실행하면 아래와 같이 출력합니다.

Not matched
Not matched
Matched
regex_match - Grouping

매치되는 파일 이름 중에 년, 월, 일만 따로 뽑고 싶을 경우가 있을 수 있습니다.
이럴 때엔 소괄호'()’를 사용하여 그룹핑 하여 매치하면 됩니다.

std::regex regex_group_filename_log(R"((\d{4})-(\d{2})-(\d{2})[.]log)"); // Grouping 하려면 () 사용
std::smatch match_filename;
if (std::regex_match(strFilename, match_filename, regex_group_filename_log))
{
    std::cout << "match string: " << match_filename[0].str() << std::endl;
    std::cout << "year: " << match_filename[1].str() << std::endl;
    std::cout << "month: " << match_filename[2].str() << std::endl;
    std::cout << "day: " << match_filename[3].str() << std::endl;
}

std::smatch에 그룹핑 한 정보가 담겨있습니다. 출력 결과는 아래와 같습니다.

match string: 2023-01-10.log
year: 2023
month: 01
day: 10
regex_search

문자열 내에서 패턴을 검색하는 방법은 regex_search 함수를 사용하면 됩니다. 해당 함수를 사용하면 문자열 내에서 패턴을 검색 합니다.

아래는 샘플 코드입니다.

// 검색 예제 1
strFilename = "파일 이름: 2023-01-10.log";
if (std::regex_search(strFilename, match_filename, regex_filename_log))
    std::cout << "Searched: " << match_filename.str() << std::endl;

// 검색 예제 2
strFilename = "파일 목록: 2023-04-15.log,2022-09-28.txt,2023-01-10.log,2022-06-22.log,2023-11-05.txt,2022-03-17.txt";
std::cout << "Searched List" << std::endl;
while (std::regex_search(strFilename, match_filename, regex_filename_log))
{
    std::cout << match_filename.str() << std::endl;
    strFilename = match_filename.suffix();
}

위 코드를 실행하면 아래처럼 출력합니다.

Searched: 2023-01-10.log

Searched List
2023-04-15.log
2023-01-10.log
2022-06-22.log

regex_match와 크게 다르지 않고 문법도 어렵지 않습니다.

regex_iterator

regex_iterator는 regex_search를 편하게 하기 위한 함수입니다. 사용법은 아래와 같습니다.

strFilename = "파일 목록: 2023-04-15.log,2022-09-28.txt,2023-01-10.log,2022-06-22.log,2023-11-05.txt,2022-03-17.txt";
std::sregex_iterator start = std::sregex_iterator(strFilename.begin(), strFilename.end(), regex_filename_log);
std::sregex_iterator end = std::sregex_iterator();
while (start != end)
{
    std::cout << start->str() << std::endl;
    ++start;
}

sregex_iterator는 std::string에 대한 반복자를 사용하는 regex_iterator입니다. 위 코드를 실행하면 아래와 같이 출력합니다.

2023-04-15.log
2023-01-10.log
2022-06-22.log
regex_replace

regex_replace는 매치된 패턴을 치환하는 함수입니다.

예를 들어 문자열 내에서 년-월-일 형식으로 되어있는 파일 이름을 일-월-년 형식으로 바꿔야 하는 경우가 있습니다.
그럴 때엔 위에서 알아본 그룹핑과 regex_replace 함수를 사용하면 편하게 가능합니다.

우선 치환하려고 하는 부분을 그룹핑 해줍니다.

(\d{4})-(\d{2})-(\d{2})[.](\w+)

앞에서 년, 월, 일, 파일 확장자 순서대로 그룹핑이 되었는데 아래와 달러 문자에 숫자를 붙여 문자열에 명시하면 regex_replace에서 역참조하여 치환해줍니다.

$1, $2, $3, $4

코드로 보면 다음과 같습니다.

 strFilename = "파일 목록: 2023-04-15.log,2022-09-28.txt,2023-01-10.log,2022-06-22.log,2023-11-05.txt,2022-03-17.txt";
 std::regex regex_replace_filename_log(R"((\d{4})-(\d{2})-(\d{2})[.](\w+))"); // Grouping 하려면 () 사용
 std::smatch match;

 std::string strFilename_New = std::regex_replace(strFilename, regex_replace_filename_log, "$3-$2-$1.$4"); // $1: 년, $2: 월, $3: 일, $4: 파일 확장자
 std::cout << strFilename << std::endl;
 std::cout << strFilename_New << std::endl;

아래는 실제 출력 결과입니다.

파일 목록: 2023-04-15.log,2022-09-28.txt,2023-01-10.log,2022-06-22.log,2023-11-05.txt,2022-03-17.txt
파일 목록: 15-04-2023.log,28-09-2022.txt,10-01-2023.log,22-06-2022.log,05-11-2023.txt,17-03-2022.txt
regex_replace

전체 코드는 아래와 같습니다.

#include <iostream>
#include <regex>
#include <vector>

int main()
{
    ////////////////////////////// regex_match
    std::cout << std::endl << "////////////////////////////// regex_match" << std::endl;
    std::regex regex_filename_log(R"(\d{4}-\d{2}-\d{2}[.]log)");

    // match 예제 1
    std::cout << "ex 1)" << std::endl;
    std::string strFilename = "2023-04-15.txt"; // 매치 안됨. log가 아님
    if (std::regex_match(strFilename, regex_filename_log))
        std::cout << "Matched" << std::endl;
    else
        std::cout << "Not matched" << std::endl;

    strFilename = "23-04-15.log"; // 매치 안됨. 년이 4글자가 아님
    if (std::regex_match(strFilename, regex_filename_log))
        std::cout << "Matched" << std::endl;
    else
        std::cout << "Not matched" << std::endl;

    // match 예제 2
    std::cout << "ex 2)" << std::endl;
    strFilename = "2023-01-10.log"; // 매치 됨
    if (std::regex_match(strFilename, regex_filename_log))
        std::cout << "Matched" << std::endl;
    else
        std::cout << "Not matched" << std::endl;

    // match 예제 3 - Grouping
    std::cout << "ex 3)" << std::endl;
    std::regex regex_group_filename_log(R"((\d{4})-(\d{2})-(\d{2})[.]log)"); // Grouping 하려면 () 사용
    std::smatch match_filename;
    if (std::regex_match(strFilename, match_filename, regex_group_filename_log))
    {
        std::cout << "match string: " << match_filename[0].str() << std::endl;
        std::cout << "year: " << match_filename[1].str() << std::endl;
        std::cout << "month: " << match_filename[2].str() << std::endl;
        std::cout << "day: " << match_filename[3].str() << std::endl;
    }

    ////////////////////////////// regex_search
    std::cout << std::endl << "////////////////////////////// regex_search" << std::endl;
    // 검색 예제 1
    std::cout << "ex 1)" << std::endl;
    strFilename = "파일 이름: 2023-01-10.log";
    if (std::regex_search(strFilename, match_filename, regex_filename_log))
        std::cout << "Searched: " << match_filename.str() << std::endl;

    // 검색 예제 2
    std::cout << "ex 2)" << std::endl;
    strFilename = "파일 목록: 2023-04-15.log,2022-09-28.txt,2023-01-10.log,2022-06-22.log,2023-11-05.txt,2022-03-17.txt";
    std::cout << "Searched List" << std::endl;
    while (std::regex_search(strFilename, match_filename, regex_filename_log))
    {
        std::cout << match_filename.str() << std::endl;
        strFilename = match_filename.suffix();
    }

    ////////////////////////////// regex_iterator
    std::cout << std::endl << "////////////////////////////// regex_iterator" << std::endl;

    // interator 예제 1
    std::cout << "ex 1)" << std::endl;
    strFilename = "파일 목록: 2023-04-15.log,2022-09-28.txt,2023-01-10.log,2022-06-22.log,2023-11-05.txt,2022-03-17.txt";
    std::sregex_iterator start = std::sregex_iterator(strFilename.begin(), strFilename.end(), regex_filename_log);
    std::sregex_iterator end = std::sregex_iterator();
    while (start != end)
    {
        std::cout << start->str() << std::endl;
        ++start;
    }

    ////////////////////////////// regex_replace
    std::cout << std::endl << "////////////////////////////// regex_replace" << std::endl;
    std::cout << "ex 1)" << std::endl;
    strFilename = "파일 목록: 2023-04-15.log,2022-09-28.txt,2023-01-10.log,2022-06-22.log,2023-11-05.txt,2022-03-17.txt";
    std::regex regex_replace_filename_log(R"((\d{4})-(\d{2})-(\d{2})[.](\w+))"); // Grouping 하려면 () 사용
    std::smatch match;

    std::string strFilename_New = std::regex_replace(strFilename, regex_replace_filename_log, "$3-$2-$1.$4"); // $1: 년, $2: 월, $3: 일, $4: 파일 확장자
    std::cout << strFilename << std::endl;
    std::cout << strFilename_New << std::endl;

	return 0;
}