MenuIcon

Owl-Networks Archive

LoginIcon

Perl : 일정한 길이로 한글 문자열 자르기 (UTF-8)

| 분류: Perl | 최초 작성: 2009-01-04 06:19:41 |

1. 서론

웹에서 동작하는 게시판 등을 제작하면서 게시물 리스트 등을 뽑을 때에, 제목이 너무 길어지는 경우 게시판이 보기 싫게 되는 경우가 있습니다. 이런 경우에 제목이 몇 글자 이상이면 몇 글자까지만 보여주고 뒤는 생략해라.. 라는 옵션을 거의 모든 게시판에서 줄 수 있는데요.

이 경우 가장 간단한 것은 substr 문을 이용해서 기계적으로 xx글자(xx바이트)만 남기고 뒤쪽은 날리는 방법입니다. 그러나, 이것은 2바이트 문자인 한글(euc-kr)의 경우 기계적으로 몇 바이트를 자르는 경우에 문자열의 끝에 1바이트가 잘린 상태의 한글이 남아 보기 흉하게 되는 경우가 생깁니다. 따라서, 끝의 1바이트를 검사해서 그것이 한글의 일부인 경우에는 1바이트를 자르거나 더 붙여주는 처리를 하여야 합니다. 다음 웹 페이지는 이러한 경우에 사용할 수 있는 예시를 보여주고 있습니다.

그러나, 바이트 수가 다양해지는 UTF-8로 오면, 문자열 끄트머리의 한글 문자 처리 외에 고려해야 할 문제가 하나 더 늘어납니다.

2바이트 한글의 경우, 한글 한 글자의 폭은 대체로 영문 두 글자의 폭과 같고, 한 글자가 차지하는 바이트 수도 한글 한 글자(2바이트)와 영문 두 글자가 같았기 때문에, 문자 종류에 따른 글자폭을 고려할 필요 없이 단순히 바이트수를 기준으로 하여 잘라내기만 하면 대체로 균등한 길이의 문자열을 얻어낼 수 있었습니다. 그러나, 사실상 웹에서의 표준 인코딩이 된 UTF-8 인코딩의 경우에는 한글 한 글자가 차지하는 바이트 수가 3바이트이므로, 글자폭으로 계산한 경우 한글 1글자=영문 2글자이지만 바이트 수로 계산한 경우 한글 1글자=영문 3글자가 되어, 단순히 바이트 수를 기준으로 하여 문자열을 자를 경우 문자열 전체의 폭이 비슷하게 출력되리라는 보장이 없게 됩니다. 따라서 UTF-8 인코딩의 문자열을 웹에서 표시하는 CGI의 경우에는 바이트 수를 감안하여 문자열 끄트머리를 자르거나 더 붙여주는 이외에, 화면에 표시될 문자의 숫자까지도 신경을 써야 하게 됩니다.

물론, 스타일 시트를 이용한다면, 테이블의 style에 overflow:hidden; 을 지정해 줌으로써 굳이 문자열을 자르지 않아도 되도록 페이지를 작성하는 것도 가능합니다. 오히려 요즘은 굳이 문자열을 자르기보다는 이런 식으로 보이지 않게만 처리하는 경우도 많은 것 같네요.

2. 코드의 설계

이 점들을 모두 해결하기 위해서, 자문자답 식으로 나름대로 코드를 짜보았습니다. 일단 UTF-8 인코딩의 문자 저장 방식을 알 필요가 있습니다.

  • UTF-8 은 하나의 문자를 저장할 때 1바이트 또는 3바이트를 사용합니다.
  • 1바이트 문자는 아스키 코드와 완전히 호환됩니다. 따라서 영문이나 1바이트 특수문자 등은 기존 아스키 코드 체계와 완전히 동일합니다.
  • 그 외 문자들(한글 등)은 3바이트를 사용하며, 이 문자가 3바이트 문자라는 것을 표시하기 위해 1110 xxxx 10xx xxxx 10xx xxxx 의 구조를 가집니다.

따라서, 문자의 첫 4비트를 체크해서 그것이 0xxx 인지, 1110인지만 체크해서 문자열을 붙여나가는 방식을 사용하면 문자열이 잘리는 일 없이 무사히 자를 수 있게 됩니다.

또한, 문자열의 전체 폭의 문제는, 문자를 하나 붙일 때마다 1바이트 문자는 +1, 기타 문자는 +2를 하여 별도의 스칼라 변수를 하나 더 도입하고, 전체 폭은 이를 기준으로 계산하는 방법을 사용하면 기존과 동일한 결과물을 뿌려줄 수 있습니다.

3. 코드

이하는 위의 설계에 맞추어 실제 작성된 코드입니다. 필자의 경우도 과거 이 코드를 사용했었고, 현재도 일부 페이지에서는 유사한 목적으로 이 코드를 사용 중입니다.

과거에 등록되어 있던 코드는 UTF-8 내부 인코딩의 처리와 관련하여 약간의 문제가 있었습니다. 현재 등록되어 있는 코드는 이 문제를 해결한 코드입니다.

sub StrCutSize {

    my $value     = shift;   # 원래 문자열 (UTF-8 인코딩의 문자열)
    my $DefWidth  = shift;   # 잘라낼 길이 (글자폭)

    my $packed_data  = unpack "H*", $value;        # 16진수로 바꾼 문자열
    my $len             = length( $packed_data );  # $packed_data의 길이

    ## 잘라낼 길이가 없다면 문자열을 그대로 돌려보낸다. ##

    if ( !$DefWidth ) {
        return $value;
    }
    else {
        undef $value;    # 이제 원 문자열은 필요가 없어...
    }

    ## 첫 4비트를 읽어서 이것이 1바이트 문자인지 3바이트 문자인지를 확인하고
    ## 그에 따라 해당 바이트 수만큼 문자열을 붙여나간다.

    ## - unpack "H*" 의 결과로 만들어진 문자열의 문자는 1개가 4비트를 나타냄.
    ## - 따라서 2개의 문자가 8비트=1바이트

    my $TempStr;      # 확인용 1바이트 저장을 위한 임시 변수
    my $i;            # 포인터용 임시 변수
    my $CharWidth;    # 총 문자열 폭을 계산하기 위한 임시 변수
    my $ResultChar;   # 결과값 저장을 위한 변수

    for ( $i=0; $i < $len; ) {

        ## unpack 문자열 중 1바이트를 읽어온다.

        $TempStr = substr( $packed_data, $i, 1 );

        ## 만약 더 이상 값이 없다면 루프에서 탈출.

        if ( $TempStr eq "" ) { last; }

        ## 0~7 : Ascii 문자의 영역. size=1/char, byte=1/char ##

        if( $TempStr eq "0" || $TempStr eq "1" || $TempStr eq "2" ||
            $TempStr eq "3" || $TempStr eq "4" || $TempStr eq "5" ||
            $TempStr eq "6" || $TempStr eq "7" 
        ) {
            $ResultChar .= substr( $packed_data, $i, 2 );   # 1바이트 추가.
            $CharWidth += 1;                                # 글자 폭은 1 증가
            $i += 2;                                        # 포인터는 2 증가
        }

        ## e : 3바이트 문자의 영역. size=2/char, byte=3/char ##

        elsif ( $TempStr eq "e" ) {

            $ResultChar .= substr( $packed_data, $i, 6 );   # 3바이트 추가.
            $CharWidth += 2;                                # 글자 폭은 2 증가
            $i += 6;                                        # 포인터는 6 증가
        }

        ## 그 나머지 문자들은 UTF-8 영역에서 나타날 수 없는 문자들.
        ## 일괄적으로 아스키 문자 0x5F (_)로 바꿔준다.
        ## 한 칸을 차지하므로 문자열의 길이에는 1을 더해준다.

        else {
            $ResultChar .= "5F";                            # 1바이트 0x5F 대체
            $CharWidth += 1;                                # 글자 폭은 1 증가
            $i += 2;                                        # 포인터는 2 증가
        }

        ## 여기까지 온 $CharWidth의 값이 목표 크기를 넘었다면 루프에서 탈출.

        if( $DefWidth <= $CharWidth ) {
            last;
        }
    }

    ## 문자열이 만들어졌으므로, 이제 문자열을 역으로 pack한다.

    $ResultChar = pack "H*", $ResultChar;

    ## unpack/pack 을 거치면서 잃어버린 utf8 내부 인코딩 플래그를 살려준다.

    use Encode qw/decode/;
    $ResultChar = decode( "utf8", $ResultChar );

    ## 만들어진 문자열을 돌려준다.

    return $ResultChar;
}

UTF-8 인코딩을 전제하고 있기 때문에, 받는 문자열은 UTF-8 인코딩의 문자열(바이트스트림이건 내부 인코딩이건 불문)이며, 돌려주는 문자열은 내부 인코딩의 UTF-8 인코딩 문자열입니다.

☞ 태그:

☞ 트랙백 접수 모듈이 설치되지 않았습니다.

☞ 덧글이 2 개 있고, 트랙백이 없습니다.

덧글을 남기시려면 여기를 클릭하십시오.

□ 우욱 님께서 2010-01-20 17:39:34 에 작성해주셨습니다.

구글링으로 http://www.perlmania.or.kr:8949/bbs/bbs.html?mode=read&table=lang&article=2461&page=1 를 찾아서 사용하다가 이상하게 문자가 깨지는 경우가 발생해서 다시 찾다보니 여기서 해결 방법을 찾아 갑니다. (아직 적용 전이라 잘 되더라.. 고 말씀드리긴 힘들지만 가져가서 사용하기 전에 감사드리고 싶어서 댓글 달고 갑니다.

감사합니다. *^_^*

⇒ 부엉이 님께서 2010-02-01 02:20:41 에 답글을 작성하셨습니다.

Perlmania 에 썼던 2006년의 그 글을 보신거네요. 본문에도 있다시피, Perl의 encode/decode 에 대해서 잘 모르던 시절에 작성한 코드여서, 출력 인코딩 쪽에 상당히 취약한 모습입니다. (제 경우에는, 폼을 자동으로 만들어주는 코드를 짰는데 계속 일부분에서 한글들이 깨져나가길래, 대체 어떻게 된 건가 싶어서 여기저기 뒤져봤었죠.) 문제 잘 해결되었으면 좋겠네요. ^^

p.s. Perl 에서 UTF-8 핸들링 관련하여 가장 잘 정리된 "한글" 문서라면 아마 이 문서일 것입니다. 필요하시면 참고하시기를..
http://aero.springnote.com/pages/1053508

□ 우욱 님께서 2010-03-15 17:34:57 에 작성해주셨습니다.

잘 작동합니다. *^_^*

마지막 문제는 HTML::Strip 을 cpan에서 받아서 사용했는데 단순히 tag만 제거해 주는게 아니라 HTML::Strip를 통과시키면 문자열이 왕창 깨져버리는 문제가 발생해서 결국 tag 제거기를 새로 만들어야 했습니다. (아마도 escape된 entity를 바꿔주는 부분이 오류가 있는 것이 아닐까 추정됩니다)

그리고 지금은 XML::LibXML이 EUC-KR(정확히는 cp949)로 작성된 XML을 well-formed임에도 불구하고 파싱오류가 나서 cp949로 만들어진 xml을 utf-8로 바꿔서 저장시킨 후에 파싱을 해보려고 하는 중에 소스를 보고 참조하기 위해서 왔다가 글 남깁니다.

교훈: cpan의 모듈이라고 100% 믿을 수 있는 것은 아니다. -_-;;

⇒ 부엉이 님께서 2010-04-15 08:58:20 에 답글을 작성하셨습니다.

저도 얼마 전에 갑자기 프로그램이 오동작해서 원인 찾느라 반나절을 낭비했지요.
알고보니 원인은 사용한 CPAN 모듈들끼리 충돌... -_-;;

예-전에 CP949(MS949)를 Perl 5.8.0 버전의 Encode 모듈에 통과시켰더니
애가 뻗어버린 일이 있었죠. 뭐 그 시절 버전이라 그냥 그랬나보다 합니다만...
아시아, 특히 동아시아에 산다는 것이 인코딩 문제에 한해서는 정말 재앙이 아닌가 합니다.
빌어먹을 CJK처리... -_-;;

뭐 저는 전문 개발자도 아니고 그냥 취미로 프로그램 짜는거라 상관없겠지만,
전문개발자나 이걸로 먹고사시는 분들은 정말 매일같이 지옥구경 하실듯 합니다.

[483] < [306] [303] [301] [300] [299] ... [298] ... [295] [294] [270] [265] [261] > [19]

(C) 2000-2018, Owl-Networks. Powered by Perl. 이 페이지는 HTML 5 표준에 따라 작성되었습니다.