열한번째 날: 유니코드를 활용한 정규 표현식

저자

@newbcode - 사랑스러운 딸 바보 도치파파. 리눅스의 모든것 공동저자.

시작하며

유니코드(Unicode)는 전 세계의 모든 문자를 컴퓨터에서 일관되게 표현하고 다룰 수 있도록 설계된 문자 집합입니다. 이라는 한국어 문자는 10진수 54148에 일대일 대응됩니다. 코드포인트(codepoint)라고 하는 이 숫자는 보통 앞에 U+를 덧붙인 16진수 형태로 표기합니다. 54148을 16진수로 표현하면 D384이며 유니코드 표현으로는 U+D384라고 씁니다.

정규 표현식을 사용하다보면 "한글과 영어를 분리하고 숫자와 문자등을 쉽게 파싱을 하는 방법은 없을까?"하는 고민을 자주하게 됩니다. 펄의 내부 유니코드 속성에 대해 조금 깊게 들여다 보고 유니코드의 속성을 활용해 조금 더 정확하고 효율적으로 정규 표현식을 활용하는 방법에 대해 알아봅니다.

준비물

필요한 모듈은 다음과 같습니다.

Encode 모듈은 코어 모듈이므로 별도로 설치할 필요는 없습니다. HTTP::Tiny의 경우 펄 5.14 이후 버전(정확히는 v5.13.9)의 경우 코어 모듈로 지정되었으므로 역시 별도로 설치할 필요는 없습니다. 하지만 HTTP::Tiny는 지속적으로 버전이 갱신되고 있으므로 가능하면 최신 버전으로 설치하도록 합니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan HTTP::Tiny

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan HTTP::Tiny

유니코드와 length 내장 함수

펄에서의 문자열은 바이트 문자열유니코드 문자열 두 가지 유형이 있습니다. 유니코드 문자열일 경우 바이트 문자열과 비교했을 때 바이트당 글자 길이가 다릅니다. 다음은 바이트 문자열을 사용한 예제입니다.

#!/usr/bin/env perl

#
# FILE: unicode-1.pl
#

use strict;
use warnings;

my $x = '☺';
printf "%s(%d)\n", $x, length($x);

실행하면 length 내장 함수는 유니코드가 아닌 경우 해당 문자열의 바이트 크기를 반환합니다.

$ perl unicode-1.pl
☺(3)

다음은 유니코드 문자열을 사용한 예제입니다.

#!/usr/bin/env perl

#
# FILE: unicode-2.pl
#

use utf8;
use strict;
use warnings;
use Encode qw( encode );

my $x = '☺';
printf "%s(%d)\n", encode( 'utf-8', $x ), length($x);

실행하면 length 내장 함수는 유니코드일 경우 해당 문자열의 길이, 즉 글자 수를 반환합니다.

$ perl unicode-2.pl
☺(1)

유니코드 문자열을 사용하기 위해 use utf8; 선언해서 utf8 프라그마를 켜고 있으며 Wide character ... 경고를 방지하기 위해 출력 시점에 Encode 모듈을 이용해 명확하게 출력할 터미널의 문자셋에 맞게 인코딩(예제에서는 리눅스기 때문에 UTF-8로 인코딩)합니다. Wide character ... 경고는 2013년 펄 크리스마스 달력의 둘째 날 기사를 참고하세요.

유니코드와 \w 정규 표현식

펄에서는 특정 유니코드 문자에 일치시키기 위한 용도의 \u라는 메타 문자가 있습니다. 보통 4자리 16진수로 표현하므로 \uD384는 유니코드 문자 에 일치시킬 수 있습니다. 이 때 \uD384U+D384라는 유니코드 문자를 일치시키라는 의미만 가지며 실제로 비교할 바이트에 대한 내용은 전혀 지정하지 않는다는 점이 중요합니다. 실제 비교할 바이트는 프로그램 내부에서 유니코드 코드 포인트를 표기하기 위해 사용하는 인코딩 방법에 따라 달라집니다. 원래는 유니코드는 코드 포인트와 문자를 1:1로 대응시키기 위해 만든 것이지만 하나의 문자를 다른 코드로 표현할 수도 있습니다. 예를 들어 U+00E0àa를 나타내는 U+0061̀\를 나타내는 U+0300으로 구성됩니다. 이렇게 여러 방법으로 인코딩될 수 있기 때문에 실제 정규 표현식에의 유니코드 일치 결과는 예상과 다를 수 있습니다.

다음은 입력한 문자에 대해 단어를 일치시키는 간단한 프로그램입니다.

#!/usr/bin/env perl

#
# FILE: unicode-3.pl
#

use v5.14;
use strict;
use warnings;

chomp( my $input =(<DATA>) );

if ( $input =~ /\w+/ ) {
    say "matched";
}
else {
    say "not matched";
}

__DATA__
ááá

실행 결과는 다음과 같습니다.

$ ./unicode-3.pl
not matched

보통 \w+를 이용해서 단어를 일치시키는데 ááá는 단어로 인식을 하지 못합니다. \w[A-Za-z0-9_]로 63개의 문자를 의미하기 때문에 á를 일치시키지 못한 것입니다. length 때와 마찬가지로 utf8 프라그마를 켜주면 \w가 알파벳 뿐만 아니라 유니코드의 글자(letter) 영역까지 일치를 시켜줍니다.

#!/usr/bin/env perl

#
# FILE: unicode-4.pl
#

use v5.14;
use utf8;
use strict;
use warnings;

chomp( my $input =(<DATA>) );

if ( $input =~ /\w+/ ) {
    say "matched";
}
else {
    say "not matched";
}

__DATA__
ááá

이제는 원하는대로 동작합니다. :)

$ perl unicode-3.pl
matched

유니코드 속성

유니코드의 겉만 보면 문자와 코드 사이의 대응관계(코드포인트)에 불과하지만 이 안에는 사실 더 많은 내용이 들어있습니다. 예를 들어 "이 문자는 소문자이다"라거나 "이문자는 오른쪽에서 왼쪽으로 써야 한다", "이 문자는 다른 문자와 함께 쓰기 위해 만들어진 기호이다"와 같은 각 문자의 속성도 정의되어 있습니다. 펄은 이러한 유니코드의 속성을 활용 할 수 있게 설계되어 있으며 \p\P를 사용해서 표현할 수 있습니다.

기본적인 속성은 다음과 같습니다.

보통 \w+를 이용해서 단어를 일치시키는데 \w[A-Za-z0-9_]로 63개의 문자를 의미합니다. utf8 모드일 경우에는 알파벳 뿐만 아니라 유니코드의 글자(letter)까지 확장해서 일치를 시킵니다. 하지만 단어이지만 숫자를 제외하고 일치시키고 싶다면 어떻게 해야할까요? \w+를 사용하는 대신 \p{L}+를 사용하면 되겠죠. :-)

유니코드의 스크립트

유니코드 스크립트란 특정 언어에만 적용되게 만든 문자(코드 포인트)의 그룹이라고 말 할수 있습니다. 시스템에 따라 \p{......}를 사용하여 이 스크립트에 속하는 문자를 찾을 수도 있습니다. 예를 들어 \p{Hangul}이라고 하면 한글 쓰기 체계에 속하는 문자에 매치됩니다. 한글, 영어, 그리스어를 분리하는 프로그램은 아래와 같습니다.

#!/usr/bin/env perl

#
# FILE: unicode-5.pl
#

use v5.14;
use strict;
use warnings;

binmode STDIN,  "encoding(utf8)";
binmode STDOUT, "encoding(utf8)";

chomp( my $input =(<STDIN>) );

say "matched: korean($&)" while $input =~ /\p{Hangul}+/g;
say "matched: latin($&)"  while $input =~ /\p{Latin}+/g;
say "matched: greek($&)"  while $input =~ /\p{Greek}+/g;

binmode의 두 번째 인자에 encoding(...)를 사용하면 지정한 인코딩에 맞게 적절히 인코딩/디코딩을 수행합니다. 예제의 코드는 표준 입력으로 들어오는 것은 자동으로 디코드하고, 표준 출력으로 나가는 것은 자동으로 인코드합니다.

다음은 실행 결과입니다.

$ perl a.pl
당신을 사랑합니다.  I love you.  Σ 'αγαπώ.
matched: korean(당신을)
matched: korean(사랑합니다)
matched: latin(I)
matched: latin(love)
matched: latin(you)
matched: greek(Σ)
matched: greek(αγαπώ)

실행 시 한글과 영어 그리스어를 모두 입력하면 펄이 멋지게 분리시켜줍니다. :)

크리스마스 선물 #1: 사전에서의 언어구분

웹 문서에서 영어나 일본어, 한자 등을 인식하고 싶을 때는 \w를 쓰거나 복잡한 정규 표현식을 사용해야 합니다. 하지만 \p 문법을 이용하면 간단히 일치시킬 수 있습니다. 네이버 사전을 예를 들어보죠. 다음은 한글과 영어, 일본어, 한자 등의 문자를 추출합니다.

#!/usr/bin/env perl

#
# FILE: naver-dic.pl
#

use strict;
use warnings;

use Encode qw( decode );
use HTTP::Tiny;

die "Usage: $0 <word>\n" unless @ARGV == 1;

my $word = shift;

my $url = "http://dic.naver.com/search.nhn?dicQuery=$word&query=$word&target=dic&ie=utf8&query_utf=&isOnlyViewEE=";

my $res = HTTP::Tiny->new->get($url);
die "failed($res->{status}): $res->{reason}\n" unless $res->{success};

my $html = decode( 'utf-8', $res->{content} );

my @korean   = $html =~ m{\p{Hangul}+}gsm;      # 한글만 가져오기
my @english  = $html =~ m{\p{Latin}+}gsm;       # 영어만 가져오기
my @japanese = $html =~ m{\p{Katakana}+}gsm;    # 일본어만 가져오기
my @chinese  = $html =~ m{\p{Han}+}gsm;         # 한자만 가져오기

이처럼 유니코드와 정규표현식을 잘 활용하면 손쉽게 각 언어별로 글자를 모아 2차 가공을 할 수 있습니다. :)

크리스마스 선물 #2: 본문 중간의 특수 문자 변경

웹 문서나 일반 문서를 가공할 때 본문 중간에 특수 문자가 있어 애를 먹는 경우가 많습니다. 이런 특수 문자를 모두 찾아 변경하거나 제거하면 예외 상황이 줄어들어 후처리하기 용이합니다. 다음은 특수 문자를 변경하거나 제거하는 기교를 부린 짧은 예제입니다.

#!/usr/bin/env perl

#
# FILE: love-perl.pl
#

use v5.14;
use utf8;
use strict;
use warnings;

binmode STDIN,  "encoding(utf8)";
binmode STDOUT, "encoding(utf8)";

chomp (my $input =(<DATA>));

say $1 while $input =~ s/(\p{Symbol})/Love/g;
say $input;

__DATA__
I ♡ Perl.

다음은 실행한 결과입니다.

$ perl a.pl
♡
I Love Perl.

정리하며

유니코드 문자열을 사용하면 간단하면서도 풍부한 정규 표현식을 사용할 수 있습니다. 펄은 내부의 유니코드 구현이 UTF-8로 되어있어 너무나도 자유롭게 유니코드 일치를 사용할 수 있죠. 마치 유니코드를 사용하는데 있어 펄은 거리낌이 없다는 느낌이랄까요?

Enjoy Unicode & Perl! ;-)

참고문서

blog comments powered by Disqus