Owl-Networks Archive
| 분류: Perl | 최초 작성: 2009-01-04 06:19:41 |
웹에서 동작하는 게시판 등을 제작하면서 게시물 리스트 등을 뽑을 때에, 제목이 너무 길어지는 경우 게시판이 보기 싫게 되는 경우가 있습니다. 이런 경우에 제목이 몇 글자 이상이면 몇 글자까지만 보여주고 뒤는 생략해라.. 라는 옵션을 거의 모든 게시판에서 줄 수 있는데요.
이 경우 가장 간단한 것은 substr 문을 이용해서 기계적으로 xx글자(xx바이트)만 남기고 뒤쪽은 날리는 방법입니다. 그러나, 이것은 2바이트 문자인 한글(euc-kr)의 경우 기계적으로 몇 바이트를 자르는 경우에 문자열의 끝에 1바이트가 잘린 상태의 한글이 남아 보기 흉하게 되는 경우가 생깁니다. 따라서, 끝의 1바이트를 검사해서 그것이 한글의 일부인 경우에는 1바이트를 자르거나 더 붙여주는 처리를 하여야 합니다. 다음 웹 페이지는 이러한 경우에 사용할 수 있는 예시를 보여주고 있습니다.
그러나, 바이트 수가 다양해지는 UTF-8로 오면, 문자열 끄트머리의 한글 문자 처리 외에 고려해야 할 문제가 하나 더 늘어납니다.
2바이트 한글의 경우, 한글 한 글자의 폭은 대체로 영문 두 글자의 폭과 같고, 한 글자가 차지하는 바이트 수도 한글 한 글자(2바이트)와 영문 두 글자가 같았기 때문에, 문자 종류에 따른 글자폭을 고려할 필요 없이 단순히 바이트수를 기준으로 하여 잘라내기만 하면 대체로 균등한 길이의 문자열을 얻어낼 수 있었습니다. 그러나, 사실상 웹에서의 표준 인코딩이 된 UTF-8 인코딩의 경우에는 한글 한 글자가 차지하는 바이트 수가 3바이트이므로, 글자폭으로 계산한 경우 한글 1글자=영문 2글자이지만 바이트 수로 계산한 경우 한글 1글자=영문 3글자가 되어, 단순히 바이트 수를 기준으로 하여 문자열을 자를 경우 문자열 전체의 폭이 비슷하게 출력되리라는 보장이 없게 됩니다. 따라서 UTF-8 인코딩의 문자열을 웹에서 표시하는 CGI의 경우에는 바이트 수를 감안하여 문자열 끄트머리를 자르거나 더 붙여주는 이외에, 화면에 표시될 문자의 숫자까지도 신경을 써야 하게 됩니다.
이 점들을 모두 해결하기 위해서, 자문자답 식으로 나름대로 코드를 짜보았습니다. 일단 UTF-8 인코딩의 문자 저장 방식을 알 필요가 있습니다.
따라서, 문자의 첫 4비트를 체크해서 그것이 0xxx 인지, 1110인지만 체크해서 문자열을 붙여나가는 방식을 사용하면 문자열이 잘리는 일 없이 무사히 자를 수 있게 됩니다.
또한, 문자열의 전체 폭의 문제는, 문자를 하나 붙일 때마다 1바이트 문자는 +1, 기타 문자는 +2를 하여 별도의 스칼라 변수를 하나 더 도입하고, 전체 폭은 이를 기준으로 계산하는 방법을 사용하면 기존과 동일한 결과물을 뿌려줄 수 있습니다.
이하는 위의 설계에 맞추어 실제 작성된 코드입니다. 필자의 경우도 과거 이 코드를 사용했었고, 현재도 일부 페이지에서는 유사한 목적으로 이 코드를 사용 중입니다.
과거에 등록되어 있던 코드는 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처리... -_-;;
뭐 저는 전문 개발자도 아니고 그냥 취미로 프로그램 짜는거라 상관없겠지만,
전문개발자나 이걸로 먹고사시는 분들은 정말 매일같이 지옥구경 하실듯 합니다.
(C) 2000-2023, Owl-Networks. Powered by Perl. 이 페이지는 HTML 5 표준에 따라 작성되었습니다.