텍스트 정규화(Text Normalization)

Java SE 2007/03/13 12:06 Posted by Sun

글쓴이: Sergey Groznyh

텍스트 정규화(text normalization)는 미리 정의된 규칙에 맞추어 텍스트를 변환하는 작업으로, 여기에는 공백 제거, 구두점 삭제, 대문자/소문자 변환 등이 포함된다. 이번 테크 팁에서는 텍스트 정규화의 중요한 형태 중 하나인 유니코드 텍스트 전처리에 관해 살펴보도록 한다. 달리 명시하지 않는 한, 본 기사에서 유니코드라는 용어는 Java SE 6 플랫폼에서 지원하는 유니코드 버전인 유니코드 4.0을 지칭한다.

유니코드 표준은 문자와 문자 시퀀스 간의 두 가지 등가(equivalence)를 정의하는데, 이는 곧 규범적 등가(canonical equivalence)와 호환성 등가(compatibility equivalence)이다. 규범적 등가의 일례로 문자와 그에 상당하는 조합 시퀀스를 들 수 있다. 예를 들어, 유니코드 문자 '?'(LATIN CAPITAL LETTER C WITH CEDILLA)는 유니코드 문자 값 U+00C7을 가지고, 유니코드 문자 시퀀스 U+0043 U+0327 역시 '?' 문자를 생성한다. 시퀀스에는 LATIN CAPITAL LETTER C, 그리고 그 다음에 나오는 COMBINING CEDILLA의 문자 값들이 포함된다. 단일 문자와 문자 시퀀스는 시각적으로 구별이 불가능하고 텍스트 비교 및 렌더링의 목적상 정확히 동일한 의미를 가지므로 규범적 등가의 특성을 지닌다고 할 수 있다.

한편, 호환성 등가(compatibility equivalence)는 주로 동일한 문자나 문자 시퀀스의 대체 시각 표현들을 정의하는 레거시 문자 세트들을 취급하는데, 호환성 등가의 일례로 DIGIT TWO 문자 '2'(U+0032)와 SUPERSCRIPT TWO 문자 '²'(U+00B2) 간의 등가를 들 수 있다. 두 문자 모두 ISO/IEC 8859-1 (Latin1) 문자 세트 내에 존재한다. DIGIT TWO 문자 '2'와 SUPERSCRIPT TWO 문자 '²'는 동일한 기본 문자의 이형이기 때문에 호환성 등가의 특성을 지닌다. 한편 이 문자들은 시각적으로 구별이 가능하고 추가적인 의미 정보를 가지기 때문에 규범적 등가에는 해당되지 않는다.

유니코드 텍스트 정규화는 문자와 문자 시퀀스를 특정 등가에서 다른 등가로 변환하는 작업으로, 유니코드는 다음 네 가지의 정규화 표준을 정의하고 있다.

NFC
Normalization Form Canonical Composition. 문자를 분해한 다음 규범적 등가로 재구성한다. 예를 들어, 가능하면 "글자+조합형 마크"와 같은 시퀀스를 구성하여 단일 문자를 만든다.
NFD
Normalization Form Canonical Decomposition. 문자를 규범적 등가로 분해한다. 예를 들어, 사전 구성된 문자 '?'(U+00C7)를 기본 문자와 조합형 부호를 포함하는 조합 시퀀스로 변환한다.
NFKC
Normalization Form Compatibility Composition. 문자를 호환성 등가로 분해한 다음 규범적 등가로 재구성한다.
NFKD
Normalization Form Compatibility Decomposition. 문자를 호환성 등가로 분해한다. 예를 들어, 분수 '½'(U+00BD)을 3개 문자의 시퀀스로 변환한다: 1/2.

Normalizer 클래스

Java SE 6은 이제는 널리 알려진 클래스 java.text.Normalizer를 제공함으로써 유니코드 텍스트 정규화를 지원한다. 이 클래스는 텍스트를 변환하는 normalize 메소드와 NFC, NFD, NFKC, NFKD 등의 유니코드 정규화 형식을 나타내는 Form enumeration을 모두 정의한다.

다음은 다양한 유니코드 정규화 형식의 응용 예이다.

예제: NFC

웹 상에서 문서를 출판하고 싶다고 가정하자. Character Model for the World Wide Web 스펙은 웹의 색인, 검색 및 기타 텍스트 관련 기능을 향상시킬 목적으로 데이터를 출판하기에 앞서 정규화를 할 것(조기 정규화)을 권장하고 있다. 또한 이 스펙은 거의 모든 레거시 데이터와 현재의 소프트웨어에 의해 생성된 데이터가 이미 NFC로 정규화되어 있기 때문에 NFC가 선호되고 있다고 기술하고 있다. 다음 코드는 표준 인풋에서 데이터를 읽고 NFC로 정규화된 데이터를 표준 아웃풋에 작성하는 경우이다. 인풋과 아웃풋 모두에 UTF-8 인코딩이 사용된다.

import java.io.*;
import java.text.Normalizer;
import java.text.Normalizer.Form;

public class NFC {
  public static void main(String[] args) {
    final String INPUT_ENC = "UTF-8";
    final String OUTPUT_ENC = "UTF-8";
    try {
      BufferedReader r = new BufferedReader(
          new InputStreamReader(System.in, INPUT_ENC));
      PrintWriter w = new PrintWriter(
          new OutputStreamWriter(System.out, OUTPUT_ENC), true);
      String s;
      while ((s = r.readLine()) != null) {
        w.println(Normalizer.normalize(s, Form.NFC));
      }
    }
    catch (Exception ex) {
      ex.printStackTrace();
    }
  }
}

NFC 정규화는 또한 문자열 동일성 검증에도 매우 적합하다. 단, 문자열 비교 시 적절한 로케일로 초기화된 java.text.Collator 클래스를 사용해야 한다는 점에 유의할 것. Collator 클래스를 사용하는 이유는 언어에 따라 악센트 부호가 붙은 글자들의 정렬 순서가 다르기 때문이다. 정렬 방식에 있어서 어떤 언어는 악센트 부호가 붙은 글자를 기본 글자 바로 다음에 배치하고 또 어떤 경우에는 모든 기본 글자 다음에 배치하기도 한다.

예제: NFD

전화번호부(phone directory) 애플리케이션을 개발 중이라고 가정해보자. 이때, 디렉토리 데이터를 데이터베이스에 저장하고 데이터를 찾기 위한 검색 형식을 설정한다. 전세계의 인명에는 악센트 부호가 붙은 문자가 다수 포함되어 있으므로 다음과 같은 두 가지의 문제가 발생하게 된다. 즉, 대부분의 데이터베이스는 악센트 부호가 붙은 문자를 좋아하지 않을 뿐 아니라 대부분의 애플리케이션 사용자는 검색 형식에 정확한(악센트 부호가 붙은) 이름을 입력하는 번거로움을 피하려 하거나 입력 방법 자체를 모르는 경우가 많다. 따라서 데이터베이스에 저장된 데이터와 검색 형식에서 읽어들인 데이터 양쪽에서 악센트 부호를 모두 제거해야만 한다.

다음 코드는 표준 인풋을 한 행씩 읽고 각 행에서 악센트 부호가 붙은 문자를 제거한 다음 결과를 표준 아웃풋에 작성하는 경우이다. 이 때, 인풋과 아웃풋 모두에 UTF-8 인코딩이 사용된다.

import java.io.*;
import java.text.Normalizer;
import java.text.Normalizer.Form;

public class NFD {
  public static void main(String[] args) {
    final String INPUT_ENC = "UTF-8";
    final String OUTPUT_ENC = "UTF-8";
    try {               
      BufferedReader r = new BufferedReader(
        new InputStreamReader(System.in, INPUT_ENC));
      PrintWriter w = new PrintWriter(
        new OutputStreamWriter(System.out, OUTPUT_ENC), true);
      String s;
      while ((s = r.readLine()) != null) {
        // decompose and remove accents
        String decomposed = Normalizer.normalize(s, Form.NFD);
        String accentsGone =
            decomposed.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
        w.println(accentsGone);
      }
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }
}

예제: NFKC

NFKC 정규화는 호환성 분해 형식을 가진 조합 마크가 있는 문자에 영향을 미친다. 따라서, 문자 시퀀스 U+1E9B U+0323(LATIN SMALL LETTER LONG S WITH DOT ABOVE 다음에 오는 COMBINING DOT BELOW)은 단일 문자 값 U+1E69로 변환된다. 정규화된 문자는 '?'(LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE)이다.

이 정규화 형식은 IDN(International Domain Names)을 위한 문자열 프로파일 스펙(RFC 3491)을 준수하는 데 필요한데, 도메인명에 비 ASCII 문자가 포함되어 있는 경우에는 반드시 NFKC 형식으로 정규화되어야 한다. 따라서 국제 도메인명 등록이 필요한 애플리케이션을 작성하는 경우에는 반드시 도메인명을 이 형식으로 인코딩해야 한다.

IDN 인코딩의 경우 유니코드 버전 3.2를 지정하고 있으며, 유니코드 버전 3.2와 4.0 간에는 일부 CJK 한자 표기를 위한 전처리 형식에 차이가 있다는 점에 유의할 것. RFC 3491을 구현하지 않고 단지 전처리된 도메인명을 얻고자 할 경우에는 java.net.IDN 클래스가 제공하는 기능을 사용하면 된다.

인코딩 과정은 NFC 예제에 표시된 것과 유사하며, 차이점이라면 Form.NFC 대신 Form.NFKC 인코딩 형식을 사용해야 한다는 것뿐이다.

예제: NFKD

이 정규화 형식은 레거시 텍스트 데이터를 XML 포맷으로 변환할 때 특히 유용하다. Unicode in XML and other Markup Languages 스펙의 경우 호환 가능한 문자들을 처리하기 위한 몇 가지 규칙을 정의하고 있는데, 예를 들어 이 스펙은 위 첨자와 아래 첨자에는 <sup><sub> 마크업을, 분수 표현에는 MathML 마크업을, 그리고 원 숫자 대신 글머리표(list-item marker) 스타일을 사용할 것 등을 권하고 있다. 레거시 데이터를 XML로 변환하는 애플리케이션을 작성 중이라면 NFKD로 정규화된 텍스트 데이터에 적절한 마크업과 스타일을 적용하는 것을 고려해야 한다.

데이터를 NFKD 형식으로 변환하려면 Normalizer.normalize 메소드에 두 번째 매개변수로 Form.NFKD를 건네주어야 한다.

Normalizer.normalize(s, Form.NFKD); 

정규화 검사

java.text.Normalizer 클래스는 주어진 문자 시퀀스가 네 가지 정규화 형식 중 하나에 의거하여 정규화되었는지 여부를 확인하는 isNormalized 메소드를 정의한다. 다음 코드는 표준 인풋에서 행을 읽고 네 가지 형식 중 하나로 정규화되었는지 여부를 보고한다. 인풋에는 UTF-8 인코딩이 사용된다.

import java.io.*;
import java.text.Normalizer;
import java.text.Normalizer.Form;

public class IsNormalized {
  public static void main(String[] args) {
    final String INPUT_ENC = "UTF-8";
    final Form[] forms = { Form.NFC, Form.NFD, Form.NFKC, Form.NFKD };
    try {               
      BufferedReader r = new BufferedReader(
          new InputStreamReader(System.in, INPUT_ENC));
      String s;
      int line = 1;
      while ((s = r.readLine()) != null) {
        System.out.printf("%5d:", line++);
        for (Form f : forms) {
          if (Normalizer.isNormalized(s, f)) {
            System.out.print(" " + f.toString());
          }
        }
        System.out.println();
      }
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }
}

관련 상세 정보

다음 문서에 정규화에 관한 자세한 내용이 나와 있다.

"Java SE" 카테고리의 다른 글

2007/03/13 12:06 2007/03/13 12:06

TRACKBACK :: http://blog.sdnkorea.com/blog/trackback/347

  1. [Javascript] 쉽게 Validation 체크하는 방법

    Tracked from 벽초  삭제

    <br>웹관련 프로젝트에서 매 폼마다 폼값을 체크하는 것이 짜증나고 지겨워서 만들어 봤습니다. <br>나름대로 쓸만하네요...&nbsp; ^^;;<br><br><span style="font-weight: bold;"><span style="color: rgb(0, 0, 255);">사용예]</span> </span>아래와 같은 폼이 있다면....<br><br><br>&lt;form name="frmTest"&gt;<br>&nbsp;&nbsp;&nb..

    2007/03/22 11:02

댓글을 달아 주세요

  1. 김형국  수정/삭제  댓글쓰기

    개인적으로 눈낄이 가는 고급 내용이네요 잘보고 갑니다.

    2007/09/12 18:08
  2. 박정숙  수정/삭제  댓글쓰기

    좋은 정보 감사해요~

    2007/09/19 03:57
  3. 고진구  수정/삭제  댓글쓰기

    텍스트 정규화 잘만 배우면 업무에 많은 도움이 될것 같네요. 자동실행 명령어 많이 쓰는데 정말 유용할것 같습니다.

    2007/09/19 15:43
  4. 진현신  수정/삭제  댓글쓰기

    좋은 정보 감사드려요~

    2007/09/19 23:19
  5. 석원홍  수정/삭제  댓글쓰기

    좋은 정보 감사드려요

    2007/10/07 19:58
[로그인][오픈아이디란?]

◀ Prev 1  ... 294 295 296 297 298 299 300 301 302  ... 624  Next ▶