regex(정규 표현식식)
정규 표현식
- 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_search
- 앞에서
regex_match
를 사용하며 전체 문자열에 대해서 원하는 패턴과 일치하는지 확인하였다. - 이번에는 원하는 문자 패턴에대해 문자열 일부분에 대해 검색하는 작업이 가능한가?
- 문자열에서 원하는 패턴을 검색하는 일은
regex_search
를 사용하면 된다. - 첫 번째 인자는 검색할 문자열, 두 번째는 패턴과 일치된 문자열을 저장할
match
객체, 마지막 인자는 정규 표현식 객체- 매칭되는 문자열이 있다면
regex_search
가true
를 리턴한다.
- 매칭되는 문자열이 있다면
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-circle
를1-sk-circle
으로- 뒤 부분의
sk-circle
을 날리면서 숫자를 앞으로 옮겨야 한다.
- 이때는 여러개의 캡처그룹을 사용하면 되는데, 캡처 그룹이 여러개 일 때 어떤 캡처 그룹이 어떤 back reference 인지 파악할 수 있는가?
- 캡처 그룹
()
괄호가 열린 순서대로$1
,$2
번호가 매겨진다.(sk-circle(\d) sk-circle)
이와 같을 때$2
는/d
에 해당하게 된다.
- 캡처 그룹
- 따라서 치환될 패턴은
$2-sk-circle
가 된다.
개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!
댓글남기기