KoreanFoodie's Study

C++ 기초 개념 17-2 : C++ 정규 표현식 <regex> 사용하기 본문

Tutorials/C++ : Beginner

C++ 기초 개념 17-2 : C++ 정규 표현식 <regex> 사용하기

GoldGiver 2022. 5. 26. 16:28

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

전체 문자열 매칭하기

로그 파일을 추출하는 코드를 보자.

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

int main() {
  // 주어진 파일 이름들.
  std::vector<std::string> file_names = {"db-123-log.txt", "db-124-log.txt",
                                         "not-db-log.txt", "db-12-log.txt",
                                         "db-12-log.jpg"};
  std::regex re("db-\\d*-log\\.txt");
  for (const auto &file_name : file_names) {
    // std::boolalpha 는 bool 을 0 과 1 대신에 false, true 로 표현하게 해줍니다.
    std::cout << file_name << ": " << std::boolalpha
              << std::regex_match(file_name, re) << '\n';
  }
}

// 출력 결과
// db-123-log.txt: true
// db-124-log.txt: true
// not-db-log.txt: false
// db-12-log.txt: true
// db-12-log.jpg: false

인자를 지정하지 않았다면 디폴트로 std::regex::ECMAScript 가 들어가게 된다. 예를 들어 std::regex::icase 를 정달하면 대소문자를 구분하지 않게 된다. 특성을 추가할 때는 | 를 사용한다.

std::regex re("db-\\d*-log\\.txt", std::regex::grep | std::regex::icase);

참고로 정규 표현식의 성능이 중요할 경우에는 std::regex::optimize 를 추가적으로 전달할 수 있다. 이 경우 정규 표현식 객체를 생성하는데에는 시간이 좀 더 걸리지만 정규 표현식 자체를 사용하는 작업은 좀 더 빠르게 수행된다.

 

 

부분 매칭 뽑아내기

해당 조건을 만족하는 문자열에서 패턴 일부분을 뽑아내고 싶다면 어떻게 해야 할까? 예를 들어, 전화번호를 위한 regex 객체를 만들어 보자.

std::regex re("[01]{3}-\\d{3, 4}-\\d{4]", std::regex::optimize);

 

먼저, 0 또는 1 이 3개 나온 후, 숫자가 3~4개 나오고, 그 후 숫자가 4개 나오는 식일 것이다.

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

int main() {
  std::vector<std::string> phone_numbers = {"010-1234-5678", "010-123-4567",
                                            "011-1234-5567", "010-12345-6789",
                                            "123-4567-8901", "010-1234-567"};
  std::regex re("[01]{3}-\\d{3,4}-\\d{4}");
  for (const auto &number : phone_numbers) {
    std::cout << number << ": " << std::boolalpha
              << std::regex_match(number, re) << '\n';
  }
}

// 출력 결과
// 010-1234-5678: true
// 010-123-4567: true
// 011-1234-5567: true
// 010-12345-6789: false
// 123-4567-8901: false
// 010-1234-567: false

만약 조건에 만족하는 전화번호 중에서 가운데 번호를 추출하고 싶다면 어떨까? 이 때는 캡쳐 그룹 (capture group) 을 사용하면 된다.

std::regex re("[01]{3}-(\\d{3, 4})-\\d{4}", std::regex::optimize);

위와 같이, ( ) 로 원하는 부분을 감싸게 된다면 해당 부분에 매칭된 문자열을 얻을 수 있다. 아래 코드를 보자.

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

int main() {
  std::vector<std::string> phone_numbers = {"010-1234-5678", "010-123-4567",
                                            "011-1234-5567", "010-12345-6789",
                                            "123-4567-8901", "010-1234-567"};
  std::regex re("[01]{3}-(\\d{3,4})-\\d{4}");
  std::smatch match;  // 매칭된 결과를 string 으로 보관
  for (const auto &number : phone_numbers) {
    if (std::regex_match(number, match, re)) {
      for (size_t i = 0; i < match.size(); i++) {
        std::cout << "Match : " << match[i].str() << std::endl;
      }
      std::cout << "-----------------------\n";
    }
  }
}

// 출력 결과
// Match : 010-1234-5678
// Match : 1234
// -----------------------
// Match : 010-123-4567
// Match : 123
// -----------------------
// Match : 011-1234-5567
// Match : 1234
// -----------------------

즉, 위에서 매칭된 문자열들을 match[i].str( ) 을 통해 접근할 수 있다. 참고로 위의 match 가 smatch 였으므로, match[i].str( ) 은 std::string 이 된다. 만약 match 가 cmatch 였다면 타입이 const char* 이 되었을 것이다!

 

 

원하는 패턴 검색하기

이제 문자열 일부 를 검색하는 작업을 해 보자. 예시로, HTML 문서에서 아래와 같은 태그만 읽을 것이다.

<div class="sk...">...</div>

위 조건을 만족하는 regex 객체는 다음과 같이 생성할 수 있다.

std::regex re(R"<div class="sk[\w -]*">\w*</div>");

실제 코드를 보면,

#include <iostream>
#include <regex>

int main() {
  std::string html = R"(
        <div class="social-login">
          <div class="small-comment">다음으로 로그인 </div>
          <div>
            <i class="xi-facebook-official fb-login"></i>
            <i class="xi-google-plus goog-login"></i>
          </div>
        </div>
        <div class="manual">
          <div class="small-comment">
            또는 직접 입력하세요 (댓글 수정시 비밀번호가 필요합니다)
          </div>
          <input name="name" id="name" placeholder="이름">
          <input name="password" id="password" type="password" placeholder="비밀번호">
        </div>
        <div id="adding-comment" class="sk-fading-circle">
          <div class="sk-circle1 sk-circle">a</div>
          <div class="sk-circle2 sk-circle">b</div>
          <div class="sk-circle3 sk-circle">asd</div>
          <div class="sk-circle4 sk-circle">asdfasf</div>
          <div class="sk-circle5 sk-circle">123</div>
          <div class="sk-circle6 sk-circle">aax</div>
          <div class="sk-circle7 sk-circle">sxz</div>
        </div>
  )";

  std::regex re(R"(<div class="sk[\w -]*">\w*</div>)");
  std::smatch match;
  while (std::regex_search(html, match, re)) {
    std::cout << match.str() << '\n';
    html = match.suffix();
  }
}

// 출력 결과
// <div class="sk-circle1 sk-circle">a</div>
// <div class="sk-circle2 sk-circle">b</div>
// <div class="sk-circle3 sk-circle">asd</div>
// <div class="sk-circle4 sk-circle">asdfasf</div>
// <div class="sk-circle5 sk-circle">123</div>
// <div class="sk-circle6 sk-circle">aax</div>
// <div class="sk-circle7 sk-circle">sxz</div>

먼저, regex_search 를 이용해 문자열에서 원하는 패턴을 검색한다. 검색된 문자열은 match 에 저장되고, match.str( ) 을 통해 std::string 으로 만들 수 있다. 

이때, 이전에 찾았던 패턴을 다시 뱉는 일을 방지하기 위해, html 을 업데이트 해 검색된 패턴 바로 뒤부터 다시 검색할 수 있도록 바꾸어 주어야 한다. 이때 html = match.suffix( ); 를 활용한다.

match.suffix( ) 를 하면 std::sub_match 객체를 리턴한다. sub_match 는 단순히 어떠한 문자열의 시작과 끝을 가리키는 반복자 두 개를 가지고 있다고 보면 된다. 이 때 suffix 의 경우, 원 문자열에서 검색된 패턴 바로 뒤부터, 이전 문자열의 끝까지에 해당하는 sub_match 객체를 리턴한다.

이 때 sub_match 클래스에는 string 으로 변환할 수 있는 캐스팅 연산자가 들어 있어 html 에 바로 대입하면 알아서 문자열로 잘 변환이 된다.

 

 

std::regex_iterator

std::regex_iterator 을 사용하면 더 간편하게 검색을 수행할 수 있다.

  std::regex re(R"(<div class="sk[\w -]*">\w*</div>)");
  std::smatch match;

  auto start = std::sregex_iterator(html.begin(), html.end(), re);
  auto end = std::sregex_iterator();

  while (start != end) {
  	std::cout << start->str() << std::endl;
  	++start;
  }

 

원하는 패턴 치환하기

마지막으로 std::regex_replace 를 통해 원하는 패턴의 문자열을 치환(replace) 하는 작업을 수행해 보자.

예를 들어, sk-circle1 같은 문자열을 1-sk-circle 로 바꿔보자.

  std::regex re(R"r(sk-circle(\d))r");
  std::smatch match;

  std::string modified_html = std::regex_replace(html, re, "$1-sk-circle");
  std::cout << modified_html;

std::regex_replace 에서 첫 번째로는 치환하고자 하는 문자열을, 두번째 인자는 정규 표현식 객체를, 마지막으로 치환 시에 어떤 패턴으로 바꿀지를 적어주면 된다.

이때, $1 라는 표현은, (\d) 의 캡쳐 그룹을 의미한다. (인덱스가 1이니까. 참고로 $0 은 전체 문자열일 것이다.)

만약 치환된 문자열을 생성하지 않고 그냥 stdout 에 출력하고 싶다면, 다음과 같이 하면 된다.

std::regex_replace(std::ostreambuf_iterator<char>(std::cout), html.begin(),
    html.end(), re, "$1-sk-circle");

 

중첩된 캡쳐 그룹

만약 아래와 같은 변환은 어떻게 해야 할까?

// 원본
<div class="sk-circle1 sk-circle">a</div>

// 치환
<div class="1-sk-circle">a</div>

위 경우는 뒤의 sk-circle 을 날려야 하므로,  두 개의 캡쳐 그룹이 필요하다.

실제 코드를 보자.

  std::regex re(R"r((sk-circle(\d) sk-circle))r");
  std::smatch match;

  std::string modified_html = std::regex_replace(html, re, "$2-sk-circle");
  std::cout << modified_html;

위에서, 캡쳐 그룹이 중첩 (괄호 안에 괄호가 있음) 되는데, 괄호가 열리는 순서대로 $1, $2 ... 가 된다고 생각하면 된다. 따라서 $2 는 (\d) 를 가리킬 것이다!

Comments