Perl에서 한글을 다루는 일들과 관련된 내용들.
-
- 1. Perl과 한글
-
-
- 1.1. 기본 개념, 문자열 상수, 표준 입출력, 정규표현식
-
- 1.2. 파일 입출력
-
- 1.3. 기타
-
2. 기타 & Comments
-
1. Perl과 한글
주인장이 네이버 Perl 까페에 올린 강좌글 합본
까페 원문 주소:
1.1. 기본 개념, 문자열 상수, 표준 입출력, 정규표현식
컴퓨터에서 영어 이외의 언어를 다룰 때는 이래저래 신경써야 하는 점들이 생깁니다. 8비트, 16비트 컴퓨터 시절에 비하면야 훨씬 상황이 나아졌습니다만, 여전히 영어를 사용할 때에 비해 손이 더 갑니다.
1) 다음 문자열의 길이는 몇인가
일단 코드 하나를 가지고 얘기해 봅시다.
#!perl
use strict;
use warnings;
my $eng = "hello";
print length($eng), "\n";
my $kor = "안녕하세요";
print length($kor), "\n";
실행결과:
5
10
똑같이 "다섯 글자"로 이루어진 스트링인데, 첫번째 줄은 5가 나오는데, 두번째 줄은 5가 아니라 10입니다. 게다가 어떤 환경에서는 15가 나올 수도 있습니다.
왜 이런 현상이 발생하는지부터 알아봅시다.
2) 아무리 그래도 이건 알고 있어야 얘기가 되는 것들
컴퓨터에는 모든 정보가 1 또는 0의 조합으로 저장됩니다. 이렇게 1 또는 0이 들어가는 한 자리가 비트(bit).
보통은 8개의 비트를 묶어서 한 단위로 처리합니다. 이것이 바이트(byte). 네트워크 통신에 관련된 분야에서는 옥텟(octet)이라고도 부릅니다.
한 바이트는 8비트이므로, 00000000 부터 11111111까지, 10진수로는 0부터 255, 16진수로는 0부터 FF 까지 총 256가지 조합 중 하나를 저장할 수 있습니다.
(앞으로는 16진수는 0x 를 앞에 붙여서 0x00, 0xA0FF 등으로 표기하겠습니다, 자릿수가 많을 때는 0x45 65 2A F3 이렇게 공백을 삽입하고, 명백히 16진수인 걸 알 수 있을 때는 0x를 생략하겠습니다)
3) 얘기를 해야 하나 말아야 하나 망설여지는 것들
컴퓨터에서 문자를 저장할 때는 각 문자에 특정한 비트조합이 대응됩니다.
- A는 0100 0001(2진수) = 65(10진수) = 41(16진수) 로 저장됩니다.
- B는 0100 0010 = 66 = 0x42 로 저장됩니다.
- ...
어느 문자가 어느 수에 대응될지 규약이 있는데, 이것을 "인코딩(encoding)"이라고 합니다.
보통 널리 사용되는 건 ASCII 규약입니다. 프로그램에 관심을 가진 후에는 "아스키 코드"란 걸 들어보셨을 겁니다.
인코딩은, "추상적인 정보를 구체적인 코드로 변환하는 것, 또는 변환하는 규칙"이라고 생각하면 됩니다. 그 역은 디코딩(decoding)이라고 합니다. 즉
- "영어에서 사용하는 알파벳 문자 중에 '에이'라고 발음하는 첫번째 문자의 대문자 형태, 종이에 그릴 때는 A 이렇게 그림" - 추상적인 정보
- 0100 0001 또는 0x41 - 구체적인 코드
둘 사이를 변환하는게 인코딩과 디코딩이라는 얘기입니다.
영문과 특수문자는 전부 합쳐도 127개보다 적기 때문에, 256가지 정보를 저장할 수 있는 한 바이트 안에 한 문자를 넣을 수 있습니다.
그런데 그외 언어의 문자를 전부 추가하려니 택도 없습니다. 특히나 중국의 한자, 한국의 한글(한글의 경우 자모가 조합된 음절 하나하나가 다 개별 문자입니다)의 수는 어마어마합니다. 그래서 한 바이트 안에 한 문자를 넣을 수 없고, 2바이트, 또는 3바이트, 또는 4바이트가 필요합니다.
자, 영문자 A를 어느 숫자에 대응할 지는 ASCII 규약을 따르면 됩니다. 그러면 한글 "가"를 어느 숫자에 대응할지는 어떻게 정하느냐?
안타깝게도 이걸 정하는 규약이 단 하나가 아니라 여러 가지가 있고,
- MS윈도우즈에서 사용되는 코드페이지949(cp949)에서는 0xb0a1 에 대응됩니다.
- 유닉스/리눅스에서 사용되었던 euc-kr에서도 0xb0a1 에 대응됩니다. (cp949는 euc-kr에 빠져있던 문자들을 추가한 거라서, euc-kr에 들어있던 문자들은 그대로 cp949에서도 같은 값에 대응됩니다)
- cp949나 euc-kr에서는 한글 "가"를 저장하기 위해서는 2바이트가 필요합니다. 다른 한글 문자들도 다 마찬가지로 2바이트가 필요합니다.
- 유닉스/리눅스에서 최근에 일반적으로 사용되고, 웹페이지에서도 널리 사용되는 UTF-8 규약에서는 0xea b0 80 에 대응됩니다. 즉 "가" 또는 그 외 한글 문자 하나를 저장하기 위해서는 3바이트가 필요합니다.
4) 펄 스크립트를 어떤 인코딩에 따라 저장했느냐에 따라서...
이제 1)의 프로그램으로 다시 돌아가봅시다. 저 프로그램을 작성하여 저장할 때, 에디터에서 어떤 인코딩을 사용하여 저장했느냐에 따라서 스크립트 내의 모든 문자가 어떤 숫자에 대응되어 저장될지, 몇 바이트를 차지할지가 달라집니다.
윈도우의 메모장(notepad.exe)에서는 "새 이름으로 저장" 메뉴에서 인코딩을 지정할 수 있습니다. 다음 그림에서 ANSI 가 cp949 입니다. ANSI와 UTF-8 말고 두 개 더 있는데 여기서는 통과.
그러면 저 스크립트를 두가지 인코딩으로 각각 저장한 후 파일의 크기를 비교해 보겠습니다.
두 인코딩 모두 영문,숫자,특수 기호를 저장하는 건 ASCII와 동일하게 대응시킵니다. 따라서 차이는 "안녕하세요" 다섯글자 뿐인데, cp949는 한글 문자 하나에 2바이트를 사용해서 총 10바이트. UTF-8은 3바이트씩 총 15바이트가 필요합니다. 따라서 전체 크기는 5바이트 차이가 납니다.
그런데 막상 확인해보면
- cp949 인코딩을 사용해 저장한 파일은 151바이트
- UTF-8 인코딩을 사용해 저장한 파일은 159바이트
이렇게 8바이트 차이가 납니다. 이것은 메모장이 UTF-8 인코딩을 사용해 문서를 저장할 때 제일 앞에 추가로 3바이트짜리 정보를 집어넣기 때문입니다. 다른 에디터를 사용할 경우는 정확히 5바이트 차이날 것입니다.
(** 메모장이 문서 제일 앞에 집어넣은 3바이트 정보는 BOM(byte order marks)이라 부르는데, 사실 UTF-8 인코딩의 경우 BOM을 넣을 필요가 없습니다. 이 이유로, 메모장에서 UTF-8로 저장한 텍스트를 다른 에디터나 프로그램에서 읽을 때 문제가 될 수 있습니다. **)
5) 파일 크기만 달라지면 다행인데...
처음 프로그램 마지막에, $eng 와 $kor 에 저장된 문자열을 출력하는 라인을 추가합니다.
print "[$eng][$kor]\n";
그런 후 마찬가지로 두 가지 인코딩을 사용해서 따로 저장한 후 실행해보면...
cp949 로 인코딩한 스크립트를 실행하면 제대로 "안녕하세요"가 출력이 되는데, UTF-8로 인코딩한 스크립트를 실행하면 한글이 깨져 나옵니다. 아아, 악몽의 시작입니다ㅠㅠ
우리가 작성한 펄 프로그램이 "안녕하세요"를 출력할 때는, "한글 음절 안,녕,하,세,요를 출력해주세요~"라고 하는게 아닙니다. 펄 프로그램은 명령 프롬프트 창에게 단지 "바이트들의 덩어리 be c8, b3 e7, c7 cf, bc bc, bf e4 를 출력해주세요"라고 전달하거나(cp949의 경우) "바이트들의 덩어리 ec 95 88, eb 85 95, ed 95 98, ec 84 b8, ec 9a 94 를 출력해주세요"(utf-8의 경우)라고 전달합니다.
명령 프롬프트창은 펄 프로그램에게서 넘겨받은 바이트 덩어리를 화면에 뿌리면서, 다시 해당 바이트 덩어리에 대응되는 문자로 바꾸어서 출력합니다. 이 때 "어떠한 바이트 덩어리가 어떤 문자에 대응될 것인가" 역시 인코딩 규약을 따르는데, 한글 윈도우를 쓸 경우 명령 프롬프트 창이 기본적으로 사용하는 인코딩은 cp949입니다. 따라서 be c8 을 "안"으로 바꿔줄 수는 있지만, ec 95 88 을 "안"으로 바꿔주지는 못합니다.
출력 뿐 아니라 입력을 받을 때도 마찬가지, 우리가 <STDIN> 표준 입력을 통해서 입력을 받는다면, 명령 프롬프트 창에서 실행한 프로그램에 키보드로 입력한다면 기본적으로 cp949 인코딩을 따르고, 키보드로 "안"을 입력했다면 실제로 펄 프로그램에게 전송되는 건 "한글 음절 안"이 아니라 "be c8"이라는 2바이트의 데이타입니다.
비슷한 맥락으로, 펄로 웹사이트 방명록 CGI를 만들었다면, 유저가 방명록에 "안"을 입력하고 전송할 경우 웹사이트의 인코딩에 따라서 "be c8"이 들어올 수도 있고 "ec 95 88"이 들어올 수도 있습니다.
6) 바이트 덩어리와 문자열 간의 변환
이제 문제의 원인을 (수박 겉핧기나마) 알았으니, 해결책을 알아봅시다.
Perl 5.8 부터는 Encode 모듈을 제공하며, 다음 두 가지를 구분해서 처리하고 서로간의 변환을 할 수 있도록 지원합니다.
- 입출력에 사용되기 위한 바이트들의 덩어리. 이것을 Encode 문서에서는 octets 라고 부릅니다. 이 연재에서는 주로 "바이트 덩어리"라고 부를 것입니다.
- 세상 여러 언어에서 사용되는 문자들이 하나 이상 모여 이뤄진 문자열. 이걸 string 으로 부르며, 이 연재에서는 "문자열" 또는 "추상적 문자열"이라고 표현할 것입니다.
Encode 모듈은 encode와 decode함수를 제공합니다.
- decode 함수는 "바이트 덩어리"를 "문자열"로, octets을 string으로 바꿉니다.
- encode 함수는 "문자열"을 "바이트 덩어리"로, string을 octets로 바꿉니다.
자, 해 봅시다.
똑같은 소스코드가 두 가지 인코딩으로 각각 따로 저장되어 있어서 두 번 나옵니다. 내용도 살짝 달라지니 주의합시다.
먼저 cp949 로 인코딩했던 스크립트입니다.
1 #!perl
2
3 use strict;
4 use warnings;
5 use Encode;
6
7 my $eng = "hello";
8 print length($eng), "\n";
9 my $kor = "안녕하세요";
10 print length($kor), "\n";
11
12 my $string = decode("cp949", $kor);
13 print length($string), "\n";
14
15 my $output = encode("cp949", $string);
16 print "[$output]\n";
다음은 UTF-8로 인코딩해서 저장한 스크립트입니다.
1 #!perl
2
3 use strict;
4 use warnings;
5 use Encode;
6
7 my $eng = "hello";
8 print length($eng), "\n";
9 my $kor = "안녕하세요";
10 print length($kor), "\n";
11
12 my $string = decode("UTF-8", $kor);
13 print length($string), "\n";
14
15 my $output = encode("cp949", $string);
16 print "[$output]\n";
10번 라인까지는 1)에서와 같습니다.
12번 라인에서, $kor 변수에 들어 있던 바이트 덩어리를, Perl의 진정한 "문자열"로 변환합니다. 이 때 decode의 첫번째 인자로는 인코딩 규약의 이름이 들어갑니다. cp949 로 저장했던 스크립트에서는 저 문자열을 cp949 규약에 맞추어서 변환해야 하고, UTF-8 로 저장했던 스크립트에서는 UTF-8 규약에 맞추어야 할 것입니다.
실행 결과는
스크립트가 어떤 인코딩으로 저장되었느냐에 따라서, $kor 변수에 들어있는 바이트 덩어리에 length() 함수를 적용한 결과는 10 또는 15가 나왔습니다. 그러나 이 바이트 덩어리를 문자열로 변환하고 나면, 인코딩에 상관 없이 글자의 개수가 반환됩니다. 둘 다 "안녕하세요"의 길이는 5가 나오는 것을 볼 수 있습니다.
화면에 출력해봅시다.
두 파일에 동일하게 다음 라인들을 추가해줍니다.
14
15 my $output = encode("cp949", $string);
16 print "[$output]\n";
명령 프롬프트 창이 cp949 인코딩을 사용하고 있기 때문에, 스크립트 자체가 어떤 인코딩을 사용해 저장했느냐에 무관하게, 명령 프롬프트 창에 문자열을 출력하기 위해서는 cp949 인코딩 규약에 기반해서 다시 바이트 덩어리로 바꾸어 준 후에 전달해야 합니다. 이때 encode 함수를 사용합니다.
실행 결과는 다음과 같습니다
아까는 UTF-8 로 인코딩한 스크립트에서 "안녕하세요"를 출력하면 한글이 깨져서 나왔지만, 이제는 제대로 나오는 걸 알 수 있습니다. "안녕하세요"가 cp949 인코딩에 맞춰 바이트 덩어리로 바뀌어 전달되었고, 명령 프롬프트 창은 전달받은 바이트 덩어리를 다시 cp949 인코딩에 맞춰 해석하여 그에 대응되는 문자들을 출력하기 때문입니다.
7) 또 다른 경우 - 정규표현식의 예
문자열과 바이트 덩어리 간의 변환관계, 즉 인코딩의 문제는 입출력에만 영향을 끼치는 게 아니라, 문자열을 대상으로 한 여러가지 연산에도 적용이 됩니다.
예를 하나 들어보겠습니다. 키보드로 입력을 받아서, 입력받은 문자열이 변수에 저장되어 있던 문자열에 포함이 되는지 검사하는 코드입니다. (사실 이것도 "키보드로 입력을 받는" 부분에서 입출력과 관련이 있긴 합니다)
my $str = "안녕하세요";
chomp( my $input = <STDIN> );
if ( $str =~ /$input/ ) {
print "match\n";
}
else {
print "not match\n";
}
이 코드를 cp949 로 저장을 하여 명령 프롬프트 창에서 실행을 시키거나, UTF-8로 저장을 한 후 UTF-8 인코딩을 사용하는 리눅스 쉘에서 실행을 한다면 상관이 없지만, 코드를 저장할 때 사용한 인코딩과 실행환경에서 사용하는 인코딩이 일치하지 않으면 문제가 됩니다.
$str 변수에는 "안녕하세요"라는 문자열이 들어가있고, 키보드로 "안녕"을 입력했는데, 어떤 때는 일치 검사를 통과하고 어떤 때는 하지 못합니다. 게다가 좌우 두 창을 비교해보면 통과 여부가 서로 반대입니다.
여기서도 앞의 경우와 마찬가지로..
- $str 변수에는 "안녕하세요"라는 문자열이 아니라, 사실은 be c8, b3 e7, c7 cf, bc bc, bf e4 (cp949로 저장한 코드) 또는 ec 95 88, eb 85 95, ed 95 98, ec 84 b8, ec 9a 94 (UTF-8로 저장한 코드) 로 구성된 바이트 덩어리가 들어 있습니다.
- 키보드로 입력한 "안녕"은 펄 프로그램으로 전달될 때 be c8, b3 e7 (cp949를 사용하는 쉘) 또는 ec 95 88, eb 85 95 (UTF-8을 사용하는 쉘)의 형태로 전달됩니다.
- 매치연산자는 저장된 바이트 덩어리 내의 일부가 전달받은 바이트 덩어리와 일치하는지를 검사합니다.
따라서 저장되어 있는 be c8, b3 e7, c7 cf, bc bc, bf e4 (cp949로 저장한 코드) 에 ec 95 88, eb 85 95 (UTF-8을 사용하는 쉘)가 포함되는지를 비교하면 일치하지 않는 것으로 판정됩니다. 우리 눈에는 "안녕하세요" 안에 "안녕"이 분명히 포함되어 있지만 말입니다.
이 문제도 마찬가지로, Encode 모듈을 사용해서, 저장되어 있는 바이트 덩어리와 입력받은 바이트 덩어리를 문자열로 변환을 함으로써 해결할 수 있습니다.
아래 코드는, 코드 자체는 cp949 인코딩을 사용하여 저장하고, 실제 실행은 UTF-8을 사용하는 쉘에서 실행할 거라고 가정했을 때의 코드입니다.
1
2 use Encode;
3
4 my $str = decode("cp949", "안녕하세요");
5
6 chomp( my $input = <STDIN> );
7 $input = decode("UTF-8", $input);
8 if ( $str =~ /$input/ ) {
9 print "match\n";
10 }
11 else {
12 print "not match\n";
13 }
4번 라인에서, 코드 내 문자 리터럴 "안녕하세요"를 decode를 써서 추상적인 문자열로 변환합니다. 이때는 cp949 인코딩 규약에 따라 변환합니다.
7번 라인에서, 키보드로 입력받은 문자열을 역시 추상적인 문자열로 변환합니다. 이때는 UTF-8에 따라 변환합니다.
변환이 끝나면 $str 과 $input 둘 다 바이트 덩어리가 아닌 문자들의 집합으로 취급되며, 8번 라인에서는 "안녕하세요"라는 문자열 안에 입력받은 문자열, 아래 스크린샷에서는 "안녕"이 포함되는지를 검사합니다.
UTF-8을 사용하는 쉘에서 cp949 인코딩으로 저장된 코드를 실행했는데도 일치 여부가 제대로 판정이 되는 걸 알 수 있습니다.
8) 정규 표현식 하나 더
단순 일치 뿐 아니라, 정규표현식에서 "임의의 문자 하나"를 뜻하는 "."의 동작도 인코딩에 영향을 받습니다.
아래 코드는 $str 에 들어있는 문자열에서, "녕"이라는 문자 바로 뒤에 있는 문자 1개, 2개, 3개를 각각 출력합니다.
1 my $str = "안녕하세요";
2
3 if ( $str =~ /녕(.)/ ) {
4 print "[$1]\n";
5 }
6 if ( $str =~ /녕(..)/ ) {
7 print "[$1]\n";
8 }
9 if ( $str =~ /녕(...)/ ) {
10 print "[$1]\n";
11 }
(** 정규표현식에 익숙하지 않은 분들을 위해 짧게 설명하면, 정규표현식 내에 괄호로 둘러쌓인 부분에 매치되는 문자열이 그 다음 줄에서 $1 이라는 변수에 담깁니다. 우리가 하고자 하는 건 "안녕하세요"에서 "녕" 바로 뒤에 오는 게 뭔지를 출력하려는 겁니다. **<footnote(/정규표현식))
섹션 7)에서와 같이, 위 코드를 cp949, UTF-8 두 가지 버전으로 따로 저장하고, 두 가지 쉘에서 각각 실행해보면 다음과 같이 나옵니다.
- cp949 에서는, 코드의 6번 라인에서 "녕" 뒤에 (..) 이렇게 마침표 두 개를 넣었을 때 제대로 "하"를 잡아냅니다.
- UTF-8 에서는, 코드의 9번 라인에서 (...) 이렇게 마침표 세 개를 넣었을 때 제대로 "하"를 잡아냅니다.
즉, "."가 "한 바이트"에 해당이 되고, cp949 에서는 한글은 2바이트를 차지하므로 ".."를 써야 제대로 한 글자에 매치가 되고, UTF-8 에서는 한글은 3바이트를 차지하므로 "..."를 써야 매치가 된다는 얘깁니다.
따라서,
여러분은 한글이 포함된 문자열에 정규표현식을 사용할 때는 항상 그 문자열이 무슨 인코딩을 써서 저장되었는지를 감안해서 정규표현식을 다르게 써 주어야 합니다.
...
...
죄송합니다, 거짓말입니다. 설마 진짜로 믿으신 분은 안 계시겠죠..?
여기서도 마찬가지로, 바이트 덩어리가 아닌 문자열로 처리하도록 고쳐줍시다.
UTF-8로 저장한 스크립트를 다음과 같이 고치겠습니다:
1
2 use Encode;
3
4 my $str = decode("UTF-8", "안녕하세요");
5 my $prefix = decode("UTF-8", "녕");
6
7 if ( $str =~ /$prefix(.)/ ) {
8 print "[", encode("UTF-8", $1), "]\n";
9 }
10 if ( $str =~ /$prefix(..)/ ) {
11 print "[", encode("UTF-8", $1), "]\n";
12 }
13 if ( $str =~ /$prefix(...)/ ) {
14 print "[", encode("UTF-8", $1), "]\n";
15 }
cp949로 저장한 스크립트였다면, "UTF-8"이라고 쓰인 부분을 "cp949"로 고치면 되겠죠?
4번 라인, "안녕하세요"를 바이트 덩어리에서 문자열로 변환했습니다.
5번 라인, 정규표현식 내에 들어가는 한글도 마찬가지로 바이트 덩어리가 아닌 문자열로 간주하게 하기 위해서 별도의 변수를 써서 문자열로 변환하여 저장했습니다.
8, 11, 14번 라인, 정규식에서 괄호안에 마침표에 매치되는 게 $1 변수에 담겼습니다. 그런데 $1 변수에 담긴 건 추상적인 문자열이고, 이것을 print 를 써서 출력할 때는 다시 바이트 덩어리로 바꿔서 쉘에 전달해야 합니다. 따라서 encode 합니다.
실행 결과는 다음과 같습니다.
이제는 한글이 깨지지 않을 뿐 아니라, 마침표 하나가 한글 음절 하나에 매치되는 것을 확인할 수 있습니다. cp949 로 저장된 스크립트를 수정해서 실행해도 동일한 출력이 나옵니다.
즉, 원래의 텍스트가 어떤 인코딩을 써서 저장되었든지간에, 일반 decode를 써서 문자열로 변환하고 나면 무조건 정규표현식의 "."는 "문자 하나"에 매치됩니다. 한글 영어 가릴 것 없이 적용할 수 있습니다.
한번 더 예를 들어,
/.{4}/
위 정규식은 바이트 덩어리를 대상으로 검사할 때는 4바이트에 매치되지만, 문자열을 대상으로 할 때는 바이트에 관계없이 문자 4개에 매치됩니다.
9) substr 의 예
substr 내장 함수는 문자열의 일부를 얻어낼 때 사용됩니다. (perldoc -f substr)
예를 들어 substr( "hello", 1, 2 );
는 hello 라는 문자열에서 1번 오프셋 뒤에 있는 "e"부터 시작해서 길이가 2인 문자열, 즉 "el"을 반환합니다. (제일 첫번째 문자는 오프셋 1이 아니라 0에 해당되니 조심)
문제는, 영어로 된 문자열은 상관이 없는데, 한글 문자열은 오프셋을 계산할 때도 길이를 계산할 때도 한글 한 글자가 몇 바이트냐를 따져야 된다는 겁니다.
- cp949 인코딩을 쓰는 경우 substr( "안녕하세요" , 2, 4 ) 가 "녕하"입니다.
- UTF-8 인코딩을 쓰는 경우 substr( "안녕하세요", 3, 6 ) 가 "녕하"입니다.
이것도, 문자열처럼 보이지만 사실은 바이트 덩어리였던 놈을 진정한 문자열로 변환하면 해결됩니다.
1
2 use Encode;
3
4 my $str = decode("cp949", "English와 한글");
5 my $length = length($str);
6
7 for (my $i = 1; $i < $length; $i++) {
8 printf "%2d : ", $i;
9 print "[",
10 encode("cp949", substr($str, 0, $i)),
11 "] [",
12 encode("cp949", substr($str, $i)),
13 "]\n";
14 }
위 코드는 "English와 한글"이라는 문자열을 두 조각으로 쪼개는 예제입니다.
- 4번 라인, 바이트 덩어리를 디코드해서 문자열로 변환했습니다.
- 5번 라인, 이제 length는 정확히 문자 갯수를 반환합니다. 11글자입니다.
- 7번 라인, $i 는 1부터 10까지 증가하며 루프를 돕니다
- 10번 라인, substr은 $str의 제일 처음(오프셋0)부터, $i개의 문자를 얻어냅니다. 이걸 출력하기 위해 다시 인코드합니다.
- 12번 라인, substr은 $str의 오프셋 $i 부터, 그 뒤의 문자 모두를 얻어냅니다. 즉 $i+1번째 문자부터 마지막 문자까지로 구성된 문자열을 반환합니다. 역시 인코드해서 출력합니다.
실행결과는 다음과 같습니다.
한글 영문 가리지 않고, 정확히 글자수를 세어서 앞뒤로 분리하는 걸 볼 수 있습니다.
이 코드를 decode, encode 없이 그냥 했다면 어떻게 될지 짐작해보시고 한번 직접 해보시기를 권장합니다.
10) 이 장의 내용 정리
글이 너무 길어지는 것 같아서 일단 여기서 끊고 다음 글에서 계속 쓰겠습니다.
이 장의 내용을 정리하면
1. "인간에게 의미있는 정보를 담고 있는 추상적인 문자열"과 "컴퓨터가 그 정보를 저장하기 위해 사용하는 구체적인 바이트 덩어리"를 구분해서 생각해야 한다. (사용자는 그럴 이유가 없지만 프로그래머는 그래야 한다)
2. Perl 은 "문자열"을 다룰 수 있으나, 기본적으로 우리가 작성해서 저장한 펄 스크립트는 "바이트 덩어리" 형태로 저장되어 있고, 따라서 별도의 지시가 없으면 바이트 단위로 연산을 하게 된다.
3. 안타깝게도 한글은 한개의 "문자"가 한개의 "바이트"에 대응되지 않기 때문에, 문제가 생긴다.
4. 따라서 Perl 프로그램 내에서 한글 영문을 가리지 않고 "문자열"로써 다루고자 하면 바이트 덩어리를 문자열로 변환, 즉 디코드해야 한다.
5. 일단 디코드된 문자열은, 정규식이나 각종 스트링 관련 함수에서 정확히 의미에 맞게 다룰 수 있다.
6. 문자열을 외부에 출력하기 전에는 다시 바이트 덩어리로 인코드해야 한다. 또 외부에서 입력을 받을 때도 바이트 덩어리 형태로 들어온다는 걸 염두에 둬야 한다.
정도가 되겠습니다.
다음 글에서는 디스크에 담긴 파일을 읽고 쓸 때의 인코딩/디코딩을 하는 예와, 매번 불편하게 decode, encode를 호출하지 않고 간편하게 처리하는 방법 등을 살펴 보겠습니다.
1장에 이어서, 이번 장에서는 파일을 다루는 데 연관되는 인코딩/디코딩 문제를 살펴보겠습니다.
1) 다시 표준입력 얘기부터
가계도 프로젝트에서 표준 입력 또는 파일을 읽어서 간단한 정보를 추출하여 출력하는 예를 다시 살펴 봅시다.
http://cafe.naver.com/perlstudy/689 여기 있던 코드 중에서, 줄번호와 각 라인의 길이를 출력하는 코드를 가져와서 살짝 고쳐서 사용해 보겠습니다.
1 #!perl
2
3 use strict;
4 use warnings;
5
6 while ( my $line = <STDIN> ) {
7 chomp $line;
8 print $., "\t", "[$line](", length($line), ")\n";
9 }
이 코드는 표준입력을 읽어서 각 라인의 길이를 출력합니다. 입력이 영문으로만 되어 있을 때는 아주 잘 동작하지만, 한글이 포함되면 문제가 있습니다.
다음과 같은 내용으로 입력 파일을 만들어서, cp949 인코딩으로 저장하고, 파일 이름은 "input1_cp949.txt"라고 하겠습니다.
hello
안녕하세요
반갑습니다요
이 파일은 cp949로 인코딩해서 저장되었습니다.
이제 실행해 보겠습니다. 한 번은 키보드로 직접 입력하고 또 한 번은 입력 리다이렉션을 써서 파일의 내용을 표준입력으로 집어넣겠습니다.
앞 장에서와 마찬가지로, 한글은 글자 하나당 길이가 2로 계산되고 있습니다.
해결책도 앞 장과 같습니다. 들어온 입력은 바이트 덩어리이므로 일단 문자열로 변환하여 계산하고, 출력할 때는 다시 바이트 덩어리로 변환합니다.
1 #!perl
2
3 use strict;
4 use warnings;
5 use Encode;
6
7 while ( my $line = <STDIN> ) {
8 chomp $line;
9 $line = decode("cp949", $line);
10
11 print $., "\t", "[",
12 encode("cp949", $line),
13 "](", length($line), ")\n";
14 }
9번 라인에서 문자열로 디코드, 12번 라인에서 출력을 위해 다시 인코드. 여러 번 반복해서 나왔던 형태입니다.
실행 결과:
1 [hello](5)
2 [안녕하세요](5)
3 [반갑습니다요](6)
4 [이 파일은 cp949로 인코딩해서 저장되었습니다.](27)
이제는 제대로 글자 수를 세고 있는 걸 볼 수 있습니다.
2) 매번 decode 하고 encode 하자니 너무 힘들어요
지난 장부터 지금까지 계속
- 입력을 받은 직후에는 decode를 해서 문자열로 변환하고
- 출력을 하기 직전에는 encode를 해서 바이트 덩어리로 재변환
하고 있습니다.
이건 너무 귀찮은 일입니다.
표준 입력으로 들어오는 건 자동으로 디코드하고, 표준 출력으로 나가는 건 자동으로 인코드하게 할 수 있습니다.
1 #!perl
2
3 use strict;
4 use warnings;
5 use Encode;
6
7 binmode STDIN, ":encoding(cp949)";
8 binmode STDOUT, ":encoding(cp949)";
9
10 while ( my $line = <STDIN> ) {
11 chomp $line;
12 print $., "\t", "[$line](", length($line), ")\n";
13 }
7번과 8번 라인에서 binmode 는 표준입력 파일핸들과 표준출력 파일핸들에다가 :encoding(cp949)
라는 레이어를 추가합니다.
기존에 표준입력, 표준출력과 우리가 만든 프로그램 사이의 데이타 흐름이 아래와 같다면
+------------------------+
+--------------+ | | +--------------+
| | 표준 입력 | | 표준 출력 | |
| 키보드 |+-------------------->| Perl 프로그램 |+-------------------->| 화면 |
| | | | | |
+--------------+ | | +--------------+
+------------------------+
binmode 로 레이어를 추가하면 다음과 같은 형태가 됩니다.
+------------------------+
+--------------+ +----------+ | | +----------+ +--------------+
| | | cp949 | | | | cp949 | | |
| 키보드 |+----| |---->| Perl 프로그램 |+----| |---->| 화면 |
| | | decode | | | | encode | | |
+--------------+ +----------+ | | +----------+ +--------------+
+------------------------+
이제는 우리가 일일이 변환하지 않아도, 표준입력으로 들어온 데이타는 자동으로 "문자열"로 변환되고 (이때 cp949 규약에 따라 변환됩니다) 또 표준출력으로 내보내는 데이타는 자동으로 "바이트 덩어리"로 다시 변환됩니다. (역시 cp949 규약에 따름)
따라서 10번-13번 라인에서는 제일 처음 봤던 코드와 똑같이, 별도의 encode, decode 를 하지 않은 채로 그냥 사용합니다.
실행 결과는 1)에서와 똑같이, 글자수를 제대로 세는 걸 확인할 수 있습니다.
d:\Work\Perl\PerlStudy_cafe\20110709_lecture_한글>perl 10_cat.pl < input1_cp949.txt
1 [hello](5)
2 [안녕하세요](5)
3 [반갑습니다요](6)
4 [이 파일은 cp949로 인코딩해서 저장되었습니다.](27)
만약에, 입력 파일이 cp949 가 아니라 UTF-8 로 작성되어 있었다면 어떻게 해야 할까요? 위에서와 비슷하게, 다음과 같은 내용으로 입력 파일을 만들어서, UTF-8 인코딩으로 저장하고, 파일 이름은 "input1_utf-8.txt"라고 하겠습니다.
hello
안녕하세요
반갑습니다요
이 파일은 UTF-8로 인코딩해서 저장되었습니다!!
위 프로그램에서 7번 라인만 바꿔줍니다.
7 binmode STDIN, ":encoding(UTF-8)";
입력 텍스트가 UTF-8 인코딩된 형태라도, 정확히 변환되어서 글자수를 제대로 세고 있습니다.
(** 다만 이 코드는 사용하기에 무리가 있는 것이, UTF-8로 된 파일을 입력 리다이렉션을 써서 읽을 수는 있지만 정작 키보드로 입력하는 건 제대로 처리를 못합니다. 키보드로 입력되는 데이타는 cp949 로 되어 있을테니까요. 애초에 표준입력과 표준출력이 서로 다른 인코딩 규약을 사용하는 건 흔치 않은 일입니다. **)
3) open 으로 파일을 열어서 읽거나 쓸 때
위에서는 표준 입력으로 입력받고, 표준 출력으로 출력했습니다.
파일 이름 두 개를 인자로 받아서, 첫번째 인자로 주어진 파일을 열어서 라인의 길이를 측정한 후 두번째 인자로 주어진 파일에 저장하도록 해 봅시다.
일일이 decode, encode 하는 코드는 계속 반복했으니, 이제부터는 생략하겠습니다. 곧바로 binmode 를 쓰는 코드를 보도록 하겠습니다:
1 #!perl
2
3 use strict;
4 use warnings;
5 use Encode;
6 use autodie;
7
8 my $in_file = $ARGV[0];
9 my $out_file = $ARGV[1];
10
11 open my $in, "<", $in_file;
12 open my $out, ">", $out_file;
13
14 binmode $in, ":encoding(cp949)";
15 binmode $out, ":encoding(cp949)";
16
17 while ( my $line = <$in> ) {
18 chomp $line;
19 print {$out} $., "\t", "[$line](", length($line), ")\n";
20 }
21
22 close $in;
23 close $out;
- 11,12번 라인에서 입력파일과 출력파일을 열고 파일핸들을 할당합니다.
- 14,15번 라인에서 두 파일핸들에 인코딩 레이어를 추가합니다.
- 17번 라인에서 $in을 통해서 읽고, 19번 라인에서 $out을 통해서 출력합니다. 여기서는 별도의 인코드/디코드 과정이 필요없습니다.
실행결과를 보면:
생성된 output.txt 파일의 내용을 보면 제대로 나오는 걸 볼 수 있습니다.
여기서는 입력 파일과 출력 파일의 인코딩이 반드시 동일할 필요도 없고, 특정한 인코딩을 사용할 이유도 없습니다. 자기가 원하면 출력 파일은 UTF-8 인코딩을 써서 저장하도록 할 수 있습니다. 아 물론 이건 출력 부분의 얘기고, 입력 쪽은 입력 파일이 저장될 때 사용된 인코딩을 따라줘야겠죠.
4) open 으로 파일을 열 때 - binmode 도 필요없다
open 후 binmode 를 써서 레이어를 추가하려니 일을 두 번 하는 것 같습니다. open 할 때 레이어를 같이 지정해 줄 수 있습니다.
11 open my $in, "<:encoding(cp949)", $in_file;
12 open my $out, ">:encoding(cp949)", $out_file;
위와 같이, open 을 호출할 때 인자 3개 형식으로 호출할 때, 두번째 인자에 입출력 방향을 나타내는 기호 뒤에 레이어를 추가로 적어줄 수 있습니다. 이 경우는 binmode 를 추가로 사용하지 않아도 됩니다.
5) null filehandle, "<>"의 경우 - open 프라그마를 사용하여 디폴트 입출력 레이어 지정
<>
연산자(다이아몬드 연산자라고도 합니다)는
- 명령행 인자가 있다면 그 인자들, 즉 @ARGV의 원소를 하나씩 읽어서 그 이름의 파일을 열어 한 라인씩 입력을 받고
- 명령행 인자가 없다면 표준 입력으로부터 한 라인씩 입력을 받습니다
([가계도 프로젝트 1.장] 참조 )
여기에 입력 레이어를 삽입하려면, 묘한 상황에 처합니다
- 명령행 인자가 없는 경우는 표준 입력으로부터 입력을 받으니까, STDIN 파일 핸들에 binmode 를 수행하면 되는데,
- 명령행 인자가 있다면 도대체 무슨 파일핸들에다 조작을 해야 하는가?
사실 이 경우는 ARGV 라는 이름의 파일 핸들이 관여가 됩니다만, 이 파일 핸들을 조작하는 것은 좀 까다롭습니다. 이건 옆으로 치워버리고, open 프라그마를 사용하도록 합니다. Perl 내장 함수 open과는 별개입니다. (perldoc open 참조)
1 #!perl
2
3 use strict;
4 use warnings;
5 use Encode;
6
7 use open IN => ':encoding(cp949)';
8 use open OUT => ':encoding(cp949)';
9 use open ':std';
10
11 open my $out, ">", "output2.txt";
12 while ( my $line = <> ) {
13 chomp $line;
14 print $., "\t", "[$line](", length($line), ")\n";
15 print {$out} $., "\t", "[$line](", length($line), ")\n";
16 }
- 7번 라인에서, 입력에 대해 cp949 규약을 써서 디코드하라고 선언합니다. 이 라인 이후부터는 open()을 써서 파일을 열어 읽을 때 저 레이어가 적용됩니다.
- 8번 라인에서, 출력에 대해 마찬가지로 cp949 규약을 써서 인코드하라고 선언합니다. 이 라인 이후부터는 open()을 써서 파일을 열어 데이타를 쓸 때 저 레이어가 적용됩니다.
- 9번 라인에는 open 뒤에 :std 라고만 적혀 있습니다. 표준입출력에 대해서, IN과 OUT에 정해진 대로 적용하라는 의미입니다. 따라서 표준입력, 표준출력 전부 cp949 규약을 써서 변환하게 됩니다.
- 11번 라인에서는 테스트를 위해 출력을 저장할 파일을 하나 엽니다. 이 때 입출력방향을 지정하는 ">" 인자에 따로 레이어가 명시되지 않았지만, 위에서 OUT 에 대해서 cp949 인코딩을 사용하라고 지정이 미리 되어 있으므로 자동으로 레이어가 적용됩니다.
- 12번 라인에서, "<>" 연산자를 사용해서 한 라인씩 읽습니다.
테스트하기 위해서, 출력도 두 가지로 나눕니다. 14번 라인에서는 표준 출력으로 출력하고, 15번 라인에서는 output2.txt 파일에 똑같은 출력을 기록합니다.
실행 결과를 살펴보겠습니다.
첫번째 줄을 보면, 입력 리다이렉션을 사용했으므로, 입력이 표준입력으로부터 들어갑니다.
또 첫번째 줄에 보면 실행할 때 "> 파일이름" 의 형태로 뒤에 적어주었습니다. 이것은 입력 리다이렉션과 반대로, 출력 리다이렉션입니다. 표준출력으로 나오는 것을 가로채서 파일에 저장해줍니다. 입력 리다이렉션과 마찬가지로 이것은 Perl 이 하는 게 아니라 명령 프롬프트 창이 처리해 주는 부분입니다. 어쨌거나, 표준출력으로 나오는 것을 가로채서 output.txt 라는 파일에 저장했습니다.
두번째 줄에서는 실행할 때 입력파일 이름을 인자로 주었습니다. 따라서 perl 이 저 파일을 열어서 읽습니다. 이 때 표준출력으로 나오는 것은 "output3.txt" 라는 이름으로 저장했습니다.
실행이 끝나고
- 표준 입력으로 읽었을 때의 출력결과 output.txt
- 명령행 인자로 받아서 열었을 때의 출력결과 output3.txt
- 두 경우 다 스크립트 내부에서 따로 저장한 output2.txt
(사실 두 번 실행을 시켰기 때문에 처음 실행할 때 저장된 output2.txt 는 덮어써 버려서 없어졌습니다만)
이 세 파일을 비교해보면 크기도 동일하고, 실제로 열어보면 완전히 내용이 똑같습니다. 이로써
- 표준 입력으로 입력을 받든 스크립트 내부에서 파일을 열어서 읽든 (<>를 썼기 때문에 명시적으로 open하진 않았지만), 제대로 디코딩되었다는 걸 확인할 수 있고
- 반대로 출력할 때도 표준 출력이든 명시적으로 open해서 파일에 출력을 하든 제대로 인코딩되었다는 것도 확인할 수 있습니다.
open 프라그마의 사용예제를 몇 가지 더 들어보자면:
1. 입력은 cp949로 된 것을 받는데, 내가 출력할 때는 UTF-8로 인코딩하고 싶다면?
8 use open OUT => ':encoding(UTF-8)';
8번 라인만 위와 같이 인코딩을 UTF-8로 명시해주면 됩니다. 이 경우는
- open을 써서 읽을 때와 표준 입력을 받을 때는 cp949를 쓰고
- open을 써서 쓸 때와 표준 출력으로 내보낼 때는 UTF-8을 씁니다
2. open을 쓸 때와 표준입출력을 별개로 다루고 싶다면?
':std'가 언급된 9번 라인은 제거하고, binmode를 써서 STDIN과 STDOUT에 적용될 레이어를 별도로 지정해 줍니다.
3. 위 코드에서는 IN과 OUT에 동일한 인코딩 레이어를 사용합니다. 이런 경우는 굳이 두번 나눌 필요 없이
7 use open IO => ':encoding(cp949)';
이렇게 "IO"에 대해서 레이어를 지정해주면 입력과 출력 양쪽에 적용되며, 아예
7 use open ':encoding(cp949)';
이렇게만 적어줘도 됩니다.
8) 스크립트 내에 들어있는 문자열 상수들을 일일이 디코딩하는 것도 귀찮아요 - utf8 프라그마
지난 1.장에서 스크립트 코드 내에 한글 문자열이 있는 경우:
my $str = "안녕하세요";
$str 에는 문자열이 담기는 게 아니라 바이트 덩어리가 담긴 상태이므로, 문자열로써 다루기 위해서는 $str 의 내용을 decode를 해야 했습니다.
my $str = "안녕하세요";
$str = decode("cp949", $str);
또는 한번에
my $str = decode("UTF-8", "안녕하세요");
그런데 이렇게 스크립트 안에 들어있는 모든 비영어권 문자열에 대해서 이런 디코드를 일일이 해주는 것도 매우 불편한 일입니다.
스크립트 내에 들어있는 문자열 상수들을, 바이트 덩어리가 아닌 추상적 문자열로 간주하여 처리하도록 지정할 수 있습니다. 다음 두 조건을 따라야 합니다:
- 스크립트 자체를 UTF-8 인코딩을 적용하여 저장하고,
-
utf8
프라그마를 사용
1 #!perl
2
3
4 use strict;
5 use warnings;
6 use Encode;
7 use utf8;
8
9 binmode STDOUT, ":encoding(cp949)";
10
11 my $eng = "hello";
12 print "[$eng][", length($eng), "]\n";
13 my $kor = "안녕하세요";
14 print "[$kor][", length($kor), "]\n";
15
16 if ( $kor =~ /하(.)/ ) {
17 print "'하' 다음에는 '$1'자가 있군요\n";
18 }
다시 한 번 얘기하지만, 위 코드를 디스크에 저장할 때는 UTF-8 인코딩을 사용하여야 합니다. 사용하시는 에디터에서 옵션을 잘 뒤져보면 저장할 때 사용할 인코딩을 지정할 수 있을 겁니다.
- 7번 라인에서 use utf8; 선언이 있습니다. 이 프라그마가 적용되는 스코프 내에서는 이제부터 스크립트 자체의 텍스트가 바이트 덩어리가 아니라 문자열로 간주됩니다.
- 9번 라인, 이제는 내부에서 다루는 데이타가 자동으로 디코드가 되니까, 출력할 때는 인코딩을 해야 합니다. 명령 프롬프트 창에서 출력할 거니까 cp949를 쓰도록 지정했습니다.
- 13번 라인, 처음처럼 그냥 문자열 상수 "안녕하세요"를 저장합니다. 따로 디코드하지 않습니다.
- 14번 라인, 이 문자열을 출력하고, 길이도 출력합니다.
- 16번 라인, 정규표현식을 지정해봅니다. 정규표현식 내에 적힌 "하" 역시 특정한 바이트 덩어리가 아니라 그냥 문자 "하"로 취급됩니다.
- 17번 라인, 정규표현식에서 마침표 하나에 매치되는 부분을 출력시켜봅니다.
실행 결과는 다음과 같습니다.
모든 것이 우리의 의도대로!!! 잘 처리되고 있습니다.
9) 이 장의 내용 정리
이 장의 내용을 정리하면,
1. 파일을 읽을 때는 파일의 내용은 바이트 덩어리이고, 이걸 문자열로써 다루기 위해서는 디코드해야 한다.
- 읽고 나서 읽은 내용을 decode()를 써서 디코드하거나,
- 읽을 파일 핸들에 binmode() 를 써서 입력 레이어를 삽입하여 자동으로 디코드 하거나
- 읽을 파일을 여는 시점에 open()의 인자로 입력 레이어를 명시해주거나
- open()을 하기 전에 미리 use open 프라그마를 써서 디폴트 레이어를 명시해 줄 수 있다
2. 문자열을 다룬 후에 파일에 저장할 때는 내가 희망하는 형태의 바이트 덩어리로 인코딩해야 한다
- 쓰기 전에 쓸 내용을 encode()를 써서 인코드하거나,
- 쓸 파일 핸들에 binmode() 를 써서 출력 레이어를 삽입하여 자동으로 인코드 하거나
- 쓸 파일을 여는 시점에 open()의 인자로 출력 레이어를 명시해주거나
- open()을 하기 전에 미리 use open 프라그마를 써서 디폴트 레이어를 명시해 줄 수 있다
3. "<>"는 파일핸들이 명시되지 않으니 use open을 써서 입력 레이어를 명시하면 된다
4. 프로그램 코드 내에 있는 문자열 상수는 use utf8 프라그마를 써서 자동으로 디코드된 상태로 사용할 수 있다.
다음 장에서는 이번 장에서 다루지 못한 몇가지 상황에서의 인코딩 이슈를 다루고 연재를 마무리하도록 하겠습니다.
1) 펄 코드의 인코딩
윈도우즈에서 텍스트 에디터를 열어서 펄 스크립트를 작성하고 저장하면, 보통은 윈도우에서 사용하는 기본 인코딩인 cp949로 인코딩되어서 저장될 겁니다. 이 스크립트를 윈도우에서 실행하면 실행환경(명령 프롬프트 창이라거나, GUI라거나) 역시 같은 인코딩을 쓰니까 별 무리 없이 입력과 출력이 되는 것처럼 보입니다.
그러나 실제로는 우리가 1장과 2장에서 봤듯이, 한글이 "문자열"이 아니라 "바이트 덩어리"로 취급되고 있기 때문에, 문자열 처리를 할 때 직관적인 처리가 힘들어집니다. 또한 다른 인코딩을 사용하는 시스템과 데이타를 교환할 경우 호환에 문제가 생길 우려도 있습니다.
다른 분들은 어쩌시나 모르겠는데 (이 글을 보시면 리플로 좀 알려주시면 좋겠습니다), 저는 그런 이유로 제가 작성한 펄 스크립트 코드는 UTF-8로 인코딩하여 저장하고, use utf8; 프라그마를 포함하고, 출력할 때는 STDOUT에 인코딩을 지정하는 형태로 씁니다.
(다만 이 경우 윈도우에서 perldoc 내_스크립트.pl 해서 POD 도움말을 볼 때 문제가 되더군요)
2) 파일 이름 인코딩
이 연재에서 파일을 읽거나 쓰기 위해서 열 때는 항상 파일 이름이 영문이나 특수문자, 숫자로만 되어 있는 걸로 했습니다. 그런데 파일이름이 "내사진.jpg" 와 같이 한글이 포함되어 있다면 어떻게 될까요?
파일을 열기 위해서는 디스크에서 찾아야 합니다. 디스크에는 어느 폴더에 들어있는 파일들의 이름 목록이나, 어느 파일이 디스크의 어디에 있는지 저장되어 있습니다. 문제는 이 때 파일 이름이 저장될 때도 역시나 특정한 인코딩 규약에 맞춰서 바이트 덩어리로 바뀌어 저장된다는 겁니다.
(애초에, 펄 프로그램 바깥에서 벌어지는 모든 일들은 바이트 덩어리를 조작한다고 간주하셔도 무방할 겁니다)
그러면 파일 이름이 디스크에 저장될 때는 어떤 인코딩 규약을 따르는가?
- 역시나 한글 윈도에서는 cp949를 따릅니다. (실제로는 내부적으로는 아닐 수도 있습니다만, 적어도 cp949에 맞춰 파일 이름을 검색하면 찾을 수 있습니다)
- 리눅스/유닉스에서는 사용자의 지역 정보, locale 설정에 따라 다릅니다. 예전에는 euc-kr이었고, 요새는 UTF-8이 대세입니다만, 사용자가 환경 변수 설정에 따라 달라질 수 있습니다.
따라서 파일을 열 때, 파일명 역시 해당 인코딩 규약에 맞춰서 인코딩된 상태여야 합니다.
예를 들어보겠습니다. 제 폴더에 문서 두 개가 있습니다.
- 하나는 "한글문서_cp949.txt"이고, 그 안에는 한글텍스트가 cp949 인코딩으로 저장되어 있습니다.
- 또 하나는 "한글문서_utf-8.txt"이고, 그 안에는 한글텍스트가 UTF-8 인코딩으로 저장되어 있습니다.
이 두 파일을 읽어서 내용을 출력하는 코드입니다. 지난 장에서 다뤘던 내용들이 동시에 등장합니다.
1 #!perl
2
3
4 use strict;
5 use warnings;
6 use autodie;
7 use Encode;
8 use utf8;
9
10 binmode STDOUT, ":encoding(cp949)";
11
12 {
13 my $filename = encode("cp949", "한글문서_cp949.txt");
14 open my $in, "<:encoding(cp949)", $filename;
15 while ( <$in> ) {
16 chomp;
17 print "[$_][", length($_), "]\n";
18 }
19 close $in;
20 }
21
22 {
23 my $filename = encode("cp949", "한글문서_utf-8.txt");
24 open my $in, "<:encoding(utf-8)", $filename;
25 while ( <$in> ) {
26 chomp;
27 print "[$_][", length($_), "]\n";
28 }
29 close $in;
30 }
- 8번 라인에서, use utf8 프라그마를 사용했습니다. 따라서 이하 코드에서 문자열 상수는 전부 디코딩된 "문자열"로 처리됩니다.
- 10번 라인, 명령프롬프트 창에서 출력할 거라서 표준출력의 인코딩을 cp949로 지정했습니다.
- 12-20번 라인에서는 "한글문서_cp949.txt" 파일을 열어서 읽습니다.
- 13번 라인, 파일명이 코드 내에 그대로 적혀 있는데, 위에서 utf8 프라그마를 썼으니 이 파일명은 "문자열"입니다. 이 파일을 디스크에서 찾기 위해서는, 파일명을 "바이트 덩어리"로 바꿔서 운영체제에 부탁해야 합니다. 따라서 인코딩해야 합니다. 윈도우즈는 cp949 인코딩이 기본이므로, cp949 규약에 맞춰 인코딩합니다.
- 14번 라인, 파일을 엽니다. 그런에 이 파일의 "내용"이 cp949로 인코딩된 한글 텍스트라고 그랬습니다. 따라서 읽을 때 cp949에 따라서 디코드하라고 레이어를 지정해줍니다.
- 15번 라인에서 읽고
- 17번 라인, 읽은 라인의 내용과 길이를 출력합니다.
- 22-30번 라인에서는 "한글문서_utf-8.txt" 를 열어서 읽습니다.
- 23번 라인, 13번 라인과 마찬가지로 파일명을 인코드해야 하는데, 역시나 cp949 로 인코드해야 합니다. (UTF-8이 아닙니다!) 파일의 내용과 무관하게 파일명은 시스템에서 정해진 규약에 맞춰 인코딩되어 있기 때문입니다.
- 24번 라인, 이 파일을 읽을 때는 내용을 UTF-8 에 따라서 디코드하라고 레이어를 지정해야 합니다.
- 나머지는 앞과 동일
실행해보겠습니다
처음 두 줄은 cp949 문서, 다음 두 줄은 UTF-8 문서를 읽고 출력한 내용입니다. 제대로 읽었고, 제대로 문자열 길이를 측정했고, 제대로 명령 프롬프트 창에 출력하고 있습니다.
위 코드에서는 utf8 프라그마를 썼기 때문에 일부러 파일명을 다시 인코딩해야 했습니다만, 다음과 같은 경우도 있을 수 있습니다.
- utf8 프라그마를 쓰지 않고 스크립트를 그냥 cp949 로 저장했다면, $filename 은 encode() 하지 않고 그냥 문자열 상수를 써도 될 겁니다. 문자열 상수가 결국 바이트 덩어리인 상태니까요
- 파일 이름을 사용자가 키보드로 입력한다면, 입력이 들어올 때 cp949 상태로 들어올 테니 따로 encode() 하지 않고 그대로 써도 될 겁니다. (물론 표준입력을 자동으로 디코드하고 있는 상태라면 다시 인코드해줘야겠죠)
3) 기존 데이타의 인코딩 알아맞추기
1장부터 지금까지는, 우리가 항상 "들어올 입력이 어떤 인코딩으로 저장되어 있는지 미리 알고 있다"고 가정했습니다. 그래서 해당 인코딩 규약을 decode()할 때 명시했습니다.
때로는 입력이 어떤 인코딩으로 되어 있는지 알 수 없을 때가 있습니다.
- 내가 만든 파일이 아니라면 그 파일의 내용이 어떤 인코딩으로 되어 있는지 알 수 없습니다.
- 네트워크로 전송받은 내용은 내가 알 수 없습니다.
- 내 컴퓨터가 아닌 다른 시스템에서 수행될 경우는 그 시스템이 표준입출력 또는 파일이름을 어떻게 인코딩하는 지 알 수 없습니다. (이 경우는 환경 변수 등을 검사해서 알아내면 되겠습니다만)
- 기타 등등, 암튼 내가 만든 게 아니면 모르는 게 일반적입니다.
이런 경우, 데이타와 별개로 그 데이타에 사용된 인코딩 정보가 전달된다면 모르겠지만 이걸 항상 기대할 수도 없습니다.
어떤 데이타가 무슨 인코딩으로 되어 있는건지 정확히 알 수 있는 방법이 있는가? 이 질문에 대한 답은 저도 모릅니다. 게다가 저도 확신은 없습니다만, 어떤 인코딩은 아예 정확히 알아내는 게 불가능할 수도 있습니다. 예를 들어 한글을 인코딩하기 위한 euc-kr 과 일본어 문자를 인코딩하기 위한 euc-jp의 경우 거의 동일한 형태로 문자와 코드간에 매핑이 되어 있어서... 인코딩된 데이타를 보면서 "이게 한국어를 euc-kr로 인코딩한 건지, 일본어를 euc-jp로 인코딩한 건지" 구분하는 게 어려운 걸로 압니다.
하지만 데이타의 인코딩을 "추측"할 수 있는 방법은 있습니다. 그 중 제가 아는 것은 Encode::Guess 코어 모듈입니다.
자세한 것은 perldoc 문서의 synopsis 를 보면서 얘기하도록 하겠습니다:
(전체 문서는 http://perldoc.perl.org/Encode/Guess.html )
1
2
3 use Encode;
4 use Encode::Guess qw/euc-jp shiftjis 7bit-jis/;
5 my $utf8 = decode("Guess", $data);
6 my $data = encode("Guess", $utf8);
7
8
9 use Encode::Guess;
10 my $enc = guess_encoding($data, qw/euc-jp shiftjis 7bit-jis/);
11 ref($enc) or die "Can't guess: $enc";
12 $utf8 = $enc->decode($data);
13
14 $utf8 = decode($enc->name, $data)
- $data 변수에 어떤 바이트 덩어리가 담겨 있는 상태입니다.
- 4번 라인, Encode::Guess 모듈을 불러오는데, 이 때 뒤에 "조사할 대상 인코딩"의 이름을 나열해 줍니다. 위에서 말했듯이 어떤 인코딩은 서로 구분이 불가능(하거나 아주 어렵거나)하며, 따라서 모든 인코딩을 다 찾아줄 수는 없고, 사용자가 지정한 인코딩들 중에 어느 인코딩에 해당하는지를 추측해줍니다. 기본적으로 ascii / utf8 / UTF-16 with BOM / UTF-32 with BOM 네 가지는 조사 대상에 포함이 되어 있습니다. 여기에 추가로 조사할 인코딩 목록을 적어주는 겁니다. 위 예제에서는 일본어를 인코딩하는 데 사용되는 세 가지 인코딩 방법을 나열했습니다.
- 5번 라인, 이제 $data 의 내용을 디코드해서 추상적인 "문자열"로 변환하여 $utf8 이라는 변수에 넣습니다. Encode 모듈에서 사용하는 decode() 함수를 쓰는데, 인코딩 이름을 넣는 첫번째 인자 자리에 "Guess"라고 넣어줍니다. 그러면 $data 의 인코딩을 추측해서, 찾아낸 인코딩 규약에 맞춰 디코드해 줍니다.
- 6번 라인은 역으로, 문자열을 인코드해서 바이트 덩어리로 만드는 예인데, 주석에도 나와 있지만 이건 실제로는 동작하지 않습니다. 상식적으로, 문자열을 인코드할 때는 내가 어떤 규약에 맞추어 인코드할지를 지정해줘야 하는데, 그걸 지정하지 않고 "추측해서" 인코드하라는 게 말이 안 됩니다.
이렇게 해서 $data 를 디코드할 수 있습니다만, 문제는 $data 에 들어가 있는 값이 제대로 된 값이 아니라서, (예를 들어 어느 인코딩에서도 생성될 수 없는 바이트값이 들어가 있다던가) 인코딩을 정확히 알아내지 못하는 경우입니다. 이런 에러 처리를 위해서 이왕이면 9번-14번 라인에 나온 것처럼 사용하는 것이 좋습니다.
- 9번 라인에서 Encode::Guess 모듈을 로드하는데, 이번에는 체크할 "인코딩 후보"를 명시하지 않았습니다.
- 10번 라인, 모듈에서 제공하는 guess_encoding() 함수를 쓰는데, 이 때 두번째 인자로 인코딩 후보 리스트를 넘겨줍니다.
- 11번 라인, 10번 라인에서 만일 인코딩을 추측해내는데 성공하면, $enc 에는 해당 데이타에 사용된 인코딩 규약에 대한 정보가 담겨 있는 레퍼런스가 들어갑니다. ref()는 인자가 레퍼런스(대상이 무엇이든)인지 아닌지를 검사합니다. 참이면 레퍼런스의 대상의 타입이름을 반환하고, 레퍼런스가 아니라면 빈 스트링을 반환합니다. 따라서 이 값을 검사해서 추측에 성공했는지를 알아낼 수 있습니다. 여기서는 추측에 실패하면 die합니다.
- 12번 라인, 추측에 성공한 경우는 12번 라인 또는 14번 라인에서와 같이 하여서 디코드할 수 있습니다.
이걸 응용해서, 지금까지 연재에서 사용했던 입력 파일들의 인코딩을 추측하는 프로그램을 만들어 보겠습니다:
1 #!perl
2
3 use strict;
4 use warnings;
5 use autodie;
6 use Encode;
7 use Encode::Guess;
8 use utf8;
9
10 binmode STDOUT, ":encoding(cp949)";
11
12 my $filename = $ARGV[0];
13 open my $in, "<", $filename;
14
15 while ( my $line = <$in> ) {
16 chomp $line;
17
18 my $enc = guess_encoding( $line, qw/cp949/ );
19
20 if (not ref($enc)) {
21 print "인코딩 추측 실패\n";
22 next;
23 }
24
25 print "[", $enc->name, "]";
26 my $str = $enc->decode($line);
27 print "[$str]\n";
28 }
- 18번 라인, 파일의 각 라인을 읽어서 그 라인이 무슨 인코딩으로 되어 있는지를 추측합니다. 이때 조사할 후보는 utf-8을 비롯한 기본후보에 추가로 cp949 까지입니다.
- 20-23번 라인, 추측에 실패하면 그 라인은 포기하고 다음 입력 라인으로 통과
- 25-27번 라인, 추측에 성공하면 그 인코딩의 이름을 출력하고, 그 다음 라인의 내용을 디코드하고, 그걸 출력합니다. 이때 출력은 명령 프롬프트 창으로 나오니까 cp949로 인코딩하도록 10번 라인에서 지정했습니다.
실행결과를 보겠습니다.
먼저, cp949로 인코딩되었던 입력파일을 읽도록 했습니다.
- 첫번째 라인은 영문밖에 없기 때문에, ascii 코드로 인코딩했다고 추측합니다.
- 두번째부터 네번째 라인까지는 cp949로 인코딩되었다는 걸 맞췄습니다.
그 다음은 UTF-8로 인코딩되었던 입력파일을 읽도록 했습니다.
- 역시 첫번째 라인은 ascii 로 추측했고
- 나머지 라인은 utf8 이라고 제대로 맞추었고, 그에 따라 제대로 디코드하고 다시 cp949로 변환하여 출력했습니다.
만약 18번 라인에서 체크할 후보를 다음과 같이 했다면
18 my $enc = guess_encoding( $line, qw/cp949 euc-kr/ );
cp949로 인코딩된 문서의 경우는 추측에 실패했다고 나옵니다. 지난 장에서 얘기했듯이 cp949와 euc-kr은 거의 동일한 규약이라서 (cp949는 euc-kr에 없는 문자가 추가로 더 들어가 있음), 이 둘을 구분하지 못합니다.
4) 서로 다른 인코딩 간의 변환
우리가 지금까지 다룬 내용은, "바이트 덩어리"를 "문자열"로, 또는 그 반대로 변환하는 것이었습니다.
때로는, A라는 규약으로 인코딩된 바이트 덩어리를, B라는 규약으로 인코딩된 바이트 덩어리로 바꿔야 할 때가 있습니다.
뭐 여기까지 읽으셨다면 쉽게 하실 수 있겠습니다만... 그냥 코드만 보여드리겠습니다
my $string = decode("cp949", $data);
my $newdata = encode("UTF-8", $string);
my $newdata = encode( "UTF-8", decode("cp949", $data) );
from_to( $data, "cp949", "UTF-8" );
Encode 모듈에서 제공하는 from_to()라는 함수가 처음 등장했습니다.
5) Perl 내부에서 "문자열"을 저장하는 방법
이 섹션은.... 몰라도 지금까지 다룬 내용을 써먹는데는 큰 지장 없겠습니다만, 이왕 여기까지 온 김에...
게다가 Perl 의 다국어 처리 문제 때문에 여러가지 문서를 보다보면 이 얘기가 나올 것이기 때문에, 미리 맛뵈기로 다룹니다.
(저도 자세히는 모릅니다)
우리가 디코드를 하면 이제는 바이트 덩어리가 추상적인 "문자열"로 변환된다고 했습니다.
그런데 여전히 이 "문자열"도 컴퓨터 메모리 안에 저장되기 위해서는 결국 0또는 1의 조합으로 나타나야 합니다. 그러면 이때는 어떻게 나타낼까, 즉 "Perl 내부에서는 문자열을 어떤 형태로 변환해서 보관해둘까"라는 의문이 들 수 있습니다.
간단히 말씀드리면
- UTF-8 인코딩 규약에 따라 인코드하고, (일부 시스템에서는 다른 규약을 사용)
- 이 데이타가 Perl 내부에서 다루는 문자열이라는 걸 별도로 표시해 둡니다. (이 표시를 UTF8 flag 라고 합니다)
특히나 여러 Perldoc 문서에서, 이 "문자열"을 가리켜서 "utf8 형태"라고 말하기도 해서 좀 혼동스러운 면이 있습니다.
"utf8"은 따라서
- 어떤 경우는 그냥 "UTF-8" 인코딩 규약을 나타내는 다른 이름이기도 하고
- 어떤 경우는 "UTF-8로 인코딩되어 있고 UTF8 flag 가 켜져 있는 상태로 저장되어 있는 문자열" = "펄 내부 문자열" = "디코딩된 상태의 문자열" 을 의미하기도 합니다.
상황과 문맥에 맞춰 읽어야 합니다 :-)
좀 더 자세한 내용은 [aero님의 "Unicode in Perl"]을 참조하세요.
6) 마치며
다국어를 컴퓨터에서 표현하기 위한 여러가지 표준과 규약은... 학자와 엔지니어(그리고 아마도 경제인들과 정치인들까지)들이 수십년에 걸쳐 현재까지 논의하며 만들어낸 것이다보니... 어쩌면 어려운 게 당연합니다.
저도 Perl을 처음 접하고 펄로 홈페이지를 만들면서 한글 처리에서 난관에 부딪혔을 때, 이걸 해결해보겠다고 Encode 모듈 문서를 비롯해서 이것저것 읽으려고 시도해봤으나, 용어부터 시작해서 저 규약에 관한 이해가 안 되어 있다보니 perldoc 문서들의 내용도 전혀 못 알아듣겠더군요. 그래서 읽다가 포기하고, 몇 달 후에 다시 맘먹고 읽으려다가 포기하고... 그러길 지금까지 반복하고 있습니다.
(솔직히 말해서 이번 연재에서 언급한 perldoc 문서 중에 제가 제대로 처음부터 끝까지 읽은 건 Encode::Guess 모듈 문서뿐입니다. Encode, utf8 이런 문서들은 아주 일부만 읽었지요. 나머지 부분은 안 읽었다기보다는 못 읽겠더라고요 이해가 안 되어서ㅠㅠㅠㅠ)
게다가 Perl은 오래전 버전에서 다국어를 지원하기 위한 모듈이나 문서가 현재 버전까지 내려오고 있어가지고, 정확히 무슨 문서부터 읽어야 큰 그림이 그려지고 최근 경향을 따라갈 수 있는지, 지금도 모르겠습니다.
이 문제 때문에 다른 분들도 종종 곤란을 겪고 질문을 올리시기도 하고 그런데... 매번 제대로 된 문서, 특히나 실제 사용에 직접적으로 참조할 수 있는 일목요연한 문서, 가장 중요한 건 한글로 적혀 있는(^^;) 문서가 있으면 좋겠다고 생각해 오면서, 저도 아는 것도 부족하고 여유도 없고 해서 손가락만 빨고 있었습니다. 이 Perl과 한글 연재가 그런 문서 중에 작게나마 일부가 될 수 있으면 좋겠습니다.
내용에 오류가 있다거나 보충할 만한 것의 지적은 언제든지 환영합니다.
2. 기타 & Comments
컴퓨터분류