文字操作のアンチパターン

(作成中)

序論

システム開発において、いわゆる「文字化け」の原因は、文字のデータ表現に対する理解不足にあることが多い。
文字のデータ表現について整理し、頻繁に見られる過ちを分類することで、「文字化け」発生前・発生後の対策を試みる。

文字のデータ表現

文字集合 (character set/charset)

一般には、「文字のバイト表現」を「文字コード」と呼ぶ。文字コードについての代表的な規格として、ASCII がある。
文字コードを使用するには、文字とバイト列との「対応表」を決定し、それに基づいて文字のコード化を行う。この対応表は、使用する文字を集めたものであるため、「文字集合」と呼ばれる。
公式な文字集合の一覧は IANA で管理されており、登録手順は RFC 2278 で規定されている。

RFC 2278: IANA Charset Registration Procedures
http://www.ietf.org/rfc/rfc2278.txt
IANA: Character Sets[]
http://www.iana.org/assignments/character-sets

例えば、ASCIIコードでは、0x00〜0x7Fの128個の数字に、英数字と記号、および制御文字を割り当てた文字集合である。

下位\上位 0 1 2 3 4 5 6 7
0 (NUL) (DLE) (SP) 0 @ P ` p
1 (SOH) (DC1) ! 1 A Q a q
2 (STX) (DC2) " 2 B R b r
3 (ETX) (DC3) # 3 C S c s
4 (EOT) (DC4) $ 4 D T d t
5 (ENQ) (NAK) % 5 E U e u
6 (ACK) (SYN) & 6 F V f v
7 (BEL) (ETB) ' 7 G W g w
8 (BS) (CAN) ( 8 H X h x
9 (HT) (EM) ) 9 I Y i y
A (LF) (SUB) * : J Z j z
B (VT) (ESC) + ; K [ k {
C (FF) (FS) , L \ l
D (CR) (GS) - = M ] m }
E (SO) (RS) . > N ^ n ~
F (SI) (US) / ? O _ o (DEL)

ASCII の正式名称は American Standard Code for Information Interchange (情報交換のためのアメリカ標準コード) であり、名前が示すとおり、アメリカ合衆国内での情報交換を目的として制定されたものである。同様に、日本においても、日本国内での情報交換を目的とした文字集合が定められている (詳細後述)。
しかしこのような考え方では、多様なコード間での変換が非常に複雑になってしまう。そこで今日では、コンピュータ上で文字を扱うための手順は、「符号化文字集合」「文字符号化方式」という 2 段階に分けて考えられている。

符号化文字集合(coded character set)

文字をコード化するための最初の手順として、使用する文字を集め、それぞれにコード値を与える。こうして作られた「文字の一覧表」を、符号化文字集合と呼ぶ。
JIS X 0213:2000 における定義は以下の通り。

符号化文字集合 (coded character set)、符号 (code) 文字集合を定め、 かつその集合内の文字とビット組合せとを1対1に対応付ける、あいまいでない規則の集合。

文字を収集しコードを与えるという点では、前述の文字集合に似ている。ただし、符号化文字集合で使用されるコードは、バイト値に直接対応せず、いわば、一覧表の文字それぞれにつけられた便宜上の番号である。例えば Unicode は世界中の文字を集めた符号化文字集合であり、それぞれの文字に 21 ビットの番号 (U+00000〜U+1FFFFF) が与えられているが、これをそのまま「1 文字=21 ビットでファイルに格納する」ということはしない。そのような場合は、次に説明する文字符号化方式を決定し、符号をバイト列に変換する。

文字符号化方式 (character encoding scheme)

符号化文字集合に収められている文字を、バイト列に対応付けるための規則のことを、文字符号化方式と呼ぶ。対応付けの規則であるから、一般的に、文字符号化方式はある特定の符号化文字集合を前提とする。
例えば、UTF-32 は、Unicode のための文字符号化方式のひとつであり、21 ビットの符号を、1文字=32 ビット (4 バイト) 固定でそのままバイト列化するという規則である。Unicode の 21 ビット符号を前提としているため、他の符号化文字集合に対して UTF-32 を適用することはできない。
このような観点から、今日、文字コードの種別を示す必要がある場面では、文字符号化方式の名前を使用することが多い。文字符号化方式が判れば、使用している符号化文字集合もおのずと判るためである。例として、XML での指定は、次のようになる。

<?xml version="1.0" encoding="Shift_JIS"?>
<doucment/>

この例では、文字符号化方式として Shift_JIS を使用している。XML の仕様では Unicode を符号化文字集合として使用しているが、UnicodeShift_JIS でバイト列にすることを示しているわけではない。Shift_JISJIS X 0201 および 0208 のための文字符号化方式であり、ここでの指定が示すのは、「Unicode に収められているのと同じ文字を JIS X 0201 および 0208 で探し、それを Shift_JIS で符号化した」というものになる。

符号化文字集合 文字符号化方式
JIS X 0201/0208 Shift_JIS
EUC-JP
ISO-2022-JP
JIPS (NEC)
KEIS (日立)
JEF漢字コード (富士通)
Unicode UTF-8
UTF-16
UTF-32

言語環境

前章では、文字をコンピュータ上で表現するための手順は、符号化文字集合文字符号化方式の 2 段階に分けられることを示した。本章では、以上の理解に基づいて、日本国内と、Unicode を中心とする言語環境について整理する。

日本語環境

日本国内では、日本工業規格 (JIS) として、文字コードの規格化がなされている。主なものを下表に挙げる。

規格番号 位置付け 構造 関連する海外規格
JIS X 0201 英数字、カタカナ、記号、制御文字を収めた文字集合 16×16 ASCII
JIS X 0208 JIS X 0201 に加え、漢字や、罫線などの追加記号を収めた文字集合 94区×94点 ISO/IEC 2022
JIS X 0212 JIS X 0208 に「補助漢字」を追加するための文字集合 (94区×94点) ISO/IEC 2022
JIS X 0213 JIX X 0208 の仕様制定上の混乱を整理した新仕様 2面×94区×94点 ISO/IEC 2022ISO/IEC 10646

最初に制定された JIS X 0201 は、ASCII を拡張してカタカナを追加したものである。ASCII では 0x00〜0x7F に文字が定義されていたが、これを 0x00〜0xFF に拡張し、増えた部分にカタカナが追加されている。
JIS X 0208 は、漢字やひらがなといった日本語文字を使用する場合のために作られた規格である。この中には JIS X 0201 で収められていた文字も含まれている。ISO/IEC 2022 の仕様に従って 94×94 の表に文字を収めており、その中での位置をは「区点」と呼ばれる。後に制定された JIS X 0212 は、JIS X 0208 の空き領域 (94×94 の表のうち、未使用の部分) に文字を追加するものであり、JIS X 0208 と併用する。ここで追加された漢字は、「補助漢字」あるいは「第 2 水準」などと呼ばれる。
JIS X 0213 は、2000 年に新たに制定された規格である。おおまかには JIS X 0208 に漢字を追加したものであるといえ、「拡張漢字」とも呼ばれる (追加された文字は従来の94区×94点に収まらなかったため、2 つの「面」を使用する形式に変更された)。しかし、それまでの JIS X 0208/0212 が抱えていた問題を解決すべく全体が整理された結果、JIS X 0208/0212 とは互換性がなく、既存コードを拡張しただけのものではない。
表の形式を採っていることからもわかるとおり、これらの規格は、もともと文字集合として制定された。しかし、実際には、Shift JIS、EUC-JP、ISO-2022-JP といった文字符号化方式を用いてバイト列に変換されることが通常であり、現在では符号化文字集合として扱われるようになっている。

Unicode環境

Unicode は、様々な言語の文字を収める符号化文字集合として、Unicode Consortium により制定された。収められている文字は、原則として各国で使われている符号化文字集合からの引用である。日本語の文字も、前述の JIS に収められた文字が、Unicode にも収められている。
当初は、世界中のすべての文字を集め、16 ビットの符号 (U+0000〜U+FFFF) を与えることを目標としていた。しかし、16 ビットで表せる 65536 通りの符号では不足するため、現在では 21 ビット (U+000000〜U+10FFFF) に拡張されている。
Unicode のための文字符号化方式としては、主に以下のようなものがある。

文字符号化方式 内容
UTF-8 各文字を 1〜4 バイトで表現する
UTF-16 各文字を 2 または 4 バイトで表現する
UTF-32 各文字を 4 バイト固定で表現する

UTF-8 の基本的な考え方は、1 バイトで表せる文字は 1 バイト、そうでない文字は 2 バイト以上で表すというものである。UTF-16 は同様に、2 バイトで表せる文字は 2 バイト、そうでない文字は 4 バイトで表す。UTF-32 は 4 バイト (32 ビット) 固定であり、Unicode の符号をそのまま格納する。
当初の計画通り Unicode が 16 ビット固定であった頃、いくつかのソフトウェアにおいて、内部で扱う文字コードUnicode としたものがあった。主な例を挙げると、Windows NT (WCHAR 型) や、Java (char 型) などがある。現在では Unicode が 21 ビットに拡張されてしまったため、これらのソフトウェアの内部コードは UTF-16 である、ということになっている。符号が 16 ビットで表せない文字を「Unicode 補助文字」といい、UTF-16 では、16 ビットを 2 単位使って表現する。この仕組みを「サロゲートペア」という。JIS X 0213 で追加された文字の一部は、後に Unicode にも追加されたが、そのうち一部は補助文字に該当している。このため、JIS X 0213 を採用したシステム (Windows Vista など) からの入力を受け取る場合には、サロゲートペアに対応する必要がある。
Unicode にはいくつかのバージョンがあり、文字や機能の追加が行われているため、使用しているシステムがどのバージョンに対応しているかは重要である。一例として、Java の対応状況を示す。

Java のバージョン 使われている Unicode のバージョン
Java 1.02 1.1
Java 1.1〜1.1.6 2.0
Java 1.1.7〜1.3 2.1
Java 1.4 3.0
Java 1.5〜1.6 4.0

なお、Unicode は ISO 10646・国際文字集合 (Universal Character Set, UCS) として国際規格化もされている。

コード変換

2 種類のコード間で変換を行う場合、概念的には下図のような順序を辿る。

夜                変換元                    変換先
                  JIS X 0208 / Shift JIS    Unicode / UTF-8
                  ========================  ========================
符号化文字集合    44区75点                →U+591C
で与えられた符号  
                     ↑                        ↓
バイト列          0x96 0xe9                 0xE5 0xA4 0x9C

このうち、バイト列から符号符号化文字集合への対応付け (上図の↑と↓) は、文字符号化方式が決めるところに従って機械的に変換することができる。
ただし、ある符号化文字集合から別の符号化文字集合への変換 (上図の→) にあたっては、様々な理由で齟齬が生じうる。よく知られている問題のひとつとして、「WAVE-DASH FULLWIDTH-TILDE問題」について述べる。
Unicode と既存文字コードとの変換では、ソフトウェアによって対応付けが異なるために、変換元が同じなのに、変換結果が異なってしまうことがある。
JIS X 0208の「〜」(1面1区33点)は、俗に「から」と読まれる文字で、「波ダッシュ」として定義されている。Unicode でこれに対応するのは、WAVE DASH (U+301C) であるが、Windows API では FULLWIDTH TILDE (U+FF5E) に変換されてしまう。このように対応付けが異なるソフトウェアが混在していると、文字列比較に失敗したり、再変換に失敗したりする。これを「WAVE-DASH FULLWIDTH-TILDE 問題」と呼ぶ。同じ理由で文字化けが発生する文字を表に示す。

文字 コード (シフト JIS) 一般的なコンバータでの対応付け Windows での対応付け
〜 (波ダッシュ) 0x8160 U+301C (WAVE DASH) U+FF5E (FULLWIDTH TILDE)
− (マイナス) 0x817C U+2212 (MINUS SIGN) U+FF0D (FULLWIDTH HYPHEN-MINUS)
‖ (二重垂直線) 0x8161 U+2016 (DOUBLE VERTICAL LINE) U+2225 (PARALLEL TO)
¢ (セント) 0x8191 U+00A2 (CENT SIGN) U+FFE0 (FULLWIDTH CENT SIGN)
£ (ポンド) 0x8192 U+00A3 (POUND SIGN) U+FFE1 (FULLWIDTH POUND SIGN)
¬ (否定) 0x81CA U+00AC (NOT SIGN) U+FFE2 (FULLWIDTH NOT SIGN)

事例

前章では、実際の言語環境において、符号化文字集合文字符号化方式が規定され、実際に利用されている状況を整理した。本章では、実際に開発中に発生した「文字化け」の事例を挙げ、個別に原因究明と解決策の提示を試みる。

Shift_JIS vs. Windows-31J

現象: 日本語文字を含む JSP (Java Server Pages) を実行したところ、「〜」が変換失敗を表す「?」に化けた。

<%@page encoding="Shift_JIS"%>
<html>
<title>文字が化けるよ〜</title>
</html>

原因: 入力側と出力側で、使用している変換テーブルが異なるため。
解決: 同一または互換性のあるテーブルを使用する。あるいは、入力直後か出力直前に符号変換を行う。
解説: 多く見られる誤解として、「Shift_JISMicrosoft が作成した」というものがある。しかしこれは正しくない。Shift_JIS の正式な仕様は JIS にて規格化されており、MicrosoftWindows などの製品で使用しているのは、JIS X 0201/0208 の文字に加え、NECIBM のプラットフォームで定義されていた文字が使えるように拡張されたバージョンである。このコードは、IANA では Windows-31J という名前で登録されている。したがって、これらを旧来的な「文字集合」として見るのであれば、Windows-31JShift_JIS の上位互換と考えることができる。
しかし、WAVE-DASH FULLWIDTH-TILDE 問題として前述した通り、Windows では UnicodeJIS X 0208 の間での対応付けが他のソフトウェアと異なっている。そして、Windows ネイティブアプリケーションとの互換性を保つため、Java では、Windows-31J が指定された場合に限り、対応付けも Windows に合わせるようになっている。これが仇となり、共通して使えるはずの JIS X 0208 文字であっても、Shift_JIS として変換した場合と Windows-31J として変換した場合で、互換性が失われ、変換失敗を示す「?:」に化けてしまうのである。
回避するには、使用するコードを入力と出力の両方でいずれかに揃えればよい。あるいは、入力直前か出力直前に符号を変換し、いずれかに揃えればよい。
同件: Windows-31J のテキストファイルを読み込み、JavaMail で ISO-2022-JP のメール本文として使用すると、「〜」などの文字が「?」に化ける。JA16SJIS で構築した Oracle DB から文字列を取得し、Windows-31JJSP 出力すると、「〜」などの文字が「?」に化ける。

JIS X 0201/0208 vs. Windows-31J 文字集合

現象: 日本語文字を含む JSP (Java Server Pages) を実行したところ、「①」が変換失敗を表す「?」に化けた。

<%@page encoding="Shift_JIS"%>
<html>
<title>文字が化けるよ①</title>
</html>

原因: 入力側と出力側で、使用している符号化文字集合が異なるため。
解決: 同一の符号化文字集合 (および、それに対応する文字符号化方式) を使用する。
解説: 前述の通り、Windows-31J では独自に文字が追加されている。一般に「NEC 拡張漢字」「IBM 拡張漢字」と呼ばれているこれらの文字には、丸付き数字のように今日頻繁に使用されている文字も含まれている。一方、Shift_JISJIS X 0201/0208 を前提とする文字符号化方式であり、これらの文字を使用することはできない。必要な場合には、Windows-31J を使用するか、Unicode の対応する文字を使用する必要がある。

[2007-12-25 追記] Shift_JIS だけかと思ったら EUC-JP でも同様の模様 (id:sardine:20071225#p1)。

ソースコンパイル時の変換

現象: 以下のような Java プログラムを Windowsコンパイルした後、UNIX 環境で実行したところ、「〜」が「?」に化けた。

public class Foo {
    public static void main(String[] args) {
        System.out.println("文字が化けるよ〜");
    }
}

原因: コンパイル時と実行時の変換テーブルが異なるため。
回避策: コンパイル時に指定する文字符号化方式を、実行時に使用するものに合わせる。あるいは、実行時に合わせた符号をエスケープシーケンスで直接指定する。
解説: JavaC# などの言語では、コンパイラ内部でソースファイルを Unicode へ変換してからコンパイルが行われる。また、標準出力への出力時にも、Unicode からのコード変換が行われる。上記の例では、UNIX 環境側が Shift_JIS に設定されており、Windows 環境のデフォルトである Windows-31J との間で WAVE-DASH FULLWIDTH-TILDE 問題が発生している。現時点では解決策といえるものがなく、コンパイル時に実行環境を意識するか、該当する文字を使用しないという消極的な対応を採らざるを得ない。

自動判別の限界

現象例: 日本語を含む HTML ファイルが文字化けした。

<html>
<title>Sample</title>
<!-- : -->
<!-- : -->
文字が化けるよ〜
</html>

原因: 文字コードの自動判別に失敗した。
解決策: 文字コードを指定する。
解説: Web ブラウザなどに搭載されている文字コードの自動判別機能は、ファイル冒頭の一定量しか検査しない。したがって、冒頭に ASCII 文字しか含まれないようなファイルでは、正しい判別結果を返さない。また、自動判別機能は、バイト列の出現パターンを見て推測を行うので、テキスト中に書かれた文字のならびによっては、判定を誤ることもある。
HTTP の Content-Type ヘッダを用いて明示的に文字コードを指定すれば、そもそも自動判別に頼らずにすむ。JSP の場合は次のようになる。

<%@page encoding="Windows-31J"%>

静的 HTML の場合は、Web サーバの設定ファイルを用いて文字コードを明示する。Apache の場合であれば、HTML ファイルと同じディレクトリに .htaccess ファイルを作成し、下記のような記述を行えばよい。

AddType "text/html; charset=Windows-31J" html

関連: Java でテキスト入力を行う際、文字コード名の代わりに "JISAutoDetect" を指定すると、日本語文字コードの自動判別が行われる。しかし、この機能もブラウザの自動判別と同様の限界がある。

IE における Windows-31J

現象例: JSP で下記のように文字コードを指定したところ、Internet Explorer で表示すると日本語文字が化けて表示された。

<%@page encoding="Windows-31J"%>
<html>
<title>Sample</title>
<meta http-equiv="Content-Type" content="text/html; charset=Windows-31J">
<!-- : -->
<!-- : -->
文字が化けるよ〜
</html>

原因: Internet Explorer が "Windows-31J" の指定に対応していない。また、ブラウザの自動判別はファイル冒頭しか検査しない。
回避策 1: ファイル冒頭に日本語文字を含める。あるいは、Shift_JIS に変更する。
回避策 2: HTTP の Content-Type ヘッダでは Windows-31J を指定し、HTML ファイル内では Shift_JIS を指定する。
解説: すでに述べた通り、Windows では Shift_JIS の拡張版である Windows-31J が使われている。しかし Microsoft 自身は「独自に拡張してはいるが、Windows で使用しているのは Shift_JIS である」という立場をとっており、Internet Explorer は "Windows-31J" を「未知の文字コード」として扱う。その場合、ユーザの設定に基づいて自動判別を試みるが、ファイル冒頭の一定量しか検査しないため、構成によっては自動判別に失敗し、日本語文字が化けてしまうことになる。
この問題の解決は困難である。
回避策のひとつは、Windows-31J を指定するかわりに Shift_JIS を指定するものである。しかし Java の場合、それでは前述の通り WAVE-DASH FULLWIDTH-TILDE 問題が発生するし、NEC/IBM 拡張文字も使えなくなってしまう。
もうひとつ、次のようにすることで、IE でも他のブラウザでも一応の正常表示を得ることはできる。これは、JSP の page encoding として指定した文字コードは、HTTP ヘッダにも出力されることを利用したトリックである。Windows-31J に対応したブラウザでは HTTP ヘッダを読み取って表示を行い、対応していない IE は次にページ内の meta 属性を見て Shift_JIS と認識する。しかし、見て判るとおり本来あるべき記述とはいえず、いわゆる「バッドノウハウ」に属している。

<%@page encoding="Windows-31J"%>
<html>
<title>Sample</title>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<!-- : -->
<!-- : -->
文字が化けるよ〜
</html>
Unicode vs. UTF-16

現象: Java で作成した文字種別判定処理が、一部の日本語文字を不正にエラー扱いした。

public void validateString(String s) {
    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        // c に対する処理
    }
}

原因: 文字を 2 バイト (16 ビット) 固定長として扱っている。
解決策: 21 ビット単位で扱えるようにする。あるいは、2 バイト可変長として扱う。
解説: すでに説明したように、当初 16 ビット固定長として設計された Unicode は、収録する文字の増加にともない 21 ビットに拡張されている。UTF-16 では、1 単位 (2 バイト) で表せない補助文字を 2 単位 (4 バイト) で表現するため、16 ビット単位の処理では正しく処理することができない。Java では、補助文字を扱うための API がバージョン 1.5 (Java 2 SE 5.0) で追加されており、文字列の走査や分割などの処理を行う場合にはこれらの API を使用する必要がある。

public void validateString(String s) {
    for (int i = 0; i < s.length(); i += Character.charCount()) {
        int c = s.codePointAt(i);
        // c に対する処理
    }
}

また、Windows では、Vista 対応版の SDK においてサロゲートペア判定用のマクロが定義されており、これを用いて可変長に対応する必要がある。

Unicode 対応の意味

現象: UTF-8 で出力したファイルを、Unicode 対応を謳っているテキストエディタで開いたら、他国語の文字が化けて表示された。
原因: テキストエディタの内部コードが Unicode でない。
解決策: 内部コードが Unicodeテキストエディタを使用する。
解説: 最近のテキストエディタには、UTF-8UTF-16 といった Unicode 系の文字符号化方式に対応していると主張するものが多い。しかし、実際には以下の制限のあるものが多く、特に多言語テキストにおいて問題が発生しやすい。

  • 内部コードは Windows-31J であり、読み込み時に UTF-8 から変換している。したがって、JIS X 0201/0208 に収められていない文字があると、編集できない。
  • 複数文字の合成や書字方向の変更など、Unicode で定義された制御機能に対応していない。

したがって、Unicode テキストの内容をより正確に確認するには、内部コードが Unicode (あるいは UTF-16 など) で、Unicode の制御機能に対応したものを選択する必要がある。
ただし、逆に非 Unicode テキストを編集する際には、内部コードが Unicodeテキストエディタでは、読み書きの際に変換が必要になる点に注意が必要である。

考察

前章で紹介した各種事例について、問題が発生した箇所とその対策は、以下のようにまとめることができる。

問題の根本原因 解決
異なる符号化文字集合の使用 同一または互換性のある符号化文字集合への変更
異なる変換テーブルの使用 同一または互換性のある符号化文字集合への変更
Unicode 対応の不十分 補助文字や各種制御機能を含めた対応
フォント収録文字の不足 該当する文字を含んだフォントの使用

これらはいずれも、異なる符号化文字集合文字符号化方式を使用している状況で、それらの差異や変換の存在を十分に考慮していないために、文字化けを発生させている。入力から出力までを順を追って精査し、変換が「いつ」「何から何に」「どのように」行われるかを把握することで、事前に文字化けの発生を避け、あるいは避けられない場合であってもその影響を最小限に抑えることができる。

結論

文字化けは、いわゆる「文字コード」が符号化文字集合文字符号化方式から成っていることを理解し、適切に対処することで、解決することができる。


参照文献

Glossary of Unicode Terms
http://unicode.org/glossary/
文字コードについてのメモ - 矢野啓介
http://www.asahi-net.or.jp/~wq6k-yn/code/
「符号化文字集合 coded character set」という語の定義
http://www.asahi-net.or.jp/~wq6k-yn/code/ccs.html
Unicode (ユニコード) と中日韓 (CJK) エンコーディングとの相互運用の問題
http://people.debian.org/~kubota/unicode-symbols.html
Encode::Supported -- Encodings supported by Encode
http://search.cpan.org/dist/Encode/lib/Encode/Supported.pod
Character Set Considered Harmful
http://www.w3.org/MarkUp/html-spec/charset-harmful
JavaにおけるUnicode補助文字のサポート - NTT未来ねっと研究所 風間一洋
http://kura.hanazono.ac.jp/paper/20040609kazama.ppt.pdf
シフトJISの産まれた歴史的背景
http://furukawablog.spaces.live.com/Blog/cns!156823E649BD3714!6054.entry
小形克宏の「文字の海、ビットの舟」―― 文字コードが私たちに問いかけるもの - 小形克宏
http://internet.watch.impress.co.jp/www/column/ogata/index.htm