정규 표현식

  • C++ 11 부터 표준에 포함된 정규 표현식(regular expression) 라이브러리
  • 정규 표현식은 문자열에서 패턴을 찾는데 사용하는데 다음 같은 경우에 매우 유용하게 사용된다.
    • 주어진 문자열이 주어진 규칙에 맞는지 확인할 때
    • 주어진 문자열에서 원하는 패턴의 문자열을 검색할 때
    • 주어진 문자열에서 원하는 패턴의 문자열로 치환할 때


  • 우선 정규 표현식에서 쓰는 표현의 의미는 아래와 같다.
^x // '^'은 문자열의 시작을 표현하며, x문자로 시작됨을 의미
x$ // '$'은 문자열의 종료를 표현하며, x문자로 종료됨을 의미
.x // '.'은 개행문자 \n을 제외한 다른 모든 문자를 의미
x+ // '+'은 1회 이상 반복을 의미, x문자가 1번 이상 반복됨을 의미 ({1,}과 동일)
x* // '*'은 0회 이상 반복을 의미, x문자가 0번 이상 반복됨을 의미 ({0,}과 동일)
x? // '?'은 0 or 1개 문자 매칭 의미, x문자가 존재할 수도 있고 안할수도 있다는 의미 ({0,1}과 동일)
x|y // '|'은 or를 표현, x 또는 y가 나온다는 의미
(x) // '()'은 그룹을 표현, 괄호로 묶인 패턴을 의미 ((abc){3}와 같이 사용해서 abcabcabc를 검출하는데 쓰임)
x{n} // '{}'은 반복을 의미, x가 n번 반복됨을 의미
x{n,} // '{,}'은 반복을 의미, x가 n번 이상 반복됨을 의미
x{n,m} // '{}'은 반복을 의미, x가 n번 이상 m번 이하로 반복됨을 의미
[xy] // '[]'은 x또는 y를 찾는다는 의미, [a-z0-9]이면 알파벳 소문자 또는 숫자를 찾는다는 의미
[^xy] // '[^]'은 not을 의미, x 및 y 를 제외하고 찾는다는 의미
[a-z] // '[-]'은 a ~ z 를 찾는다는 의미

\d // '\d'은 digit으로 숫자를 의미
\D // '\D'은 not digit으로 숫자를 제외하고 나머지 다른 문자를 의미
\s // '\s'은 space로 공백문자를 의미
\S // '\S'은 not space로 공백문자를 제외한 나머지 다른 문자를 의미
\t // '\t'은 tap을 의미
\w // '\w'은 알파벳 대문자,소문자와 숫자를 의미, [A-Za-z0-9]을 의미
\W // '\W'은 not \w, 즉 \w를 제외한 특수문자를 의미

(?:) // 캡쳐하지 않는 그룹 생성


전체 문자열 매칭하기 - regex_match

  • 예제로 알아보자 ( 예를 들어 서버 관리를 하는데 시간 마다 로그 파일을 생성한다. )
    • 해당 로그 파일은 db-(시간)-log.txt 형태로 생성된다.
  • 여러가지 파일이 있는 폴더에서 위의 파일만 읽어낼 수 있을까?
  • 위의 내용을 참고하여 표현하면 정규 표현식은 db-\d*-log\.txt 이다.
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::cout << file_name << ": " << std::boolalpha << std::regex_match(file_name, re) << '\n';
}
  • 정규 표현식을 사용하기 위해 정규 표현식 객체를 정의해야 한다. std::regex re("db-\\d*-log\\.txt");
    • 참고로 정규 표현식 문법의 종류와, 정규 표현식을 처리하는 엔진 역시 여러가지 종류가 있고, 추가적으로 생성자에 인자를 전달할 수 있다
    • grep 정규 표현식을 쓰고 싶다면 std::regex re("db-\\d*-log\\.txt", std::regex::grep); 처럼 전달하면 된다.
  • 만약에 인자를 지정하지 않았다면 디폴트로 std::regex::ECMAScript 가 들어간다.
  • 또 문법 이외 몇 가지 특성도 추가가능한데, 다음과 같이 전달하면 대소 문자를 구분하지 않게 된다.
    • std::regex re("db-\\d*-log\\.txt", std::regex::grep | std::regex::icase); 처럼 추가할 수 있다.
    • 특성을 추가하는 방법은 | 으로 연결하면 된다.


  • std::regex_match(file_name, re) 전달된 문자열과 정규 표현식 객체를 비교하여 표현식과 완전히 매칭이 된다면 true를 리턴한다.
    • 매칭된다는 것은 정규 표현식의 패턴에 부합한다는 의미이다.


문자열의 원하는 부분을 뽑아서 사용 할 수 있는가? - capture group(캡처 그룹)

  • regex_match함수로 패턴과 일치하는지 확인했는데, 문자 일부분만 체크하는 방법이 있을까?
    • capture group(캡처 그룹)
  • 정규 표현식 객체에서 표현식에서 원하는 부분이 있으면 ()로 감싸면 된다.


  • 전화번호에서 가운데 번호를 추출하고 싶다면 std::regex re("[01]{3}-(\\d{3,4})-\\d{4}"); 이렇게 표현식을 만들 수 있다.
    • 이제 원하는 부분이 어디인지 표현을 했는데 이걸 어떻게 가져오냐??


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";
    }
  }
}
  • std::smatch 함수는 매칭된 결과를 string으로 반환해준다.
    • 그 외에도 const char* 로 돌려주는 cmatch 가 있다.
  • 이 때 표현식 객체와 패턴이 일치하면 매칭된 결과가 저장된다.
  • 만약에 정규 표현식 안에 () 가 여러개 있다면 for 문을 통해 순차적으로 접근할 수 있다.


  • 앞에서 regex_match를 사용하며 전체 문자열에 대해서 원하는 패턴과 일치하는지 확인하였다.
  • 이번에는 원하는 문자 패턴에대해 문자열 일부분에 대해 검색하는 작업이 가능한가?


  • 문자열에서 원하는 패턴을 검색하는 일은 regex_search 를 사용하면 된다.
  • 첫 번째 인자는 검색할 문자열, 두 번째는 패턴과 일치된 문자열을 저장할 match 객체, 마지막 인자는 정규 표현식 객체
    • 매칭되는 문자열이 있다면 regex_searchtrue를 리턴한다.
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();
  }
}
  • 한 가지 문제는 while문에서 계속 찾았던 패턴을 계속 검색한다는 것이다.
  • 그래서 해당 문자열을 업데이트 해서 검색된 패턴 바로 뒤 부터 다시 검색하게 만들어야 한다.


  • match.suffix()
    • 해당 함수를 사용하면 std::sub_match 객체를 리턴한다.
    • std::sub_match는 단순히 어떠한 문자열의 시작과 끝을 가리키는 반복자 2개를 가지고 있다.
    • 이 때 suffix의 경우 원 문자열에서 검색된 패턴 바로 뒤 부터, 문자열의 끝까지 해당하는 sum_match를 리턴한다.
      • sub_match 클래스는 string으로 변환하는 캐스팅 연산자가 검색하는 문자열에 캐스팅되어 대입된다.
        • 그래서 검색된 문자열 다음 문자 부터 다시 검색을 시작한다.


std::regex_iterator

  • 정규 표현식의 반복자를 사용하면 좀 더 편리하게 검색할 수 있다.
    • 정규 표현식으로 매칭된 문자열들에 대한 반복자이다.
  • 주어진 문자열에서 정규 표현식으로 매칭된 문자열들을 쭈르륵 뽑아낼 수 있다.
  • 위 예시 코드로 얘를 들자면
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;
}


  • 어떻게 반복자를 선언하냐??
  • auto start = std::sregex_iterator(html.begin(), html.end(), re); 생성자 호출함으로 생성할 수 있다.
    • 1, 2번째 인자로 검색할 문자열의 시작과 끝을 전달하고, 마지막 인자는 정규 표현식 객체를 전달하면 된다.
    • 참고로 std::sregex_iterator 우선 s가 붙어있는데 string을 사용하는 반복자이다.


  • regex_iterator는 처음 생성될 때와, ++ 연산자로 증감 될 때마다 regex_search 를 통해 초기에 주어진 문자열 범위 내에서 패턴에 맞는 문자열을 검색한다.
  • 또한 * 연산자를 통해서 역참조 하게 된다면 현재 검색된 패턴에 대한 match_results 객체를 얻어낼 수 있습니다.


원하는 패턴 치환하기 - std::regex_replace

  • 원하는 패턴의 문자열을 다른 문자열로 치환하는 작업은 std::regex_replace로 할 수 있다.
  • 예를 들어서 html 에서 sk-circle1 과 같은 문자열을 1-sk-circle 로 바꿔보는 정규 표현식을 생각해보자.
    • 이를 위해서 먼저 sk-circle1 과 같은 형태를 어떤 regex 로 매칭하기 위해 sk-circle\d 로 표현할 수 있다.
    • 그 다음에, 이를 어떠한 형태로 치환할지를 생각해야 한다.
    • 간단하게 숫자-sk-circle로 해야 하는데, 캡쳐 그룹을 이용하면 간단하다.
      • 먼저 sk-circle(\d)를 통해서 숫자에 해당하는 부분을 첫 번째 캡쳐그룹으로 만든다.
      • 그 다음에, 치환을 할 때 첫 번째 캡쳐 그룹을 표현하기 위해 $1 라고 명시할 수 있다. (이와 같은 요소를 back reference 라고 부른다.)
      • 따라서 $1-sk-circle과 같이 표현할 수 있다.


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

std::string modified_html = std::regex_replace(html, re, "$1-sk-circle");
std::cout << modified_html;
  • regex_replace 로 문자열을 치환하고 싶다면,
    • 첫 번째 인자로 치환하고자 하는 문자열을, 두 번째 인자로 정규 표현식 객체를, 마지막으로 치환 시에 어떠한 패턴으로 바꿀 지 전달한다.


  • 이번에는 다음과 같이 치환하고 싶다고 가정하자.
    • sk-circle1 sk-circle1-sk-circle 으로
    • 뒤 부분의 sk-circle을 날리면서 숫자를 앞으로 옮겨야 한다.
  • 이때는 여러개의 캡처그룹을 사용하면 되는데, 캡처 그룹이 여러개 일 때 어떤 캡처 그룹이 어떤 back reference 인지 파악할 수 있는가?
    • 캡처 그룹 () 괄호가 열린 순서대로 $1, $2 번호가 매겨진다.
      • (sk-circle(\d) sk-circle) 이와 같을 때 $2/d에 해당하게 된다.
  • 따라서 치환될 패턴은 $2-sk-circle가 된다.


공부한 내용 복습

개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!

카테고리:

업데이트:

댓글남기기