[첫화면으로]Perl/문자열인코딩

마지막으로 [b]

Perl에서 문자열 인코딩 변환과 관련된 이슈

1. 인코딩 변환(1)
2. Encode 모듈과 Text::Iconv 모듈
2.1. 문자열 나누기
2.2. 파일에 저장해서 hex값 보기
2.3. 그래서...
3. 인코딩 추측
4. 참고
5. Comments

1. 인코딩 변환(1)

UseModWiki소스수정에서 사용되는 인코딩 변환 함수는 다음 두 개의 모듈 중 하나를 사용한다.

Encode 모듈이 있으면 그걸 사용하고, 없으면 Text::Iconv 모듈을 사용한다. 그것마저 없으면 변환 못 하는 거고...

# $str 의 인코딩을 $from 에서 $to 로 컨버트
sub convert_encode {
    my ($str, $from, $to) = @_;

    eval { require Encode; };
    unless($@) {
        $str = Encode::encode($to, Encode::decode($from, $str));
    } else {
        eval { require Text::Iconv; };
        unless($@) {
            my $converter = Text::Iconv->new($from, $to);
            $str = $converter->convert($str);
        }
    }
    return $str;
}

Encode(4) 모듈을 사용하는 경우에 한해서는 위의 decode()(5)encode()(6)를 결합하는 대신에 from_to()(7)를 사용해서 아래와 같이 쓸 수도 있음
        Encode::from_to($str, $from, $to);    # 이 경우는 $str의 값 자체가 바뀐다.

2. Encode 모듈과 Text::Iconv 모듈

이 두 모듈이... "UCS-2(8)", "UTF-16(9)", "UTF-32(10)" 이 세 가지 인코딩 명칭에 대해 동작이 서로 다르다. Encode(11) 모듈은 이걸 BE(big endian)으로 처리하고, Text::Iconv(12) 모듈은 이걸 LE(little endian)으로 처리한다.

따라서 UTF-8(13) =변환1=> UTF-16(14) =변환2=> UTF-8 순으로 변환(15)하는 경우, 당연히 원래의 바이트 시퀀스가 되어야 하지만, 변환1과 변환2에 쓰이는 모듈이 동일하지 않으면 제대로 되지 않는다.

이와 관련한 글은 [KLDP BBS: perl 로, 인코딩에 관계없이 문자열을 두 개의 문자열로 나누는 코드] 글타래에서 언급되었고, 여기에 몇가지 테스트 결과를 정리하면 다음과 같다.

2.1. 문자열 나누기

일단, split_string 에서 중간에 정규표현식을 사용하여 쪼개는 과정을 생략하고, 단지 utf-8 -> 어떤 인코딩 -> utf-8 로 돌리는 것은 웬만하면 다 됩니다. "어떤 인코딩"이 UCS-2(16), UTF-16(17), UTF-32(18), 여기에 각각 BE, LE 붙여가며 해 봤는데, Encode(19) 모듈과 Text::Iconv(20) 모듈 다 스트링이 원래의 것으로 잘 나오더군요.

그런데 정규 표현식을 써서 쪼개기를 하려면, 제대로 쪼개지질 않더군요. 어떤 인코딩을 뭘로 지정해도...

이런 인코딩들의 경우는 정규표현식의 "."이 여전히 "1바이트"를 의미한다고 생각하고, $length 값을 2배로 곱해서 처리하게 했더니 그제서야 제대로 쪼개집니다.

아래의 표는 각각의 인코딩 이름을 두 개의 모듈을 써서, 한글 스트링, 일본어 스트링, 그리고 위키페디아에서 적당히 복사해 온 정체모를 언어 스트링을 split_string($str, 2) 를 거치게 했을 때의 결과입니다. (딱히 언어에 따라 다른 결과가 나오지는 않았습니다)

                Encode모듈        Text::Iconv모듈
UCS-2           ok                ok
UCS-2BE         ok                ok
UCS-2LE         ok                ok
UTF-16          error(주1)        error(주5)
UTF-16BE        ok                ok
UTF-16LE        ok                ok
UTF-32(주2)     error(주3)        error(주6)
UTF-32BE        ok                ok
UTF-32LE        ok                ok
UTF-8           error(주4)        error(주7)

* 주1) UTF-16:Unrecognised BOM b098 at /usr/lib/perl5/5.8.5/i386-linux-thread-multi/Encode.pm line 162

* 주2) $lengh 값을 4배 곱한 후 처리

* 주3) 주1과 같은 에러, 명칭만 UTF-32 로 나옴 (utf-16과 utf-32의 경우는 스트링 앞에 바이트오더를 알려주는
  BOM이란 게 붙는다는 설명으로 보아, 스트링을 둘로 쪼갠 후에 뒷쪽 부분에는 그 BOM이 안 붙은 상태라서
  이렇게 되는듯

* 주4) 문자들마다 바이트 수가 다르므로, 제대로 쪼개지지 않음.

* 주5) split_string 에 인자로 준 숫자보다 하나 적은 수의 문자를 세어서 쪼갬.(쪼갤 때 BOM도 두 바이트를
  차지하기 때문인듯)

* 주6) 주5)와 동일

* 주7) 아예 결과가 ("", "") 으로 나옴

즉 endian 을 지정하지 않은 경우에, BOM이 붙느냐 마느냐, BOM이 붙은 스트링을 쪼갤 때 다시 양쪽에 BOM을 붙여 주느냐 마느냐 등의 문제로 서로 다르게 동작했다.

2.2. 파일에 저장해서 hex값 보기

use Encode;

$str = "\x{ac00}\x{b098}";  # "가나" in unicode
open OUT, ">out_unicode.txt"; print OUT "$str\n"; close OUT;

별도의 인코딩 없이 저 유니코드 표현을 그대로 파일에 찍으면 UTF-8로 출력하더군요.

$ hexdump out_unicode.txt
0000000 b0ea eb80 9882 000a      <- 가 %EA%B0%80 나 %EB%82%98 그런데 두 바이트씩 끊은 후 little endian 으로 저장한 듯
0000007

그 다음은, 온갖 인코딩을 바꿔가면서 파일에 저장 =.=;;

# Encode 모듈의 경우는, 저 펄의 유니코드 표현을 아래의 각 이름별로 encode에 넣은 후 출력
for $enc qw(EUC-KR UCS-2 UCS-2BE UCS-2LE UTF-16 UTF-16BE UTF-16LE UTF-32BE UTF-32LE UTF-8) {
    $str2 = encode($enc, $str);
    open OUT, ">out_$enc.txt"; print OUT "$str2\n"; close OUT;
}

# Text::Iconv 의 경우는, 펄의 내부 표현을 나타내는 이름이 뭔지 모르기 때문에
# 일단 utf-8 스트링을 지정한 후 그걸 아래의 각 이름별로 utf-8 -> $enc 변환
$str = "가나";
for $enc qw(EUC-KR UCS-2 UCS-2BE UCS-2LE UTF-16 UTF-16BE UTF-16LE UTF-32BE UTF-32LE UTF-8 UNICODE) {
    $converter = Text::Iconv->new("UTF-8", "$enc");
    $str2 = $converter->convert($str);
    open OUT, ">conv_$enc.txt"; print OUT "$str2\n"; close OUT;
}

Encode()를 사용하여                      Text::Iconv()를 사용하여
펄 내부 표현을 각각의 인코딩으로 변환    UTF-8 스트링을 각각의 인코딩으로 변환

--------------------------------------------------------------------
out_EUC-KR.txt                           conv_EUC-KR.txt
0000000 a1b0 aab3 000a                   0000000 a1b0 aab3 000a
0000005                                  0000005
(가 %B0%A1 나 %B3%AA 인데 두 바이트씩 순서가 바뀌어 저장)

--------------------------------------------------------------------
out_UCS-2.txt                            conv_UCS-2.txt
0000000 00ac 98b0 000a                   0000000 ac00 b098 000a
0000005                                  0000005
(둘이 다르다! Encode에서는 UCS-2BE로, Text::Iconv는 LE로 저장했음)

(파일이 저장되면서 두 바이트씩 바이트 순서가 바뀌어 저장되는지,
눈으로 봤을때는 좌측이 LE처럼 보임 =.=;)

--------------------------------------------------------------------
out_UCS-2BE.txt                          conv_UCS-2BE.txt
0000000 00ac 98b0 000a                   0000000 00ac 98b0 000a
0000005                                  0000005

--------------------------------------------------------------------
out_UCS-2LE.txt                          conv_UCS-2LE.txt
0000000 ac00 b098 000a                   0000000 ac00 b098 000a
0000005                                  0000005

--------------------------------------------------------------------
out_UTF-16.txt                           conv_UTF-16.txt
0000000 fffe 00ac 98b0 000a              0000000 feff ac00 b098 000a
0000007                                  0000007
(이것도 다르다! Encode는 UTF-16BE로, Text::Iconv는 LE로 저장했음)

(BOM 마크가 좀 이상하다... Encode는 BE의 BOM인 0xFeFF 를 앞에 붙이고
그게 파일로 저장되면서 순서가 바뀌었다고 볼 수 있는데, Text::Iconv가
붙인 BOM은 해석이 안 됨. BE의 BOM인 0xFeFF가 순서 바뀜 없이 저장된 거라면
그 뒤의 따라오는 "가나"가 16LE.txt와 동일하게 구성된 것과 상충하고,
LE의 BOM은 0xFFeF 이므로 순서를 바꾸면 ef ff 여야 한다. -_-???)

--------------------------------------------------------------------
out_UTF-16BE.txt                         conv_UTF-16BE.txt
0000000 00ac 98b0 000a                   0000000 00ac 98b0 000a
0000005                                  0000005

--------------------------------------------------------------------
out_UTF-16LE.txt                         conv_UTF-16LE.txt
0000000 ac00 b098 000a                   0000000 ac00 b098 000a
0000005                                  0000005

--------------------------------------------------------------------
out_UTF-32BE.txt                         conv_UTF-32BE.txt
0000000 0000 00ac 0000 98b0 000a         0000000 0000 00ac 0000 98b0 000a
0000009                                  0000009

--------------------------------------------------------------------
out_UTF-32LE.txt                         conv_UTF-32LE.txt
0000000 ac00 0000 b098 0000 000a         0000000 ac00 0000 b098 0000 000a
0000009                                  0000009

--------------------------------------------------------------------
out_UTF-8.txt                            conv_UTF-8.txt
0000000 b0ea eb80 9882 000a              0000000 b0ea eb80 9882 000a
0000007                                  0000007

--------------------------------------------------------------------
* 이건 좀 특별한 케이스

out_unicode.txt                          conv_UNICODE.txt
0000000 b0ea eb80 9882 000a              0000000 feff ac00 b098 000a
0000007                                  0000007

(Encode의 경우는 \x{ac00}\{b098}을 그대로 출력했더니 UTF-8과 동일하게
저장됐고, Text::Iconv 의 경우는 "UTF-8"을 "UNICODE"로 변환시켰더니만
UTF-16과 동일하게 저장됐음)

2.3. 그래서...

http://search.cpan.org/~dankogai/Encode-2.18/Unicode/Unicode.pm - perldoc Encode::Unicode

위 문서에 있는 내용에 따르면

iconv 쪽에서는 어느 문서를 봐야 하는지 알수가 없어 못 찾았지만, 저 설명대로라면 UTF-16(25) 이나 UTF-32(26)Encode(27) 모듈은 제 맘대로 BE취급하고 Text::Iconv(28) 는 LE 취급한다 싶군요. 그건 그러려니 하더라도, UCS-2(29) 만큼은 두 모듈이 공히 BE로 처리해줘야 할 텐데 Iconv 쪽은 LE로 처리하는 것이 의아합니다.

뭐 해결은 못해도... 일단 현재 상황은 정확히 알게 되었으니 만족해야겠고...

인코딩 명칭으로 "UTF-16(30)", "UTF-32(31)", "UCS-2(32)"를 사용할 경우, 두 모듈은 다르게 처리하더라...는 게 결국 힘빠지는 결말.

3. 인코딩 추측

스트링이 뭘로 인코딩되었는지 추측(33)하는 건 Cpan:Encode::Guess 모듈을 쓴다. (이건 Encode모듈과는 별개) UTF-8이전작업/브라우저GET요청인코딩판별에서 사용된 코드:
    # 추측
    if (eval "require Encode; require Encode::Guess;") {
        my @suspects = qw(euc-kr utf8);
        my $decoder = Encode::Guess::guess_encoding($string, @suspects);
        if (ref($decoder)) {
            # 추측 성공
            return convert_encode($string, $decoder->name, $HttpCharset);
        }
    }

4. 참고

코드값 = 0xAC00 + (초성값 * 21 * 28) + (중성값 * 28) + 종성값

초성값 : ㄱ=0, ㄲ=1, ㄴ=2 ... ㅎ=18
중성값 : ㅏ=0, ㅑ=1, ... ㅣ=20
종성값 : 없음 = 0, ㄱ = 1, ㄲ = 2...ㅎ=27

5. Comments

이름:  
Homepage:
내용:
 


컴퓨터분류
찾아보기:
Encode   2, 4, 11, 19, 27
  decode()   5
  encode()   6
  from_to()   7
Text::Iconv   3, 12, 20, 28인코딩   
  UCS-2   8, 16, 23, 29, 32
  UCS-2BE   24
  UTF-16   9, 14, 17, 21, 25, 30
  UTF-32   10, 18, 22, 26, 31
  UTF-8   13
  변환   1, 15
  추측   33
 

마지막 편집일: 2012-2-11 12:25 am (변경사항 [d])
6210 hits | Permalink | 변경내역 보기 [h] | 페이지 소스 보기