티스토리 뷰



C언어에서 문자열 처리는 자주 사용할 뿐만아니라 반드시 익숙해져 할 만큼 매우 중요한 요소입니다. memcpy, strcpy, strncpy 등의 함수로 단순히 문자열을 복사하기, memcmp, strcmp, strncmp로 문자열 비교하기, strstr, strchr과 같은 함수로 특정 문자열이나 문자 찾기까지 C 프로그래밍에서는 너무도 빈번하게 사용하는 함수들입니다. 이번 포스팅은 문자열에서특정 항목을 추출하는 다양한 요령을 다루어 볼까 합니다. 텍스트 파일을 분석하거나 파이프나 필터를 통해서 다른 응용 프로그램의 출력을 입력으로 해서 분석하거나 웹 조회 결과를 분석하는 등의 과정에서 특정 단어를 추출하거나 숫자 항목을 추출하는 등의 작업은 C 프로그래밍에서 피할 수 없는 숙명과 같은 작업입니다.

//고정형식의 문자열에서 필요한 항목 추출 1
char *str = "ALARM 2016-12-08 11:14:01";
char temp[32];

if (str[10] != '-' || str[13] != '-' || str[16] != ' ' ||
    str[19] != ':' || str[22] != ':' || str[25] != 0)
    printf("\n Invalid string %s", str);
printf("\nInput : %s", str);
memcpy(temp, &str[11], 2);
temp[2] = 0;
printf("\n월 : %d", atoi(temp)); 

첫번째 예제는 입력 자료가 고정된 형식으로 들어올 때 또는 구분자 없는 문자열에 적용할 수 있는 아주 단순한 방식입니다.  문자열의 특정 위치에 값이 들어 오기 때문에 항목을 나누는 구분자(Delimeter)의 존재 여부에 따라 입력의 정합성 검사를 수행할 수도 있습니다. 예제에서는 입력 검사의 하나로 '-'나 ':', ' '와 같은 구분자 뿐만아니라 C 문자열의 끝을 표시하는 널(Null, 0)문자의 존재 여부도 확인하는데 C언어에서는 문자열 끝을 널 문자로 표시하기 때문에 strcmp, strcpy와 같은 문자열 비교 및 복사 함수를 호출하면서 널 문자가 없는 문자열을 적용하지 않도록 주의해야 합니다. 많은 C 응용 프로그램의 런타임 오류는 이 과정에서 발생합니다. 문장열을 입력으로 해서 정수 값을 리턴하는 atoi() 함수의 경우에도 정확한 문자열을 건네주기 위해서 temp 문자열 끝에 널을 입력하는 과정을 거치고 있습니다.(temp[2] = 0;) 참고로 *str 변수에 입력한 "..." 초기 값에는 맨끝의 널 문자까지 포함됩니다.  이 방법은 단순하기는 하지만 문자열에 대한 유연한 대처가 어려워서 입력 스트링이 조금만 변형되어도 처리하지 못하는 단점이 있습니다.

//고정형식의 문자열에서 필요한 항목 추출 2
char *str = "ALARM 2016-12-08 11:14:01";
char temp[32];
int year, mon, day, hour, min, sec;


if (sscanf(str, "%5s %4d-%2d-%2d %2d:%2d:%2d", 
            temp, &year, &mon, &day, &hour, &min, &sec) != 7)
    printf("\n Invalid string %s", str);
else {
    printf("\nInput : %s", str);
    printf("\n월,분,문자열 : %d, %d, %s", mon, min, temp);
}

이번에는 sscanf 내장 함수를 사용하여 필요한 항목을 추출하는 방법으로 필요한 데이터 타입으로 변환해서 각 항목 데이터를 받을 수 있을 뿐만아니라 가변적인 입력에 대해서도 유연하게 대처할 수 있는 장점이 있습니다. sscanf(분석할 문자열, "문자열의 형식", 데이터를 받을 변수의 주소......);의 형식으로 호출합니다. scanf가 키보드(표준 입력)로 부터 입력을 받는다면 sscanf는 문자열을 키보드 입력처럼 받는 다는 차이가 있습니다. %s는 문자열, %d는 정수형을 의미하는 것처럼 printf 사용법과 크게 다르지 않습니다. %5s, %4d는 해당 항목의 최대 길이로 입력 문자열의 길이가 가변적인 것을 자동 인식한다는 것입니다. 예를 들어 "ALARM 2016-12-08 11:14:01" 입력이 "ALM 2016-12-8 11:14:1"으로 입력되어도 년월일시분초는 동일한 결과를 리턴합니다. sscanf를 사용할 때 주의할 점은 데이터를 받을 변수를 "입력 형식"의 항목 개수와 타입에 맞추어 "변수의 주소" 형태로 전달해야 하고 sscanf()의 리턴값을 통해 정상적인 자료 입력을 확인해야 합니다. sscanf()의 리턴값은 추출한 변수의 개수로 위의 예제처럼 정상적인 입력 여부를 입력 형식에서 지정한 모든 항목이 추출되었는지, 그 개수를 확인하여 기본 검사 합니다. 정수나 실수값에 대하여 atoi()등으로 별도로 변환할 필요도 없고 여러 항목을 한번에 추출할 수 있으니 편리한 장점이 있습니다. 물론 가변적이나마 입력 형식이 정해져 있을 때 사용할 수 있습니다.

//고정형식의 문자열에서 필요한 항목 추출 3
char *str = "ALARM 2016-12-08 11:1401";
char temp[32]="aaaaaaaaaaaaaaaaaa";
int year, mon, day, hour, min, sec;

printf("\nInput : %s", str);
printf("\nsscanf return : %d", 
    sscanf(str, "%5s %4d-%2d-%2d %2d:%2d:%2d", 
            temp, &year, &mon, &day, &hour, &min, &sec));
printf("\n월,분,문자열 : %d, %d, %s", mon, min, temp); 

위에서 언급한 sscanf의 장점외에도 위의 예제와 같이 sscanf는 문자열을 추출하면서 그 뒤에 자동적으로 널 문자를 삽입해주고(temp) 정확한 입력이 아닌 경우라도 분석 가능한 범위까지는 최대한 추출해주는 장점이 있습니다. 예제에서 "분초" 사이에 ':'문자가 없지만 분에 대한 형식이 %2d로 되어 있어 "1401"중에 14를 잘라서 분으로 인식했고 초는 형식에 맞지않아 추출되지 않았습니다.(sscanf 리턴 값이 7이 아니라 6)

// 구분자 기준으로 항목 추출
char *str = "ALARM 2016-12-08 11:14:01";
char temp[32], *pt1;
char *deli = " -:";

strcpy(temp, str);
pt1 = strtok(temp, deli);
while (pt1) {
    printf("\n%s", pt1);
    pt1 = strtok(NULL, deli);
}

이번에 사용할 방식은 strtok() 함수를 사용하는 것입니다. strtok(분석대상 문자열, 구분자문자열);의 형식으로 호출합니다. 구분자 문자열에 포함된 문자가 분석 대상 문자열에서 발견되면 해당 위치에 널 문자를 대신 입력해서 그 시작점을 리턴합니다. strtok가 널 문자를 구분자 대신에 입력하기 때문에 원본 문자열을 변형시킨다는 점에 유의해야 합니다. 혹여라도 분석이후에도 원본 문자열을 사용한다면 위의 예제처럼 별도로 버퍼에 저장해서 처리해야 합니다. 주의할 또 한가지는 하나의 문자열을 구분자로 잘라가면서 연속적으로 개별 항목을 추출하려면 두번째 strtok() 함수 호출 부터는 분석 대상 문자열을 NULL로 전달해야만 연속적인 추출을 할 수 있습니다. 첫번째 strtok()호출 시점에 시스템 공간에 분석 대상 문자열의 포인터를 저장해두고 NULL이 전달되면 앞서 호출 했던 문자열의 위치에서 잘라내기를 이어가는 형태입니다. 위의 예제에서는 구분자를 공백문자(' '), ':', '-' 세가지를 한번에 기술했는데 이 문자들중에 하나라도 있으면 해당 위치가 항목 구분의 기준점이 됩니다. 구분자가 여러개 붙어 있더라도 하나의 구분자로 간주됩니다.

// 위치검색으로 항목 추출
char *str = "ALARM 2016-12-08 11:14:01";
char *pt1, *pt2, temp[16];
int y = 0, m = 0, d = 0;

if ((pt1=strstr(str, " "))==NULL || (pt2=strstr(&pt1[1], " "))==NULL)
    printf("\n Invalid string %s", str);
else {
    memcpy(temp, ++pt1, pt2 - pt1);
    temp[pt2 - pt1] = 0;
    if ((pt1=strstr(temp, "-"))) {
        *pt1 = 0;
        y = atoi(temp);
        pt1++;
        if ((pt2=strstr(pt1, "-"))) {
            *pt2 = 0;
            m = atoi(pt1);
            pt2++;
            d = atoi(pt2);
        }
    }
    printf("\nInput : %s", str);
    printf("\n Y/M/D = %d/%d/%d", y, m, d);
}

마지막 예제는 strstr이나 strchr과 같은 검색 함수를 활용하는 방법입니다. 첫번째 if 문은 첫번째 공백문자를 찾고 찾은 경우(||, OR 연산이기 때문에 첫번째 조건이 False, 즉 공백 문자를 찾으면 다음 조건도 비교합니다) 찾은 위치 이후로(&pt1[1]) 두번째 공백 문자를 찾습니다. 공백으로 나누어진 날짜 블럭을 찾으면 버퍼 해당 블럭을 복사하고 널 문자를 붙여서 정상적인 문자열로 만듭니다. 여기서 확인하고 넘어 갈 것은 "포인터-포인터"의 포인터 연산은 정수 값으로 두 위치간의 차이, 즉 길이를 추출합니다. 날짜 블럭에서 "-"를 찾는 strstr() 호출을 통해서 구분자를 찾아 널 문자를 넣고 해당 문자열을 atoi()로 정수로 변화하는 과정도 참고해 볼만 합니다.

끝으로 PHP, 자바스크립트, 파이썬등의 최근 언어에서는 정규식(Regular expression)을 사용해서 보다 간편하게 문자열을 처리할 수 있지만 C언어의 기본 내장 함수가 아니기 때문에 사용하고 싶다면 관련 라이브러리를 활용하는 것도 방법입니다.


댓글
댓글쓰기 폼