A Tour of C++ : 9장 문자열과 정규 표현식
.
9. 문자열과 정규 표현식
9.1 소개
C++ 표준 라이브러리 std::string 타입. string_view 타입을 이용하면 string, char[] 상관없이 문자열 시퀀스 조작 가능. 정규표현식 제공.
9.2 문자열
string 타입의 활용.
s1 = s1 + ‘\n’;
s2 += ‘\n’;
string name = “Niels Stroustrup’;
string s = name.substr(6,10); // s = “Stroustrup”;
name.replace(0, 5, “nicholas”); // name = “nicholas Stroustrup”;
name[0] = toupper(name[0]); // name = “Nicholas Stroustrup”;
if ( name == “NicholasStroustrup”) // 허용
{ ... }
printf(“printf를 선호한다면: %s\n”, name.c_str());
auto s = “Cat”s; // std::string 으로 할당
auto p = “Dog”; // C 스타일 문자열: const char* 로 할당
9.2.1 string 구현
짧은 문자열 최적화를 바탕으로 구현. 짧은 문자열 값은 string 객체에 저장, 더 긴 문자열은 자유저장소에 저장.
9.3 문자열 뷰
문자열을 함수로 전달할 때, 복잡성 해결을 위한 표준 라이브러리 string_view 제공. 문자열 시퀀스를 나타내는 포인터, 길이의 쌍. string_view 가 문자열을 직접 소유하지 않음. 포인터, 참조, 반복자와 유사.
string_view : {begin(), size()}
string cat(string_view sv1, string_view sv2)
{
string res(sv1.length()+sv2.length());
char* p = &res[0];
// 복사 방법 1
for (char c: sv1)
*p++ = c;
// 복사 방법 2
copy(sv2.begin(), sv2.end(), p);
return res;
}
위 함수의 장점
- 다양한 타입의 문자 시퀀스에 적용 가능.
- C스타일 문자열을 인자로 받을 때 임시 string 객체 생성 안 됨.
- 부분 문자열 쉽게 전달.
cat( “Edward”, “Stephen”sv );
“...”sv를 쓰려면 아래를 선언해야.
using namespace std::literals::string_view_literals; // 5.4.4 절
string_view를 반환할 때는, 포인터와 유사함을 기억해야.
9.4 정규 표현식
표준 라이브러리 <regex>, 정규 표현식을 지원하기 위한 std::regex클래스와 함수들.
regexpat {R”(\w{2}\s*\d{5}(-\d{4})*)”}; // 미국 우편번호 패턴 : XXddddd-dddd와 그 변종.
\w{2}\s*\d{5}(-\d{4})*
두 문자로 시작 \w{2}
선택적 공뱅 \s*
숫자 다섯 개 \d{5}
선택적 대시와 숫자 네 개 -\d{4}
로우 문자열 리터럴 사용 R”( ... )”
백슬래시와 큰 따옴표를 직접 사용할 수 있어짐.
로우 문자열 리터럴이 아닌 일반 문자열이었다면 백슬래시를 두 개씩 써줘야 함.
<regex> 표준 라이브러리
- regex_match() : 문자열에 매칭
- regex_search() : 매칭 문자열 찾기
- regex_replace() : 데이터스트림에서 매칭 찾기
- regex_iterator() : 매치와 부분 매치를 찾아 순회
- regex_token_iterator() : 매치되지 않는 부분 순회
9.4.1 검색
스트림에서 패턴 찾기
int lineno = 0;
for (string line; getline(cin, line); ) { // 버퍼 line 에 읽기
++lineno;
smatch matches; /// 매치된 문자열 저장
if (regex_search(line, matches, pat)) // line 에서 pat 찾기
cout<<lineno<< “:” << matche[0] << ‘\’;
9.4.2 정규 표현식 표기법
regex 는 다양한 정규표현식 표기법을 인식. 여기에선 ECMAScript 의 ECMA 표준을 사용.
정규표현식 특수문자
. 모든 문자 하나(와일드카드)
[, ] 문자 클래스 시작, 끝
{, } 카운팅 시작, 끝
(, ) 그룹핑 시작, 끝
\ 다음 문자를 특수문자로 인식
* 0번 이상(접미 연산)
+ 1번 이상(접미 연산)
? 선택적(0번이나 1번)(접미 연산)
| 대안(또는 (or))
^ 행의 시작; 반전
$ 행의 끝
Q. 행의 시작이 0개 이상의 A, 다음에 1개 이상의 B, 그 뒤에 C가 선택작으로 등장
A. ?A?B+C?$
ex) AAAAAAAAAAABBBBBBBBC
ex) BC
ex) B
매치되지 않는 예
ex) AAAAA // B가없다
ex) AAABC // 맨 앞이 공백
ex) AABBCC // C가 너무 많음
(괄호에 둘러싸인) 패턴의 일부분을 부분 패턴이라고 함.
\d+?\d+ // 부분패턴 없음
\d+(?\d+) // 부분패턴 하나
(\d+)(?\d+)
반복
{n} 정확히 n번
{n,} n번 이상
{n,m} n번 이상, m번 이하
* 0번 이상, {0,}과 동일
+ 1번 이상, {1,}과 동일
? 선택적(0또는 1), {0, 1}과 동일
EX> A{3}B{2,4}C?
매칭되는 문자열
AAABBC
AAABBB
애칭되지 않는 문자열
AABBC // A가 너무 적음
AAABC // B가 너무 적음
AAABBBBBCCC // B가 너무 많음
반복 표기( ?, *, +, { } ) 다음 접미사 ?를 사용하면 패턴 매처를 지연시키거나 non-greedy로 만든다. 즉, 패턴을 발견했을 때 가장 긴 매치 대신 가장 짧은 매치를 찾는다.
기본적으로는 패턴 매처가 항상 가장 긴 매치를 찾음. Max Munch Rule(크게 베어 먹기 규칙).
ababab 아이디 활용 중.
패턴 (ab)+ 는 ababab 전체에 매치
패턴 (ab)+? 는 첫 번째 ab만 매치.
문자 분류, 문자 클래스
almun 알파벳이나 숫자
alpha 알파벳
blank 행 구분자를 제외한 공백 문자
cntrl 제어 문자
d 10진 숫자
digit 10진 숫자
graph 그래픽 문자
lower 소문자
print 출력 가능 문자
punct 문장 부호
s 공백 문자
space 공백 문자
upper 대문자
w 단어 문자(알파벳과 숫자, 밑줄)
xdigit 16진 숫자
문자 클래스는 이름을 [: :] 로 둘러싸야 한다.
ex)
\d [[:digit:]] // 모든 10진 숫자에 매치
\s [[:space:]]
\w [_[:alnum:]]
\D [^[:digit:]]
\S [^[:space:]]
\W [^_[:alnum:]]
언어 자체 제공 정규 표현식
/l : 소문자
/w : 대문자
/L : /l 이 아닌 것
/U : /u
축약어보다는 문자열 클래스 이름 정의가 좋음. 모든 가능성에 대비.
C++의 식별자 패턴.
-> 맨 앞에 밑줄이나 문자가 하나, 선택적으로 알파벳이나 숫자, 밑줄 구성.
틀린 예
[:alpha:][:alpha:]* // 틀림 : 문자열 “:alpha” 중 하나가 앞에 옴.
[[:alpha:]][[:alpha:]]* // 틀림 : 밑줄이 없음(‘_’는 alpha가 아님) alpha 는 알파벳만.
([[:alpha:]]|_)[[:alnum:]]* // 틀림 : 밑줄은 alnum에 속하지 않음. _ 를 추가해줘야함.
([[:alpha:]]|_)([[:alnum:]]|_) // OK : 그러나 지저분. 길다.
[[:alpha:]_][[:alnum:]_]* // OK : 문자클래스 안에 밑줄이 있음
[_[:alpha:]][_[:alnum:]]* // OK
[_[:alpha:]]\w* // OK : \w 는 [_[:alnum:]] 과 동일
간단 버전의 regex_match() 활용예
bool is_identifier(const string& s)
{
regex pat {“[_[:alpha:]]\\w?” };
return regex_match( s, pat );
}
일반 문자열 리터럴에서 백슬래시는 두 번 써야함. R”...” 로우 문자열 리터럴을 쓰면 하나만 써도 됨.
예제 패턴
Ax* // A, Ax, Axxxx. x가 0개 이상.
Ax+ // Ax, Axxx. | A는 매칭 안 됨. x가 1개 이상.
\d* \d // 1-2, 12. | 1--2는 매칭 안 됨
\w{2}?\d{4,5} // Ab-1234, XX-54321, 22-5432. | 숫자는 \w에 포함 됨.
(\d*:)?(\d+) // 12:3, 1:23, 123, :123. | 123:은 매칭 안 됨.
(bs|BS) // bs, BS. | bS는 매칭 안 됨.
[aeiouy] // a, o, u. | 영어 모음, x는 매칭 안 됨.
[?aeiouy] // x, k | 영어 모음이 아닌 것, e는 매칭 안 됨
[a?eiouy] // a, ?, o, u | 영어 모음 또는 ?
group(부분 패턴)은 sub_match로 표현될 수 있으며, 괄호로 구분.
부분패턴 정의 없이 괄호가 필요하면 ( 대신 (?: 을 사용.
(\s|:|,)?(\d*) // 공백 문자나 콜론, 쉼표가 0개 이상 나오고, 0개 이상의 숫자가 뒤따름.
(?:\s|:|,)?(\d*)
정규 표현식 그룹핑 예제
\d*\s\w+ 그룹 없음. 부분 패턴도 없음.
(\d*)\s(\w+) 두 그룹
(\d*)(\s(\w+))+ 두 그룹(그룹 중첩 X)
(\s?\w?)+ 한 그룹, 하나 이상의 부분 패턴. 마지막 부분 패턴만 sub_match에 저장.
<(.*)>(.*)</\1> 세 그룹. \1은 ‘그룹1과 같음’을 의미. XML 파싱에 유용한 패턴.
정규표현식은 좀 더 시간을 들여 연습하고 암기해야한다.
9.4.3 반복자
패턴 매칭하며 시퀀스 순회를 하고싶다면, regex_iterator 를 정의.
예) 공백문자로 구분된 단어를 모두 출력.
void test()
{
string input = “aa as; asd ++e^asdf asdfg”;
regex pat {R”(\s+(\w+))”};
for (sregex_iterator p(input.begin(), input.end(), pat); p != sregex_iterator{}; ++p)
cout << (*p)[1] << ‘\n’;
}
출력
as
asd
asdfg
regex pat {R”(\s+(\w+))”}; 를 단순화 해서 regex pat {R”((\w+))”}; 라면
출력
aa
as
asd
e
asdf
asdfg
regex_iterator에 쓰기를 수행할 순 없다.
9.5 조언
[2] C스타일 문자열 함수 대신 string 연산 사용하자.
[4] string 을 값으로 반환하자 (9.2, 9.2.1)
[5] 부분 문자열 읽을 때는 substr(), 쓸 때는 replace()
[8] 최적화된 성능을 원하면 at() 대신 반복자 or [] 를 사용. (9.2)
[9] string 입력은 오버플로우 되지 않는다. (9.2, 10.3)
[10] string 을 C 스타일로 표현해야 할 경우에만 c_str() 을 사용. (9.2)
[11] 문자열을 수치로 변환할 때는 stringstream 이나 (to<X>같은) 제네릭 값 추출 함수를 사용.
[13] 표준 라이브러리 string 타입으로 만들고 싶은 문자열 리터럴에는 접미가 s 를
[14] 여러 방식으로 저장된 문자 시퀀스를 읽어야 한다면 string_view 를 인자로
[15] 써야 한다면 std::string_span
[16] string_view 는 크기를 동반한 포인터로 생각.
[17] string_view 타입으로 만들고 싶은 문자열 리터럴에는 접미사 sv
[18] 정규 표현식 관례적으로 대부분의 경우 regex 를 이용.
[19] 패턴 표현 시 로우 문자열 리터럴을 사용하자. R”...”
[20] 입력 전체에 매칭하려면 regex_match()
[21] 입력 스트림에 포함된 입력 검색 regex_search()
[23] 기본 정규 표현식 표기법은 ECMAScript 따름
[26] ?를 이용하여 패턴에 지연된(lazy) 매칭 적용
.
.
.