プログラマがシステム開発において共通で必要となる、技術と業務の狭間の共通知識を解説します。連載第2回は文字コードの実践編です。
0. 前回の復習と今回の概要
システム開発で必要となる標準規格の話、前回 は文字コードの概要について説明しました。ざっくりまとめるとこんな内容でした。
- 「符号化文字集合」で文字集合と符号位置を定義し、「符号化方式」でバイト表現に変換していること。
- 日本では、しばらく文字集合 JIS X 0208 を、ISO-2022-JP、EUC-JP、Shift_JIS の符号化方式で利用してきたこと。
- 近年は、世界中の文字が扱える Unicode が主流となっており、UTF-8、UTF-16 などの符号化方式があること。
- 常用漢字、人名用漢字に限っても、字体を正確に扱おうとすると、JIS X 0208 の範囲では不十分であり、JIS X 0213、Unicode、サロゲートペア、Unicode正規化、異体字セレクタ等の知識が必要になること。
連載第2回は文字コードの実践編です。プログラミングでの扱い方、システム開発において特に注意すべき問題点と対策案を解説します。
1. プログラミング言語での扱い (Java)
本章では、Java を例に、プログラミング言語で文字コードを扱う方法 (文字列の走査、文字コード変換、Unicode 正規化) を説明します。
コード例は Java 11 を基準にしています。
Java は char
型で文字1つを、String
クラスで char
の並びとしての文字列を管理します。char
、String
の実体は UTF-16 形式の Unicode です。したがって、基本多言語面 (BMP) 以外の文字はサロゲートペアで表現されます。
1-1. 文字列の走査
本節では、文字列を構成する文字を順に走査し、処理してみます。
char 単位の走査
一番簡単なのは char
ごとに走査する方法です。for
文で回すこともできますが、今どきっぽくラムダ式で書いてみました。String#chars()
で char
を int
に拡張した IntStream
が返されます。
class CharsetsTest { public static void main(String[] args) throws Exception { testChars(); // ... } static void testChars() { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; // char 単位で処理を行う。 text.chars().forEach(c -> { if (Character.isISOControl(c)) { System.out.printf("%04X (%s)%n", c, getName(c)); } else { System.out.printf("%04X [%c]%n", c, (char) c); } }); } static String getName(int codePoint) { switch (codePoint) { case '\r': return "CR"; case '\n': return "LF"; default: return Character.getName(codePoint); } } }
実行結果:
0041 [A] 0061 [a] 0031 [1] FF71 [ア] FF76 [カ] FF9E [゙] FF21 [A] FF11 [1] 30A2 [ア] 30AC [ガ] 4E9C [亜] 00A5 [¥] FFE5 [¥] FFE3 [ ̄] 212B [Å] 2460 [①] 5F45 [彅] 000D (CR) 000A (LF) D842 [?] DF9F [?] ← サロゲートペア D84D [?] DD94 [?] FA19 [神] 795E [神] FE00 [︀] ← 異体字シーケンス 30AB [カ] 309A [゚] ← 結合文字列 02E9 [˩] 02E5 [˥]
char
単位の走査では、BMP の文字は正しく処理できますが、サロゲートペアや結合文字列、異体字シーケンスは複数の char
で表現されるので、正しく1文字として扱えません。
コードポイント (符号位置) 単位の走査
次に、コードポイント (符号位置) 単位に走査してみます。String#codePoints()
でコードポイントのストリームが IntStream で返されます。
import java.util.stream.Collectors; // ... static void testCodePoints() { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; // コードポイント単位で処理を行う。 text.codePoints().forEach(cp -> { String s = codePointsToString(cp); if (Character.isISOControl(cp)) { System.out.printf("U+%04X (%s) (%s)%n", cp, toHexString(s), getName(cp)); } else { System.out.printf("U+%04X (%s) [%s]%n", cp, toHexString(s), s); } }); } static String codePointsToString(int... codePoints) { return new String(codePoints, 0, codePoints.length); } static String toHexString(String s) { return s.chars() .mapToObj(cp -> String.format("%04X", cp)) .collect(Collectors.joining(" ")); }
実行結果:
U+0041 (0041) [A] U+0061 (0061) [a] U+0031 (0031) [1] U+FF71 (FF71) [ア] U+FF76 (FF76) [カ] U+FF9E (FF9E) [゙] U+FF21 (FF21) [A] U+FF11 (FF11) [1] U+30A2 (30A2) [ア] U+30AC (30AC) [ガ] U+4E9C (4E9C) [亜] U+00A5 (00A5) [¥] U+FFE5 (FFE5) [¥] U+FFE3 (FFE3) [ ̄] U+212B (212B) [Å] U+2460 (2460) [①] U+5F45 (5F45) [彅] U+000D (000D) (CR) U+000A (000A) (LF) U+20B9F (D842 DF9F) [𠮟] ← サロゲートペア U+23594 (D84D DD94) [𣖔] U+FA19 (FA19) [神] U+795E (795E) [神] U+FE00 (FE00) [︀] ← 異体字シーケンス U+30AB (30AB) [カ] U+309A (309A) [゚] ← 結合文字列 U+02E9 (02E9) [˩] U+02E5 (02E5) [˥]
サロゲートペアが正しく処理できるようになりました。ただし、結合文字列、異体字シーケンスは複数のコードポイントで表現されるので、正しく1文字として扱えていません。
BreakIterator による走査
java.text.BreakIterator
は文字列の境界を見つけて処理する機能を提供します。BreakIterator.getCharacterInstance()
で文字分割用の BreakIterator
を取得できます。
import java.text.BreakIterator; import java.util.Locale; // ... static void testBreakIterator() { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; // 文字分割用の BreakIterator を取得する。 BreakIterator bi = BreakIterator.getCharacterInstance(Locale.JAPAN); // 処理対象のテキストを設定する。 bi.setText(text); // 1文字ずつ処理を行う。 for (int start = bi.first(), end = bi.next(); end != BreakIterator.DONE; start = end, end = bi.next()) { String s = text.substring(start, end); if (Character.isISOControl(s.charAt(0))) { System.out.printf("%s (%s)%n", toHexString(s), getName(s)); } else { System.out.printf("%s [%s]%n", toHexString(s), s); } } } static String getName(String s) { return s.codePoints() .mapToObj(CharsetsTest::getName) .collect(Collectors.joining("+")); }
実行結果:
0041 [A] 0061 [a] 0031 [1] FF71 [ア] FF76 [カ] FF9E [゙] FF21 [A] FF11 [1] 30A2 [ア] 30AC [ガ] 4E9C [亜] 00A5 [¥] FFE5 [¥] FFE3 [ ̄] 212B [Å] 2460 [①] 5F45 [彅] 000D 000A (CR+LF) ← 改行コード D842 DF9F [𠮟] ← サロゲートペア D84D DD94 [𣖔] FA19 [神] 795E FE00 [神︀] ← 異体字シーケンス 30AB 309A [カ゚] ← 結合文字列 02E9 [˩] 02E5 [˥] ← これも結合文字列
サロゲートペア、結合文字列、異体字シーケンス、いずれも正しく1文字として処理することができました。加えて、改行コード CR+LF も1文字として扱われています。
ただし、02E9 02E5
はそれぞれ基底文字であるせいか別の文字として扱われています。要件次第ですが、02E9 02E5
を結合文字列として扱いたい場合は、次の正規表現を用いるとよいと思います。
正規表現を用いた走査
正規表現を用いれば文字列を好きな位置で分割して処理することが可能です。Unicode の書記素クラスタ (grapheme cluster) を利用すると、正規表現 \X
で「基底文字+結合文字」の並びにマッチできるようです。なお、Java の文字列では \
を \\
とエスケープします。
コード例では、書記素クラスタに加えて 02E9 02E5
、02E5 02E9
も1文字として扱う実装にしています。
import java.util.regex.Pattern; // ... /** * 論理的な文字を表す正規表現のパターン。 */ static final Pattern CHAR_PATTERN = Pattern.compile("\\u02E9\\u02E5|\\u02E5\\u02E9|\\X"); static void testRegex() { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; // 正規表現にマッチした単位で処理を行う。 CHAR_PATTERN.matcher(text).results().forEach(r -> { String s = r.group(); if (Character.isISOControl(s.charAt(0))) { System.out.printf("%s (%s)%n", toHexString(s), getName(s)); } else { System.out.printf("%s [%s]%n", toHexString(s), s); } }); }
実行結果:
0041 [A] 0061 [a] 0031 [1] FF71 [ア] FF76 FF9E [ガ] ← 半角片仮名の濁音 FF21 [A] FF11 [1] 30A2 [ア] 30AC [ガ] 4E9C [亜] 00A5 [¥] FFE5 [¥] FFE3 [ ̄] 212B [Å] 2460 [①] 5F45 [彅] 000D 000A (CR+LF) ← 改行コード D842 DF9F [𠮟] ← サロゲートペア D84D DD94 [𣖔] FA19 [神] 795E FE00 [神︀] ← 異体字シーケンス 30AB 309A [カ゚] ← 結合文字列 02E9 02E5 [˩˥] ← これも結合文字列
サロゲートペア、結合文字列、異体字シーケンスが、いずれも1文字として処理することができました。加えて、改行コード CR+LF、半角片仮名の濁音も1文字として扱われています。
以上、BreakIterator
や正規表現を用いれば、どんな種類の文字でも正しく1文字として処理することができますが、BMP の文字しか扱わないのであれば String#chars()
の方がパフォーマンス的に優位ではあるので、適材適所で使い分けるのがよいと思います。
1-2. String クラスによるエンコード/デコード
本節では、String
クラスを用いた、Unicode と各文字コードとのコード変換を説明します。具体的には、Unicode (UTF-16) を表す文字列 (String
) と、各文字コードを表すバイト配列 (byte[]
) との変換になります。
Unicode (String
) から各文字コード (byte[]
) への変換をエンコード (encode)、各文字コード (byte[]
) から Unicode (String
) への変換をデコード (decode) と呼びます。
Unicode から各文字コードへの変換 (エンコード)
String#getBytes()
で Unicode (String
) から各文字コード (byte[]
) へ変換ができます。
変換先の文字コードは、エンコーディング名を文字列で指定するか、java.nio.charset.Charset
クラスを指定します。
import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.stream.IntStream; // ... // Shift_JIS-2004 の Charset を取得する。 static final Charset SHIFT_JIS_2004 = Charset.forName("x-SJIS_0213"); static void testEncoding() throws UnsupportedEncodingException { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; // Unicode (UTF-16) から Windows-31J に変換 (エンコーディング名指定)。 byte[] w31j = text.getBytes("Windows-31J"); System.out.println("Windows-31J:"); System.out.println(toHexString(w31j)); // Unicode (UTF-16) から Shift_JIS-2004 に変換 (Charset クラス指定)。 byte[] sjis2004 = text.getBytes(SHIFT_JIS_2004); System.out.println("Shift_JIS-2004:"); System.out.println(toHexString(sjis2004)); } static String toHexString(byte[] a) { return IntStream.range(0, a.length) .mapToObj(i -> String.format("%02X", a[i])) .collect(Collectors.joining(" ")); }
実行結果:
Windows-31J: 41 61 31 B1 B6 DE 82 60 82 50 83 41 83 4B 88 9F 5C 81 8F 81 50 81 F0 87 40 FA 67 0D 0A 3F 3F FB 7E 90 5F 3F 83 4A 3F 3F 3F Shift_JIS-2004: 41 61 31 B1 B6 DE 82 60 82 50 83 41 83 4B 88 9F 3F 81 8F 81 50 81 F0 87 40 EA B8 0D 0A 98 73 F4 49 ED 5B 90 5F 3F 83 97 86 85
変換できない文字は置換文字 (3F
(?) など) に置き換わります。
各文字コードから Unicode への変換 (デコード)
各文字コード (byte[]
) から Unicode (String
) への変換は String
クラスのコンストラクタを用います。
変換元の文字コードは、エンコードと同様に、エンコーディング名を文字列で指定するか、java.nio.charset.Charset
クラスを指定します。
static void testDecoding() throws UnsupportedEncodingException { byte[] sjis = bytes( // JIS X 0201、JIS X 0208 など。 0x41, 0x61, 0x31, 0xB1, 0xB6, 0xDE, 0x82, 0x60, 0x82, 0x50, 0x83, 0x41, 0x83, 0x4B, 0x88, 0x9F, // Windows-31J など。 0x5C, 0x81, 0x8F, 0x81, 0x50, 0x81, 0xF0, 0x87, 0x40, 0xFA, 0x67, 0xED, 0x4B, 0xFB, 0x7E, 0xEE, 0x62, 0x0D, 0x0A, // JIS X 0213 など。 0x98, 0x73, 0xF4, 0x49, 0xED, 0x5B, 0x83, 0x97, 0x86, 0x85); // Windows-31J から Unicode (UTF-16) に変換する (エンコーディング名指定)。 String w31jText = new String(sjis, "Windows-31J"); System.out.println("Windows-31J:"); System.out.println(toHexString(w31jText)); System.out.println(w31jText); // Shift_JIS-2004 から Unicode (UTF-16) に変換する (Charset クラス指定)。 String sjis2004Text = new String(sjis, SHIFT_JIS_2004); System.out.println("Shift_JIS-2004:"); System.out.println(toHexString(sjis2004Text)); System.out.println(sjis2004Text); } /** * {@code int} の並びからバイト配列を生成します。 * * @param values {@code int} の並び * @return バイト配列 */ static byte[] bytes(int... values) { byte[] bytes = new byte[values.length]; for (int i = 0; i < values.length; ++i) { bytes[i] = (byte) values[i]; } return bytes; }
実行結果:
Windows-31J: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AC 4E9C 005C FFE5 FFE3 212B 2460 5F45 5F45 FA19 FA19 000D 000A FFFD 0073 E2F9 501E FFFD 87BA FFFD Aa1アガA1アガ亜\¥ ̄Å①彅彅神神 ← ベンダー外字も正しくデコード �s倞�螺� ← JIS X 0213 固有文字は文字化け Shift_JIS-2004: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AC 4E9C 005C FFE5 FFE3 212B 2460 D860 DCBB 78F2 9633 85ED 000D 000A D842 DF9F D84D DD94 FA19 30AB 309A 02E9 02E5 Aa1アガA1アガ亜\¥ ̄Å①𨂻磲阳藭 ← ベンダー外字は文字化け 𠮟𣖔神カ゚˩˥ ← JIS X 0213 固有文字は正しくデコード
変換できないバイト列は置換文字 (FFFD
) に置き換わります。
Windows-31J のベンダー外字を Shift_JIS-2004 で、JIS X 0213 固有文字を Windows-31J でデコードすると文字化けが発生します。
1-3. CharsetEncoder / CharsetDecoder によるエンコード/デコード
Java NIO (New I/O API) のクラスを使うと、よりきめ細かいエンコード/デコードが可能になります。
Unicode から各文字コードへの変換 (エンコード)
Charset#newEncoder()
で java.nio.charset.CharsetEncoder
のインスタンスを取得し、CharsetEncoder#encode()
で、Unicode から各文字コードへ変換します。String
、byte[]
の代わりに java.nio.CharBuffer
、java.nio.ByteBuffer
を使用する必要があるため、コード例ではメソッドに切り出しました。
import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; // ... // Windows-31J の Charset を取得する。 static final Charset WINDOWS_31J = Charset.forName("Windows-31J"); static void testEncoder() throws CharacterCodingException { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; byte[] w31j = encode(source, WINDOWS_31J); System.out.println("Windows-31J:"); System.out.println(toHexString(w31j)); } /** * 文字列を指定された文字コードでバイト配列にエンコードします。 * * @param s 文字列 * @param charset 文字コード * @return バイト配列 * @throws CharacterCodingException エンコードに失敗した場合 */ static byte[] encode(CharSequence s, Charset charset) throws CharacterCodingException { CharBuffer in = CharBuffer.wrap(s); ByteBuffer out = charset.newEncoder().encode(in); byte[] bytes = new byte[out.remaining()]; out.get(bytes); return bytes; }
実行結果:
java.nio.charset.UnmappableCharacterException: Input length = 2
CharsetEncoder
は、変換できない文字があるとデフォルトで例外をスローします。
標準のメッセージだとエラー箇所が分かりにくいので、エラーの発生位置や入力情報を加えるなど、少し手を入れた方が良いかもしれません。
CharsetEncoder
に対してオプションを指定すると、変換できない文字があったときの動作を変更することができます。サンプルでは下駄記号 (〓) に置き換えてみました。
import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; // ... static void testEncoder2() { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; byte[] w31j = encode(source, WINDOWS_31J, "〓"); System.out.println("Windows-31J:"); System.out.println(toHexString(w31j)); } /** * 文字列を指定された文字コードでバイト配列にエンコードします。 * * @param s 文字列 * @param charset 文字コード * @param replace エンコードに失敗した場合、置き換える文字列 * @return バイト配列 */ static byte[] encode(CharSequence s, Charset charset, String replace) { CharsetEncoder encoder = charset.newEncoder() .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE); if (replace != null) { encoder.replaceWith(replace.getBytes(charset)); } try { ByteBuffer out = encoder.encode(CharBuffer.wrap(s)); byte[] bytes = new byte[out.remaining()]; out.get(bytes); return bytes; } catch (CharacterCodingException e) { throw new UncheckedIOException(e); } }
実行結果:
Windows-31J: 41 61 31 B1 B6 DE 82 60 82 50 83 41 83 4B 88 9F 5C 81 8F 81 50 81 F0 87 40 FA 67 0D 0A 81 AC 81 AC FB 7E 90 5F 81 AC 83 4A 81 AC 81 AC 81 AC
81 AC
が置き換えられた部分です。
各文字コードから Unicode への変換 (デコード)
Charset#newDecoder()
で java.nio.charset.CharsetDecoder
のインスタンスを取得し、CharsetDecoder#decode()
で、各文字コードから Unicode へ変換します。byte[]
、String
の代わりに java.nio.ByteBuffer
、java.nio.CharBuffer
を使用する必要があるため、コード例ではメソッドに切り出しました。
static void testDecoder() throws CharacterCodingException { byte[] sjis = bytes( // JIS X 0201、JIS X 0208 など。 0x41, 0x61, 0x31, 0xB1, 0xB6, 0xDE, 0x82, 0x60, 0x82, 0x50, 0x83, 0x41, 0x83, 0x4B, 0x88, 0x9F, // Windows-31J など。 0x5C, 0x81, 0x8F, 0x81, 0x50, 0x81, 0xF0, 0x87, 0x40, 0xFA, 0x67, 0xED, 0x4B, 0xFB, 0x7E, 0xEE, 0x62, 0x0D, 0x0A, // JIS X 0213 など。 0x98, 0x73, 0xF4, 0x49, 0xED, 0x5B, 0x83, 0x97, 0x86, 0x85); String w31jStr = decode(sjis, WINDOWS_31J); System.out.println("Windows-31J:"); System.out.println(toHexString(w31jStr)); System.out.println(w31jStr); } /** * バイト配列を指定された文字コードで文字列にデコードします。 * * @param bytes バイト配列 * @param charset 文字コード * @return 文字列 * @throws CharacterCodingException デコードに失敗した場合 */ static String decode(byte[] bytes, Charset charset) throws CharacterCodingException { ByteBuffer in = ByteBuffer.wrap(bytes); return charset.newDecoder().decode(in).toString(); }
実行結果:
java.nio.charset.MalformedInputException: Input length = 1
CharsetDecoder
は、変換できない文字があるとデフォルトで例外をスローします。
標準のメッセージだとエラー箇所が分かりにくいので、エラーの発生位置や入力情報を加えるなど、少し手を入れた方が良いかもしれません。
CharsetDecoder
に対してオプションを指定すると、変換できない文字があったときの動作を変更することができます。サンプルでは下駄記号 (〓) に置き換えてみました。
import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; // ... static void testDecoder2() throws CharacterCodingException { byte[] sjis = bytes( // JIS X 0201、JIS X 0208 など。 0x41, 0x61, 0x31, 0xB1, 0xB6, 0xDE, 0x82, 0x60, 0x82, 0x50, 0x83, 0x41, 0x83, 0x4B, 0x88, 0x9F, // Windows-31J など。 0x5C, 0x81, 0x8F, 0x81, 0x50, 0x81, 0xF0, 0x87, 0x40, 0xFA, 0x67, 0xED, 0x4B, 0xFB, 0x7E, 0xEE, 0x62, 0x0D, 0x0A, // JIS X 0213 など。 0x98, 0x73, 0xF4, 0x49, 0xED, 0x5B, 0x83, 0x97, 0x86, 0x85); String w31jStr = decode(sjis, WINDOWS_31J, "〓"); System.out.println("Windows-31J:"); System.out.println(toHexString(w31jStr)); System.out.println(w31jStr); } /** * バイト配列を指定された文字コードで文字列にデコードします。 * * @param bytes バイト配列 * @param charset 文字コード * @param replace デコードに失敗した場合、置き換える文字列 * @return 文字列 */ static String decode(byte[] bytes, Charset charset, String replace) { CharsetDecoder decoder = charset.newDecoder() .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE); if (replace != null) { decoder.replaceWith(replace); } try { return decoder .decode(ByteBuffer.wrap(bytes)) .toString(); } catch (CharacterCodingException e) { throw new UncheckedIOException(e); } }
実行結果:
Windows-31J: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AC 4E9C 005C FFE5 FFE3 212B 2460 5F45 5F45 FA19 FA19 000D 000A 3013 0073 E2F9 501E 3013 87BA 3013 Aa1アガA1アガ亜\¥ ̄Å①彅彅神神 〓s倞〓螺〓
1-4. Reader / Writer によるエンコード/デコード
本節では、ファイルの読み書きと同時にエンコード/デコードする方法を説明します。
Unicode から各文字コードへの変換 (エンコード)
java.io.OutputStreamWriter
クラスを使うと、文字コードを指定して、ファイルへの書き込みができます。
import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; // ... static void testWriter() throws IOException { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; File dir = new File("target/output"); dir.mkdirs(); File file = new File(dir, "out-1-w31j.txt"); PrintWriter out = new PrintWriter(new BufferedWriter( new OutputStreamWriter(new FileOutputStream(file), WINDOWS_31J)), true); try { out.println(text); } finally { out.close(); } }
出力ファイル:
Aa1アガA1アガ亜\¥ ̄Å①彅 ??神神?カ???
各文字コードから Unicode への変換 (デコード)
java.io.InputStreamReader
クラスを使うと、文字コードを指定して、ファイルからの読み込みができます。
import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; // ... static void testReader() throws IOException { File dir = new File("src/test/resources/input"); File file = new File(dir, "in-short-sjis.txt"); BufferedReader in = new BufferedReader( new InputStreamReader(new FileInputStream(file), WINDOWS_31J)); try { StringBuilder sb = new StringBuilder(); String line; while ((line = in.readLine()) != null) { sb.append(line); sb.append(System.lineSeparator()); } String s = sb.toString().stripTrailing(); System.out.println("Windows-31J:"); System.out.println(toHexString(s)); System.out.println(s); } finally { in.close(); } }
実行結果:
Windows-31J: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AC 4E9C 005C FFE5 FFE3 212B 2460 5F45 5F45 FA19 FA19 000D 000A FFFD 0073 E2F9 501E FFFD 87BA FFFD Aa1アガA1アガ亜\¥ ̄Å①彅彅神神 �s倞�螺�
java.nio.file.Files クラスを使った読み書き。
java.nio.file.Files
クラスにはファイル操作のための便利なメソッドがいくつも定義されていますが、エラー処理が不十分なため、符号化方式が UTF-8 で、ほとんどエラーが発生しないようなケースでのみ使用した方が良さそうです。
1-5. Unicode 正規化
java.text.Normalizer
クラスを使用すると、Unicode 正規化の各正規化形式へ変換することができます。
import java.text.Normalizer; import java.text.Normalizer.Form; // ... static void testNormalize() { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; System.out.println("Text:"); System.out.println(toHexString(text)); System.out.println(text); String nfd = Normalizer.normalize(text, Form.NFD); System.out.println("NFD:"); System.out.println(toHexString(nfd)); System.out.println(nfd); String nfc = Normalizer.normalize(text, Form.NFC); System.out.println("NFC:"); System.out.println(toHexString(nfc)); System.out.println(nfc); String nfkd = Normalizer.normalize(text, Form.NFKD); System.out.println("NFKD:"); System.out.println(toHexString(nfkd)); System.out.println(nfkd); String nfkc = Normalizer.normalize(text, Form.NFKC); System.out.println("NFKC:"); System.out.println(toHexString(nfkc)); System.out.println(nfkc); }
実行結果:
Text: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AC 4E9C 00A5 FFE5 FFE3 212B 2460 5F45 000D 000A D842 DF9F D84D DD94 FA19 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å①彅 𠮟𣖔神神︀カ゚˩˥ NFD: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AB 3099 4E9C 00A5 FFE5 FFE3 0041 030A 2460 5F45 000D 000A D842 DF9F D84D DD94 795E 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å①彅 ← 「ガ」が分解 𠮟𣖔神神︀カ゚˩˥ ← 「Å」「神」が別の文字に変換 × NFC: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AC 4E9C 00A5 FFE5 FFE3 00C5 2460 5F45 000D 000A D842 DF9F D84D DD94 795E 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å①彅 𠮟𣖔神神︀カ゚˩˥ ← 「Å」「神」が別の文字に変換 × NFKD: 0041 0061 0031 30A2 30AB 3099 0041 0031 30A2 30AB 3099 4E9C 00A5 00A5 0020 0304 0041 030A 0031 5F45 000D 000A D842 DF9F D84D DD94 795E 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å1彅 ← 「ガ」が分解、半角片仮名、全角英数字が正規化 𠮟𣖔神神︀カ゚˩˥ ← 「Å」「神」が別の文字に変換 × NFKC: 0041 0061 0031 30A2 30AC 0041 0031 30A2 30AC 4E9C 00A5 00A5 0020 0304 00C5 0031 5F45 000D 000A D842 DF9F D84D DD94 795E 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å1彅 ← 半角片仮名、全角英数字が正規化 𠮟𣖔神神︀カ゚˩˥ ← 「Å」「神」が別の文字に変換 ×
NFD、NFKD では、合成済み文字「ガ (30AC
)」が結合文字列「カ+゛(30AB 3099
)」に分解されています。
NFKC では、半角片仮名「ガ (FF76 FF9E
)」が全角片仮名「ガ (30AC
)」に、全角英数字「A (FF21
)」「1 (FF11
)」が半角英数字「A (0041
)」「1 (0031
)」に、それぞれ変換されています。
また、いずれの正規化形式でも、「Å (212B
)」が「Å (00C5
または 0041 030A
)」に、「神 (FA19
)」が「神 (795E
)」に変換されてしまい、元の情報が維持できません。この問題は、「3. よくある問題と対策」で考察します。
2. システム開発における注意点
本章では、具体的なシステム開発の例を用いて、文字データ、文字コードの設計を試行してみたいと思います。
2-1. ケーススタディ
とあるウェブシステムの開発に携わることになりました。利用者の多くは日本人ですが、日本語の範囲内で多くの文字種が扱えるようにしたいと考えています。
また、当該システムは PC ベースの他システムおよびメインフレームと接続する必要があります。各システムで利用可能な文字は表のとおりです。
システム | データ項目 | 利用可能な文字 |
---|---|---|
他システム | フリガナ | JIS X 0208 片仮名 (全角片仮名) |
その他 | マイクロソフト標準キャラクタセット (Windows-31J) | |
メインフレーム | フリガナ | EBCDIC カナ文字拡張 (英小文字不可) |
その他 | IBM漢字 (JIS X 0208、IBM拡張文字) | |
共通 | 口座名等 | 0123456789 (数字) |
ABCDEFGHIJKLMNOPQRSTUVWXYZ (英大文字) | ||
アイウエオ カキクケコ サシスセソ タチツテト ナニヌネノ ハヒフヘホ マミムメモ ヤユヨ ラリルレロ ワン (カナ) | ||
゙ (濁点)、゚ (半濁点) | ||
- (ハイフン)、( ) (丸括弧)、. (ピリオド)、␣ (スペース) | ||
EDI情報等 | 上記に加えて、ヲ、¥ (円記号)、「 」 (かぎ括弧)、/ (スラッシュ) |
2-2. データ形式の設計
さて、本システムにおけるデータ形式の設計ですが、ケーススタディの要求、OS・ミドルウェア・プログラミング言語の対応状況等から、基本は Unicode ベースになると思います。扱う言語が日本語である点と、他システム連携を考慮して、文字集合は、マイクロソフト標準キャラクタセット (CommonJ)、JIS X 0213、JIS X 0212、+α としました。+α の部分は要件定義で詳細化します。
記録形式
データの記録形式は、DBMS の選定にもよりますが、とある RDBMS を採用し、文字コードは Unicode をすべて格納可能な UTF-8 としました。
Unicode の重複収録文字については、原則として英数字は半角、片仮名は全角へ寄せたいと考えています。
口座名等の特殊な形式については、他システムにそのまま渡せる形式で保持するか、一般的な形式で保持し、他システムに渡す際に変換するか、方法がいくつかありますが、他の用途で使用することがなければ、トラブルを防ぐ意味で、そのまま渡せる形式で保持しようと考えています。
転送形式
ウェブアプリ、ウェブ API で使用するデータ形式は、OS・ブラウザの対応状況、普及率、拡張性を考慮すれば、UTF-8 一択でしょう。最新の HTML 標準でも UTF-8 が推奨されており、全世界の90%以上のウェブサイトが UTF-8 を採用との報告もあります。他システム連携のデータ形式は各システム指定の文字コードにしましたが、状況次第ではこちらも UTF-8 にするかもしれません。
2-3. データ入出力時の処理
システムに対してデータを入出力する際、エンコード/デコード、入力チェック、形式変換などの処理が一般に行われます。本節では、これらの処理の設計について考察したいと思います。
入力チェック vs 入力形式変換
入力画面で「半角で入力してください」「全角で入力してください」と指定している項目をときどき見かけます。私は、「切り替えや変換が面倒だな」と思ったり、IME が変な変換の癖を覚えてしまってイラっとすることがありますが、みなさんはどうでしょう? 半角・全角変換、大文字・小文字変換、平仮名・片仮名変換のようにシステムで一意に変換可能なものは、システム内の入力形式変換で変換してあげた方がユーザビリティは高いと思います。IME で入力文字が制限できればそれもよいと思います。
ケーススタディであげた「口座名」であれば、英字の小文字・大文字変換 (a → A)、片仮名の小文字・大文字変換 (キャ → キヤ)、長音からハイフンへの変換 (ー → -) なども、システム側で補助した方がよいでしょう。
なお、「利用者が入力したデータをそのまま保存しなければならない」という制約がある場合は、変換後のデータを利用者に確認してもらうような画面遷移にするとよいと思います。
形式変換前チェック vs 形式変換後チェック
入力データの形式変換を行う場合、入力チェックを変換前に行うか、変換後に行うかよく悩みます。
変換前にチェックを行った方が、エラーの発見が早く、エラー発生時に適切なメッセージを返しやすいのですが、変換を考慮して広めに許容する必要があります。逆に、変換後にチェックを行う場合、チェックルールが素直に書けますが、エラー発生時のメッセージをそのまま返すと利用者に分かりにくく、エラーメッセージの変換が必要になる場合があります。どちらがよいか一概には言えませんが、個人的には入力チェックは形式変換の前に行うのが、トータルでシンプルになる気がします。
なお、システムがレイヤーやマイクロサービスなどに分割される場合は、エラー発生時の早期発見と原因特定、被害の拡大防止の観点から、各責任分界点の内側で最低限の形式チェックを行うことも検討した方がよいかもしれません。そのとき発生するエラーは、入力チェックエラーではなくシステムエラー (バグ) として扱うことになると思います。
タイミング | メリット | デメリット |
---|---|---|
形式変換前チェック | ・エラーの発見が早い。 ・適切なエラーメッセージが返しやすい。 |
・変換を考慮してチェックを記述する必要がある。 |
形式変換後チェック | ・チェックルールが素直に書ける。 | ・適切なエラーメッセージを返しにくい。 |
置換文字へ変換 vs 類似文字へ置換
他システムに連携するデータに相手先システムが扱えない文字が含まれる場合、〓 のような置換文字へ一律変換してしまうのが楽ですが、厳密性よりも利便性が求められるデータであれば、類似の文字へ変換することを検討した方がよいかもしれません。例えば、JIS X 0213 固有文字を含む「八神さん」へのメールや郵便物を JIS X 0208 ベースのシステムへ連携して処理する場合、宛先、本文などを「八〓様」とするより「八神様」と出力した方が好ましいと思います。
「2-2. データ形式の設計」でも触れましたが、口座名等の厳密性が求められる項目については、トラブルを防ぐ意味で、出力時に変換するのではなく、そのまま渡せる形式でシステムに保持した方がよいと思います。
3. よくある問題と対策
本章では、システム開発において、文字コードに関して、よくある問題と対策案について解説します。
3-1. 円記号問題
JIS X 0201 制定の際、ASCII の「逆斜線 (\, 0x5C)」、「チルダ (~, 0x7E)」を「円記号 (¥)」、「オーバーライン ( ̄)」に置き換えたことに起因する問題です。
問題点
ASCII、JIS X 0201 の 0x5C について、以下の問題があります。
- 0x5C が ASCII、EUC-JP 等では「逆斜線 (\)」、JIS X 0201、Shift_JIS 等では「円記号 (¥)」に割り当てられている。
- Shift_JIS の 0x5C を Unicode の U+00A5 (YEN SIGN) にマッピングした場合、0x5C に特別な機能を持たせている OS やプログラミング言語が不具合を起こす。
- Windows のディレクトリ区切り文字
- プログラミング言語のエスケープシーケンス (\n, \t など)
- Shift_JIS の 0x5C を Unicode の U+005C (REVERSE SOLIDUS) にマッピングした場合、U+005C が環境によって「逆斜線 (\)」で表示されたり、「円記号 (¥)」で表示されたりする。
同様に、ASCII、JIS X 0201 の 0x7E について、以下の問題があります。
- 0x7E が ASCII、EUC-JP 等では「チルダ (~)」、JIS X 0201、Shift_JIS 等では「オーバーライン ( ̄)」に割り当てられている。
- Shift_JIS の 0x7E を Unicode の U+203E (OVERLINE) にマッピングした場合、0x7E に特別な機能を持たせている OS やプログラミング言語が不具合を起こす。
- UNIX 系の OS のホームディレクトリ
- ビット演算子の NOT、正規表現との比較
- Shift_JIS の 0x7E を Unicode の U+007E (TILDE) にマッピングした場合、U+007E が環境によって「チルダ (~)」で表示されたり、「オーバーライン ( ̄)」で表示されたりする。
対策案
現時点で取りうる対策はこんな感じかと思います。
- EUC-JP、Shift_JIS などの従来の符号化方式ではなく、Unicode (UTF-8 等) を使用する。
- ディレクトリ区切り文字、プログラミング言語のエスケープシーケンスなど、見た目よりも機能の保証が重要な場合は、U+005C、U+007E を使用する。
- 通常の文章では、U+005C、U+007E を逆斜線、チルダの意味でのみ用いる。
- 逆斜線、チルダで「表示」されることを重視する場合は全角形 U+FF3C、U+FF5E の使用を検討する
(ただし、U+FF5E は JIS X 0208 のみ対応の環境では使用不可)。 - 円記号は U+00A5 または U+FFE5 を用い、U+005C の誤用を避ける。
- オーバーラインは U+203E または U+FFE3 を用いる。
妥当な変換表
私が考える、ネイティブコード (Shift_JIS、EUC-JP など、従来の文字コードをこう呼ぶことにします) → Unicode、Unicode → ネイティブコードの妥当な変換表です。ラウンドトリップにより、同じ文字へ戻るようにしています。
Shift_JIS の 0x5C、0x7E は、仕様的には U+00A5、U+207E にデコードするのが本来ですが、多くの変換ツールは U+005C、U+007E にデコードしており、私もこの変換が妥当だと思います。なお、JIS X 0201 と明示した場合は、仕様本来の U+00A5、U+207E にデコードすることにします。
「オーバーライン (‾, U+203E)」の全角形として、「全角マクロン ( ̄, U+FFE3)」を使用するため、「マクロン (¯, U+00AF)」も参考として掲載しました。
(表は横にスクロールしてご覧ください)
JIS X 0201 |
EUC-JP | Shift_JIS | Windows -31J |
Shift_JIS -2004 |
文字 | Unicode | JIS X 0201 |
EUC-JP | Shift_JIS (Windows-31J) |
Shift_JIS -2004 |
||
---|---|---|---|---|---|---|---|---|---|---|---|---|
- | 5C |
5C |
5C |
5C |
→ | \ | U+005C | → | 5C ▲ |
5C ○ |
5C ○ |
5C ○ |
- | A1C0 |
815F |
815F |
815F |
→ | \ | U+FF3C | → | 5C ▲ |
A1C0 ○ |
815F ○ |
815F ○ |
5C |
- | - | - | - | → | ¥ | U+00A5 | → | 5C ○ |
A1EF △ |
818F △ |
818F △ |
- | A1EF |
818F |
818F |
818F |
→ | ¥ | U+FFE5 | → | 5C △ |
A1EF ○ |
818F ○ |
818F ○ |
- | 7E |
7E |
7E |
7E |
→ | ~ | U+007E | → | 7E ▲ |
7E ○ |
7E ○ |
7E ○ |
- | 8FA2B7 |
- | 8160 |
81B0 |
→ | ~ | U+FF5E | → | 7E ▲ |
8FA2B7 ○ |
8160 ○ |
81B0 ○ |
- | A1C1 |
8160 |
- | 8160 |
→ | 〜 | U+301C | → | 7E ▲ |
A1C1 ○ |
8160 ○ |
8160 ○ |
7E |
- | - | - | - | → | ‾ | U+203E | → | 7E ○ |
A1B1 △ |
8150 △ |
8150 △ |
- | A1B1 |
8150 |
8150 |
8150 |
→ |  ̄ | U+FFE3 | → | 7E △ |
A1B1 ○ |
8150 ○ |
8150 ○ |
- | 8FA2B4 |
- | - | 854A |
→ | ¯ | U+00AF | → | 7E △ |
8FA2B4 ○ |
8150 △ |
854A ○ |
- ○: ラウンドトリップで同じ文字に戻る。
- △: ラウンドトリップで似た文字に変換。
- ▲: ラウンドトリップで異なる文字に変換。
- 全角チルダ (~) については「3-2. 波ダッシュ問題」も参照。
同じ変換で、Unicode → ネイティブコード → Unicode のラウンドトリップを行ったのが次の表です。この場合も、できるだけ同じ文字へ戻るようになっています。真ん中の JIS X 0201 列の ( ) はエンコードの場合のみ適用する対応です。
多くの変換ツールで U+00A5、U+002E を Shift_JIS にエンコードすると 0x5C、0x7E に変換されるのですが、「円記号 (¥)」「オーバーライン ( ̄)」が「逆斜線 (\)」「チルダ (~)」に変わってしまい、好ましくありません。個人的な意見としては、全角の 0x818F、0x8150 に変換する方がよいと思います。
具体的には、ネイティブコードにエンコードする前に、U+00A5 を U+FFE5 に、U+203E を U+FFE3 に変換します。Java であれば、CharsetEncoder をカスタマイズして、変換処理を組み込むことも可能です。
なお、JIS X 0201 の場合は、全角が使えないので、0x5C、0x7E にエンコードしています。
(表は横にスクロールしてご覧ください)
文字 | Unicode | JIS X 0201 |
EUC-JP | Shift_JIS | Windows -31J |
Shift_JIS -2004 |
文字 | Unicode | ||
---|---|---|---|---|---|---|---|---|---|---|
\ | U+005C | → | (5C ) |
5C |
5C |
5C |
5C |
→ | \ | U+005C ○ |
\ | U+FF3C | → | (5C ) |
A1C0 |
815F |
815F |
815F |
→ | \ | U+FF3C ○ |
¥ | U+00A5 | → | 5C |
- | - | - | - | → | ¥ | U+00A5 ○ |
→ | - | A1EF |
818F |
818F |
818F |
→ | ¥ | U+FFE5 △ | ||
¥ | U+FFE5 | → | (5C ) |
A1EF |
818F |
818F |
818F |
→ | ¥ | U+FFE5 ○ |
~ | U+007E | → | (7E ) |
7E |
7E |
7E |
7E |
→ | ~ | U+007E ○ |
~ | U+FF5E | → | (7E ) |
8FA2B7 |
(8160 ) |
8160 |
81B0 |
→ | ~ | U+FF5E ○ |
〜 | U+301C | → | (7E ) |
A1C1 |
8160 |
(8160 ) |
8160 |
→ | 〜 | U+301C ○ |
‾ | U+203E | → | 7E |
- | - | - | - | → | ‾ | U+203E ○ |
→ | - | A1B1 |
8150 |
8150 |
8150 |
→ |  ̄ | U+FFE3 △ | ||
 ̄ | U+FFE3 | → | (7E ) |
A1B1 |
8150 |
8150 |
8150 |
→ |  ̄ | U+FFE3 ○ |
¯ | U+00AF | → | (7E ) |
- | 8150 |
8150 |
- | → |  ̄ | U+FFE3 △ |
→ | - | 8FA2B4 |
- | - | 854A |
→ | ¯ | U+00AF ○ |
- ○: ラウンドトリップで同じ文字に戻る。
- △: ラウンドトリップで似た文字に変換。
- 全角チルダ (~) については「3-2. 波ダッシュ問題」も参照。
3-2. 波ダッシュ問題
本来同じ符号化方式のはずの Shift_JIS と Windows-31J で、Unicode との変換に差異があることに起因する問題です。
(表は横にスクロールしてご覧ください)
区点 | SJIS | 名称 | 文字 | Shift_JIS でデコード | Windows-31J でデコード |
---|---|---|---|---|---|
1-29 | 815C |
EM DASH | — | U+2014 (EM DASH) |
U+2015 (HORIZONTAL BAR) |
1-33 | 8160 |
WAVE DASH | 〜 | U+301C (WAVE DASH) |
U+FF5E (FULLWIDTH TILDE) |
1-34 | 8161 |
DOUBLE VERTICAL LINE | ‖ | U+2016 (DOUBLE VERTICAL LINE) |
U+2225 (PARALLEL TO) |
1-61 | 817C |
MINUS SIGN | − | U+2212 (MINUS SIGN) |
U+FF0D (FULLWIDTH HYPHEN-MINUS) |
1-81 | 8191 |
CENT SIGN | ¢ | U+00A2 (CENT SIGN) |
U+FFE0 (FULLWIDTH CENT SIGN) |
1-82 | 8192 |
POUND SIGN | £ | U+00A3 (POUND SIGN) |
U+FFE1 (FULLWIDTH POUND SIGN) |
2-44 | 81CA |
NOT SIGN | ¬ | U+00AC (NOT SIGN) |
U+FFE2 (FULLWIDTH NOT SIGN) |
問題点
変換の差異のため、以下の問題が発生します。
ネイティブコード → (Shift_JIS でデコード) → Unicode → (Windows-31J でエンコード) → ネイティブコードのような変換を行うと、これらの文字はエラーとなり、「?」に置換されたり、例外が発生します。
- 例)
8160
→ (Shift_JIS でデコード) → U+301C → (Windows-31J でエンコード) → エラー
また、Unicode → (Shift_JIS でエンコード) → ネイティブコード → (Windows-31J でデコード) → Unicode のような変換を行うと、異なる符号位置に変換されてしまいます。
- 例) U+301C → (Shift_JIS でエンコード) →
8160
→ (Windows-31J でデコード) → U+FF5E
対策案
最初の問題については、どちらの Unicode も、Shift_JIS でも Windows-31J でも同じネイティブコードへエンコードされるように修正します。例えば、U+301C、U+FF3E どちらも 8160
へ変換します。
2番目の問題は、現状を許容するか、Shift_JIS でも Windows-31J でも同じ Unicode へデコードされるように、一方へ寄せます。
妥当な変換表
私が考える、ネイティブコード → Unicode、Unicode → ネイティブコードの妥当な変換表です。ラウンドトリップにより、できるだけ同じ文字へ戻るようにしています。
対策案で書いたように、例えば、U+301C、U+FF5E どちらの Unicode も、Shift_JIS でも Windows-31J でも同じネイティブコード 8160
へエンコードします。
具体的には、Shift_JIS でエンコードする前に U+FF5E を U+301C に変換し、Windows-31 でエンコードする前に U+301C を U+FF5E に変換します。Java であれば、CharsetEncoder をカスタマイズして、変換処理を組み込むことも可能です。
(表は横にスクロールしてご覧ください)
EUC-JP | Shift_JIS | Windows -31J |
Shift_JIS -2004 |
文字 | Unicode | EUC-JP | Shift_JIS (Windows-31J) |
Shift_JIS -2004 |
||
---|---|---|---|---|---|---|---|---|---|---|
A1BD |
815C |
- | 815C |
→ | — | U+2014 | → | A1BD ○ |
815C ○ |
815C ○ |
- | - | 815C |
- | → | — | U+2015 | → | A1BD ○ |
815C ○ |
815C ○ |
A1C1 |
8160 |
- | 8160 |
→ | 〜 | U+301C | → | A1C1 ○ |
8160 ○ |
8160 ○ |
8FA2B7 |
- | 8160 |
81B0 |
→ | ~ | U+FF5E | → | 8FA2B7 ○ |
8160 ○ |
81B0 ○ |
A1C2 |
8161 |
- | 8161 |
→ | ‖ | U+2016 | → | A1C2 ○ |
8161 ○ |
8161 ○ |
- | - | 8161 |
81D2 |
→ | ∥ | U+2225 | → | A1C2 ○ |
8161 ○ |
81D2 ○ |
A1DD |
817C |
- | 817C |
→ | − | U+2212 | → | A1DD ○ |
817C ○ |
817C ○ |
- | - | 817C |
81AF |
→ | - | U+FF0D | → | A1DD ○ |
817C ○ |
81AF ○ |
A1F1 |
8191 |
- | 8191 |
→ | ¢ | U+00A2 | → | A1F1 ○ |
8191 ○ |
8191 ○ |
- | - | 8191 |
- | → | ¢ | U+FFE0 | → | A1F1 ○ |
8191 ○ |
8191 ○ |
A1F2 |
8192 |
- | 8192 |
→ | £ | U+00A3 | → | A1F2 ○ |
8192 ○ |
8192 ○ |
- | - | 8192 |
- | → | £ | U+FFE1 | → | A1F2 ○ |
8192 ○ |
8192 ○ |
A2CC |
81CA |
- | 81CA |
→ | ¬ | U+00AC | → | A2CC ○ |
81CA ○ |
81CA ○ |
- | - | 81CA |
- | → | ¬ | U+FFE2 | → | A2CC ○ |
81CA ○ |
81CA ○ |
- ○: ラウンドトリップで同じ文字に戻る。ただし、符号可能式を変えた場合は似た文字へ変換もあり。
同じ変換で、Unicode → ネイティブコード → Unicode のラウンドトリップを行ったのが次の表です。この場合も、できるだけ同じ文字へ戻るようになっています。
真ん中4列の ( ) はエンコードの場合のみ適用する対応です。右の Unicode 列の ( ) は Shift_JIS、Windows-31J でデコードするとき、片寄せする場合の対応です。U+2015、U+FF5E、U+2225、U+FF0D を U+2014、U+301C、U+2016、U+2212 へ片寄せするのはよいと思いますが、U+00A2、U+00A3、U+00AC と U+FFE0、U+FFE1、U+FFE2 については、EBCDIC のように半角の ¢、£、¬ が存在する文字集合を考慮すると後者へ寄せるもありかなと思います。対策案で書いたように、現状のラウンドトリップを優先するなら、デコード時の変換はなしでもよいかもしれません。
(表は横にスクロールしてご覧ください)
文字 | Unicode | EUC-JP | Shift_JIS | Windows-31J | Shift_JIS-2004 | 文字 | Unicode | ||
---|---|---|---|---|---|---|---|---|---|
— | U+2014 | → | A1BD |
815C |
(815C ) |
815C |
→ | — | U+2014 ○ |
― | U+2015 | → | (A1BD ) |
(815C ) |
815C |
(815C ) |
→ | ― | U+2015 ○ (U+2014) |
〜 | U+301C | → | A1C1 |
8160 |
(8160 ) |
8160 |
→ | 〜 | U+301C ○ |
~ | U+FF5E | → | 8FA2B7 |
(8160 ) |
8160 |
81B0 |
→ | ~ | U+FF5E ○ (U+301C) |
‖ | U+2016 | → | A1C2 |
8161 |
(8161 ) |
8161 |
→ | ‖ | U+2016 ○ |
∥ | U+2225 | → | (A1C2 ) |
(8161 ) |
8161 |
81D2 |
→ | ∥ | U+2225 ○ (U+2016) |
− | U+2212 | → | A1DD |
817C |
(817C ) |
817C |
→ | − | U+2212 ○ |
- | U+FF0D | → | (A1DD ) |
(817C ) |
817C |
81AF |
→ | - | U+FF0D ○ (U+2212) |
¢ | U+00A2 | → | A1F1 |
8191 |
(8191 ) |
8191 |
→ | ¢ | U+00A2 ○ (U+FFE0) |
¢ | U+FFE0 | → | (A1F1 ) |
(8191 ) |
8191 |
(8191 ) |
→ | ¢ | U+FFE0 ○ ~~(U+00A2)~~ |
£ | U+00A3 | → | A1F2 |
8192 |
(8192 ) |
8192 |
→ | £ | U+00A3 ○ (U+FFE1) |
£ | U+FFE1 | → | (A1F2 ) |
(8192 ) |
8192 |
(8192 ) |
→ | £ | U+FFE1 ○ ~~(U+00A3)~~ |
¬ | U+00AC | → | A2CC |
81CA |
(81CA ) |
81CA |
→ | ¬ | U+00AC ○ (U+FFE2) |
¬ | U+FFE2 | → | (A2CC ) |
(81CA ) |
81CA |
(81CA ) |
→ | ¬ | U+FFE2 ○ ~~(U+00AC)~~ |
- ○: ラウンドトリップで同じ文字に戻る。ただし、符号可能式を変えた場合は似た文字へ変換もあり。
3-3. 半角・全角
ISO-2022-JP、EUC-JP、Shift_JIS などの符号化方式、Unicode の文字集合では、いわゆる「半角文字」「全角文字」が重複収録されています。これらを一方へ寄せることを考えます。
方法としては、a) ラテン文字を半角、片仮名を全角へ寄せる方法 (本節ではこれを「正規化」と呼ぶことにします、「Unicode 正規化」と似ていますが全く同じではありません)、b) ラテン文字・片仮名とも半角へ寄せる、c) ラテン文字・片仮名とも全角へ寄せる、の3通りが考えられます。これらを順に考察していきます。
なお、前回説明しましたが、ASCII のいくつかの記号について、JIS X 0208 では対応する全角文字が1対1で定義されていません。
(表は横にスクロールしてご覧ください)
半角 | 全角 | ||||||
---|---|---|---|---|---|---|---|
ASCII | 文字 | Unicode | JIS X 0208 |
W31J | JIS X 0213 |
文字 | Unicode |
22 |
“ | U+0022 (QUOTATION MARK) |
- | 115-24 (92-94) |
1-2-16 | " | U+FF02 (FULLWIDTH QUOTATION MARK) |
1-15 | 1-15 | 1-1-15 | ¨ | U+00A8 (DIAERESIS) |
|||
1-40 | 1-40 | 1-1-40 | “ | U+201C (LEFT DOUBLE QUOTATION MARK) |
|||
1-41 | 1-41 | 1-1-41 | ” | U+201D (RIGHT DOUBLE QUOTATION MARK) |
|||
1-77 | 1-77 | 1-1-77 | ″ | U+2033 (DOUBLE PRIME) |
|||
27 |
’ | U+0027 (APOSTROPHE) |
- | 115-23 (92-93) |
1-2-15 | ' | U+FF07 (FULLWIDTH APOSTROPHE) |
1-13 | 1-13 | 1-1-13 | ´ | U+00B4 (ACUTE ACCENT) |
|||
1-38 | 1-38 | 1-1-38 | ‘ | U+2018 (LEFT SINGLE QUOTATION MARK) |
|||
1-39 | 1-39 | 1-1-39 | ’ | U+2019 (RIGHT SINGLE QUOTATION MARK) |
|||
1-76 | 1-76 | 1-1-76 | ′ | U+2032 (PRIME) |
|||
2D |
- | U+002D (HYPHEN-MINUS) |
- | 1-61 | 1-2-17 | - | U+FF0D (FULLWIDTH HYPHEN-MINUS) |
1-30 | 1-30 | 1-1-30 | ‐ | U+2010 (HYPHEN) |
|||
1-61 | - | 1-1-61 | − | U+2212 (MINUS SIGN) |
|||
7E |
~ | U+007E (TILDE) |
- | 1-33 | 1-2-18 | ~ | U+FF5E (FULLWIDTH TILDE) |
1-33 | - | 1-1-33 | 〜 | U+301C (WAVE DASH) |
正規化
記号については、むやみに全角・半角変換を行うと、円記号問題や波ダッシュ問題に関連して問題が発生したり、表示上の見た目が大幅に変わることもあるため、a1) 英数字のみ半角へ寄せるパターン、a2) 記号を含むすべての全角形を半角へ寄せるパターンの2つを考えてみました。なお、半角片仮名は、推奨されないケースが多いので、どちらの場合も全角へ寄せます。
a1 は通常の入力データを記録する前の変換、a2 は検索インデックスを作成する際の変換、といった使い方を想定しています。
変換範囲 | 説明 | 用途 | |
---|---|---|---|
a1 | 英数字を半角へ、片仮名を全角へ寄せる。 | 円記号問題、波ダッシュ問題、表示上の見た目等あるので、英数字のみ半角へ寄せる。 | 通常のデータ記録時など |
a2 | 全角形を半角へ、半角形を全角へ寄せる。 | すべての全角形を半角へ寄せる。 | 検索インデックス作成時など |
具体的な変換イメージは以下の通りです。
元の文字 | a1 | a2 | 備考 | ||||
---|---|---|---|---|---|---|---|
文字 | Unicode | 文字 | Unicode | 文字 | Unicode | ||
U+3000 | → | (U+3000) | (SP) | U+0020 | スペース | ||
1 | U+FF11 | → | 1 | U+0031 | 1 | U+0031 | 数字 |
A | U+FF21 | → | A | U+0041 | A | U+0041 | 英大文字 |
a | U+FF41 | → | a | U+0061 | a | U+0061 | 英小文字 |
, | U+FF0C | → | , | (U+FF0C) | , | U+002C | コンマ |
. | U+FF0E | → | . | (U+FF0E) | . | U+002E | ピリオド |
" | U+FF02 | → | " | (U+FF02) | ” | U+0022 | 二重引用符 |
' | U+FF07 | → | ' | (U+FF07) | ’ | U+0027 | アポストロフィ |
( | U+FF08 | → | ( | (U+FF08) | ( | U+0028 | 始め丸括弧 |
) | U+FF09 | → | ) | (U+FF09) | ) | U+0029 | 終わり丸括弧 |
[ | U+FF3B | → | [ | (U+FF3B) | [ | U+005B | 始め角括弧 |
] | U+FF3D | → | ] | (U+FF3D) | ] | U+005D | 終わり角括弧 |
+ | U+FF0B | → | + | (U+FF0B) | + | U+002B | プラス |
- | U+FF0D | → | - | (U+FF0D) | - | U+002D | ハイフン・マイナス |
* | U+FF0A | → | * | (U+FF0A) | * | U+002A | アスタリスク |
/ | U+FF0F | → | / | (U+FF0F) | / | U+002F | 斜線 |
\ | U+FF3C | → | \ | (U+FF3C) | \ | U+005C | 逆斜線 |
~ | U+FF5E | → | ~ | (U+FF5E) | ~ | U+007E | チルダ |
¥ | U+FFE5 | → | ¥ | (U+FFE5) | ¥ | U+00A5 | 円記号 |
 ̄ | U+FFE3 | → |  ̄ | (U+FFE3) | ‾ | U+203E | オーバーライン |
ア | U+FF71 | → | ア | U+30A2 | ア | U+30A2 | 片仮名 |
ガ | U+FF76 U+FF9E | → | ガ | U+30AC | ガ | U+30AC | 片仮名 (濁音) |
カ゚ | U+FF76 U+FF9F | → | カ゚ | U+30AB U+309A | カ゚ | U+30AB U+309A | 片仮名 (結合文字列) |
パ | U+FF8A U+FF9F | → | パ | U+30D1 | パ | U+30D1 | 片仮名 (半濁音) |
キャ | U+FF77 U+FF6C | → | キャ | U+30AD U+30E3 | キャ | U+30AD U+30E3 | 片仮名 (拗音) |
ヲ | U+FF66 | → | ヲ | U+30F2 | ヲ | U+30F2 | 片仮名 (ヲ) |
ヴ | U+FF73 U+FF9E | → | ヴ | U+30F4 | ヴ | U+30F4 | 片仮名 (ヴ) |
゙ | U+FF9E | → | ゛ | U+3099 | ゛ | U+3099 | 濁点 |
゚ | U+FF9F | → | ゜ | U+309A | ゜ | U+309A | 半濁点 |
ー | U+FF70 | → | ー | U+30FC | ー | U+30FC | 長音記号 |
・ | U+FF65 | → | ・ | U+30FB | ・ | U+30FB | 中点 |
、 | U+FF64 | → | 、 | U+3001 | 、 | U+3001 | 読点 |
。 | U+FF61 | → | 。 | U+3002 | 。 | U+3002 | 句点 |
「 | U+FF62 | → | 「 | U+300C | 「 | U+300C | 始めかぎ括弧 |
」 | U+FF63 | → | 」 | U+300D | 」 | U+300D | 終わりかぎ括弧 |
¢ | U+FFE0 | → | ¢ | (U+FFE0) | ¢ | U+00A2 | セント記号 |
£ | U+FFE1 | → | £ | (U+FFE1) | £ | U+00A3 | ポンド記号 |
¬ | U+FFE2 | → | ¬ | (U+FFE2) | ¬ | U+00AC | 否定 |
¦ | U+FFE4 | → | ¦ | (U+FFE4) | ¦ | U+00A6 | 破断線 |
半角変換
ASCII や JIS X 0201 などの1バイト項目にデータを格納するための変換です。半角・全角が直接対応する文字以外にも、左右引用符 (“, ”, ‘, ’)、ハイフン (‐)、マイナス (−) 等は意味的に半角変換してよいと思います。銀行とのデータ連携では JIS X 0201 より使用可能な文字が限られるので、英小文字から英大文字への変換、小書き片仮名から大文字の通常の片仮名への変換、長音からハイフンへの変換などを用意すると便利でしょう。
変換範囲 | 説明 | 用途 | |
---|---|---|---|
b1 | 直接対応する文字+α | 左右引用符、ハイフン、マイナス等も変換。 | 古いシステムとのデータ連携 |
b2 | 全銀使用可能文字 | 英小文字、小書き片仮名、長音 (ー) 等も変換。 | 銀行とのデータ連携 |
具体的な変換イメージは以下の通りです。
元の文字 | b1 | b2 | 備考 | ||||
---|---|---|---|---|---|---|---|
文字 | Unicode | 文字 | Unicode | 文字 | Unicode | ||
U+3000 | → | (SP) | U+0020 | (SP) | U+0020 | スペース | |
1 | U+FF11 | → | 1 | U+0031 | 1 | U+0031 | 数字 |
A | U+FF21 | → | A | U+0041 | A | U+0041 | 英大文字 |
a | U+FF41 | → | a | U+0061 | A | U+0041 | 英小文字 |
, | U+FF0C | → | , | U+002C | - | - | コンマ |
. | U+FF0E | → | . | U+002E | . | U+002E | ピリオド |
" | U+FF02 | → | “ | U+0022 | - | - | 二重引用符 |
“ | U+201C | → | ” | U+0022 | - | - | 左二重引用符 |
” | U+201D | → | “ | U+0022 | - | - | 右二重引用符 |
' | U+FF07 | → | ’ | U+0027 | - | - | アポストロフィ |
‘ | U+2018 | → | ’ | U+0027 | - | - | 左単一引用符 |
’ | U+2019 | → | ’ | U+0027 | - | - | 右単一引用符 |
( | U+FF08 | → | ( | U+0028 | ( | U+0028 | 始め丸括弧 |
) | U+FF09 | → | ) | U+0029 | ) | U+0029 | 終わり丸括弧 |
[ | U+FF3B | → | [ | U+005B | - | - | 始め角括弧 |
] | U+FF3D | → | ] | U+005D | - | - | 終わり角括弧 |
+ | U+FF0B | → | + | U+002B | - | - | プラス |
- | U+FF0D | → | - | U+002D | - | U+002D | ハイフン・マイナス |
‐ | U+2010 | → | - | U+002D | - | U+002D | ハイフン |
− | U+2212 | → | - | U+002D | - | U+002D | マイナス |
* | U+FF0A | → | * | U+002A | - | - | アスタリスク |
/ | U+FF0F | → | / | U+002F | / | (U+002F) | 斜線 |
\ | U+FF3C | → | \ | U+005C | - | - | 逆斜線 |
~ | U+FF5E | → | ~ | U+007E | - | - | チルダ |
〜 | U+301C | → | ~ | U+007E | - | - | 波ダッシュ |
¥ | U+FFE5 | → | ¥ | U+00A5 | ¥ | (U+00A5) | 円記号 |
 ̄ | U+FFE3 | → | ‾ | U+203E | - | - | オーバーライン |
あ | U+3042 | → | ア | U+FF71 | ア | U+FF71 | 平仮名 |
ア | U+30A2 | → | ア | U+FF71 | ア | U+FF71 | 片仮名 |
が | U+304C | → | ガ | U+FF76 U+FF9E | ガ | U+FF76 U+FF9E | 平仮名 (濁音) |
ガ | U+30AC | → | ガ | U+FF76 U+FF9E | ガ | U+FF76 U+FF9E | 片仮名 (濁音) |
か゚ | U+304B U+309A | → | カ゚ | U+FF76 U+FF9F | カ゚ | U+FF76 U+FF9F | 平仮名 (結合文字列) |
カ゚ | U+30AB U+309A | → | カ゚ | U+FF76 U+FF9F | カ゚ | U+FF76 U+FF9F | 片仮名 (結合文字列) |
ぱ | U+3071 | → | パ | U+FF8A U+FF9F | パ | U+FF8A U+FF9F | 平仮名 (半濁音) |
パ | U+30D1 | → | パ | U+FF8A U+FF9F | パ | U+FF8A U+FF9F | 片仮名 (半濁音) |
きゃ | U+304D U+3083 | → | キャ | U+FF77 U+FF6C | キヤ | U+FF77 U+FF94 | 平仮名 (拗音) |
キャ | U+30AD U+30E3 | → | キャ | U+FF77 U+FF6C | キヤ | U+FF77 U+FF94 | 片仮名 (拗音) |
を | U+3092 | → | ヲ | U+FF66 | オ | (U+FF75) | 平仮名 (を) |
ヲ | U+30F2 | → | ヲ | U+FF66 | オ | (U+FF75) | 片仮名 (ヲ) |
ゔ | U;3094 | → | ヴ | U+FF73 U+FF9E | ヴ | U+FF73 U+FF9E | 平仮名 (ゔ) |
ヴ | U+30F4 | → | ヴ | U+FF73 U+FF9E | ヴ | U+FF73 U+FF9E | 片仮名 (ヴ) |
゛ | U+3099 | → | ゙ | U+FF9E | ゙ | U+FF9E | 濁点 (結合文字) |
゛ | U+309B | → | ゙ | U+FF9E | ゙ | U+FF9E | 濁点 |
゜ | U+309A | → | ゚ | U+FF9F | ゚ | U+FF9F | 半濁点 (結合文字) |
゜ | U+309C | → | ゚ | U+FF9F | ゚ | U+FF9F | 半濁点 |
ー | U+30FC | → | ー | U+FF70 | - | U+002D | 長音記号 |
・ | U+30FB | → | ・ | U+FF65 | - | - | 中点 |
、 | U+3001 | → | 、 | U+FF64 | - | - | 読点 |
。 | U+3002 | → | 。 | U+FF61 | - | - | 句点 |
「 | U+300C | → | 「 | U+FF62 | 「 | (U+FF62) | 始めかぎ括弧 |
」 | U+300D | → | 」 | U+FF63 | 」 | (U+FF63) | 終わりかぎ括弧 |
¢ | U+FFE0 | → | ¢ | U+00A2 | - | - | セント記号 |
£ | U+FFE1 | → | £ | U+00A3 | - | - | ポンド記号 |
¬ | U+FFE2 | → | ¬ | U+00AC | - | - | 否定 |
¦ | U+FFE4 | → | ¦ | U+00A6 | - | - | 破断線 |
- b2 列の ( ) は、EDI情報等でのみ使用可能な文字。
全角変換
日本語のデータを全角項目に格納するための変換です。使える文字集合に応じて、変換先を若干変えています。
変換範囲 | 説明 | 用途 | |
---|---|---|---|
c1 | JIS X 0208 (JIS97) | 漢字用7ビット符号で符号化可能。 | EDI の全角項目 |
c2 | Windows-31J | Windows-31J の2バイト文字。 | 全角項目 |
c3 | JIS X 0213 (JIS2004) | 漢字用8ビット符号で符号化可能。 | 全角項目 |
具体的な変換イメージは以下の通りです。
例えば、JIS X 0208 では、左右が曖昧な全角引用符が定義されていないため、右引用符へ変換しています。
(表は横にスクロールしてご覧ください)
元の文字 | c1 | c2 | c3 | 備考 | |||||
---|---|---|---|---|---|---|---|---|---|
文字 | Unicode | 文字 | Unicode | 文字 | Unicode | 文字 | Unicode | ||
(SP) | U+0020 | → | U+3000 | U+3000 | U+3000 | スペース | |||
1 | U+0031 | → | 1 | U+FF11 | 1 | U+FF11 | 1 | U+FF11 | 数字 |
A | U+0041 | → | A | U+FF21 | A | U+FF21 | A | U+FF21 | 英大文字 |
a | U+0061 | → | a | U+FF41 | a | U+FF41 | a | U+FF41 | 英小文字 |
, | U+002C | → | , | U+FF0C | , | U+FF0C | , | U+FF0C | コンマ |
. | U+002E | → | . | U+FF0E | . | U+FF0E | . | U+FF0E | ピリオド |
” | U+0022 | → | ” | U+201D | " | U+FF02 | " | U+FF02 | 二重引用符 |
’ | U+0027 | → | ’ | U+2019 | ' | U+FF07 | ' | U+FF07 | アポストロフィ |
( | U+0028 | → | ( | U+FF08 | ( | U+FF08 | ( | U+FF08 | 始め丸括弧 |
) | U+0029 | → | ) | U+FF09 | ) | U+FF09 | ) | U+FF09 | 終わり丸括弧 |
[ | U+005B | → | [ | U+FF3B | [ | U+FF3B | [ | U+FF3B | 始め角括弧 |
] | U+005D | → | ] | U+FF3D | ] | U+FF3D | ] | U+FF3D | 終わり角括弧 |
+ | U+002B | → | + | U+FF0B | + | U+FF0B | + | U+FF0B | プラス |
- | U+002D | → | − | U+2212 | - | U+FF0D | - | U+FF0D | ハイフン・マイナス |
* | U+002A | → | * | U+FF0A | * | U+FF0A | * | U+FF0A | アスタリスク |
/ | U+002F | → | / | U+FF0F | / | U+FF0F | / | U+FF0F | 斜線 |
\ | U+005C | → | \ | U+FF3C | \ | U+FF3C | \ | U+FF3C | 逆斜線 |
~ | U+007E | → | 〜 | U+301C | ~ | U+FF5E | ~ | U+FF5E | チルダ |
¥ | U+00A5 | → | ¥ | U+FFE5 | ¥ | U+FFE5 | ¥ | U+FFE5 | 円記号 |
‾ | U+203E | → |  ̄ | U+FFE3 |  ̄ | U+FFE3 |  ̄ | U+FFE3 | オーバーライン |
¯ | U+00AF | → |  ̄ | U+FFE3 |  ̄ | U+FFE3 | ¯ | (U+00AF) | マクロン |
ア | U+FF71 | → | ア | U+30A2 | ア | U+30A2 | ア | U+30A2 | 片仮名 |
ガ | U+FF76 U+FF9E | → | ガ | U+30AC | ガ | U+30AC | ガ | U+30AC | 片仮名 (濁音) |
カ゚ | U+FF76 U+FF9F | → | カ゜ | U+30AB U+309C | カ゜ | U+30AB U+309C | カ゚ | U+30AB U+309A | 片仮名 (結合文字列) |
パ | U+FF8A U+FF9F | → | パ | U+30D1 | パ | U+30D1 | パ | U+30D1 | 片仮名 (半濁音) |
キャ | U+FF77 U+FF6C | → | キャ | U+30AD U+30E3 | キャ | U+30AD U+30E3 | キャ | U+30AD U+30E3 | 片仮名 (拗音) |
ヲ | U+FF66 | → | ヲ | U+30F2 | ヲ | U+30F2 | ヲ | U+30F2 | 片仮名 (ヲ) |
ヴ | U+FF73 U+FF9E | → | ヴ | U+30F4 | ヴ | U+30F4 | ヴ | U+30F4 | 片仮名 (ヴ) |
゙ | U+FF9E | → | ゛ | U+309B | ゛ | U+309B | ゛ | U+309B | 濁点 |
゚ | U+FF9F | → | ゜ | U+309C | ゜ | U+309C | ゜ | U+309C | 半濁点 |
ー | U+FF70 | → | ー | U+30FC | ー | U+30FC | ー | U+30FC | 長音記号 |
・ | U+FF65 | → | ・ | U+30FB | ・ | U+30FB | ・ | U+30FB | 中点 |
、 | U+FF64 | → | 、 | U+3001 | 、 | U+3001 | 、 | U+3001 | 読点 |
。 | U+FF61 | → | 。 | U+3002 | 。 | U+3002 | 。 | U+3002 | 句点 |
「 | U+FF62 | → | 「 | U+300C | 「 | U+300C | 「 | U+300C | 始めかぎ括弧 |
」 | U+FF63 | → | 」 | U+300D | 」 | U+300D | 」 | U+300D | 終わりかぎ括弧 |
¢ | U+00A2 | → | ¢ | (U+00A2) | ¢ | U+FFE0 | ¢ | (U+00A2) | セント記号 |
£ | U+00A3 | → | £ | (U+00A3) | £ | U+FFE1 | £ | (U+00A3) | ポンド記号 |
¬ | U+00AC | → | ¬ | (U+00AC) | ¬ | U+FFE2 | ¬ | (U+00AC) | 否定 |
¦ | U+00A6 | → | - | - | ¦ | U+FFE4 | ¦ | (U+00A6) | 破断線 |
3-4. 異体字
異体字の変換も2パターン考えてみました。要件次第ですが、必要な情報が欠落する可能性があるので、通常のデータでの異体字の統合は慎重に行った方がよいと思います。
変換範囲 | 説明 | 用途 | |
---|---|---|---|
a1 | ・JIS X 0213 の包摂基準を適用する。 ・表外漢字字体表で簡易慣用字体に未掲載の文字は印刷標準字体へ変換する。 |
標準規格で包摂される異体字のみ変換。 | 通常のデータなど |
a2 | ・常用漢字、人名用漢字は新字体に統一する。 ・それ以外の表外漢字は印刷標準字体に統一する。 |
新字体・旧字体、印刷標準字体・簡易慣用字体は一方へ寄せる。 | 検索用インデックスなど |
具体的な変換イメージは以下の通りです。
例えば、JIS X 0213 には ISO/IEC 8859-1 の文字が収録されましたが、「µ (マイクロ)」だけ収録されておらず、「μ (ギリシャ文字のミュー)」に包摂されるため、a1 で変換してます。同じく、JIS X 0213 の13区に NEC特殊文字 が収録されましたが、「∑ (総和)」は収録されておらず、「Σ (ギリシャ文字のシグマ)」に包摂されるため、a1 で変換しています。
表外漢字字体表において、「鴎」は簡易慣用字体として掲載されているため a1 では変換しませんが、「騨」は掲載されていないため a1 で変換しています。「髙 (はしごだか)」は「高」に包摂されるため、a1 で変換しています。
元の文字 | a1 | a2 | 備考 | ||||
---|---|---|---|---|---|---|---|
文字 | Unicode | 文字 | Unicode | 文字 | Unicode | ||
µ | U+00B5 | → | μ | U+03BC | μ | U+03BC | 包摂 (利用不可) |
∑ | U+2211 | → | Σ | U+03A3 | Σ | U+03A3 | 包摂 (IBM拡張文字) |
亞 | U+4E9E | → | 亞 | (U+4E9E) | 亜 | U+4E9C | 常用漢字 |
堯 | U+582F | → | 堯 | (U+582F) | 尭 | U+5C2D | 人名用漢字 |
神 | U+FA19 | → | 神 | (U+FA19) | 神 | U+795E | CJK互換漢字 |
鴎 | U+9D0E | → | 鴎 | (U+9D0E) | 鷗 | U+9DD7 | 表外漢字字体表 |
騨 | U+9A28 | → | 驒 | U+9A52 | 驒 | U+9A52 | 表外漢字字体表 |
髙 | U+9AD9 | → | 高 | U+9AD8 | 高 | U+9AD8 | 包摂 (IBM拡張文字) |
3-5. 合成・分解、CJK互換漢字、異体字シーケンス
合成・分解は、分解側へ揃えた方が統一感はありますが、文字数の節約、エンコード/デコードの対応状況から、合成可能な結合文字列は合成側へ寄せます。
ソフトウェアの異体字シーケンス対応状況がまだまだなので、通常のデータでは当面「CJK互換漢字」を使おうと思います。検索インデックスでは正規化し、異体字セレクタは除去します。
変換範囲 | 説明 | 用途 | |
---|---|---|---|
共通 | ・合成可能な結合文字列は合成する。 | 文字数の節約、エンコード/デコードの対応状況から。 | |
a1 | ・CJK互換漢字に対応する異体字シーケンスはCJK互換漢字へ変換する。 | ソフトウェアの異体字セレクタ対応状況が低いため。 | 通常のデータなど |
a2 | ・CJK互換漢字は正規化する。 ・異体字セレクタは除去する。 |
読み方、使い方、意味が等しい異体字は一方へ寄せる。 | 検索用インデックスなど |
具体的な変換イメージは以下の通りです。
元の文字 | a1 | a2 | 備考 | ||||
---|---|---|---|---|---|---|---|
文字 | Unicode | 文字 | Unicode | 文字 | Unicode | ||
ガ | U+30AB U+3099 | → | ガ | U+30AC | ガ | U+30AC | 片仮名 (結合文字列) |
神 | U+FA19 | → | 神 | (U+FA19) | 神 | U+795E | CJK互換漢字 |
(神︀) | U+795E U+FE00 | → | 神 | U+FA19 | 神 | U+795E | 異体字シーケンス |
3-6. Unicode 正規化
前回および 1-5 で説明したように、Unicode を単純に正規化すると、意図しない変換まで行われてしまいます。これを回避するために、文字列全体を正規化せずに、正規化可能な部分だけ抽出して正規化をかけます。
正規化の対象から指定された文字を除外する
以下の例では、変換したくない文字を指定し、それを除外して正規化 (合成・分解) を行っています。
// 正規化の対象から除く文字のパターン。 static final Pattern EXCLUSION_PATTERN = Pattern.compile( // アキュート付きα、ε、オングストローム、CJK互換漢字を除く。 "[^\\u1F71\\u1F73\\u212B\\uF900-\\uFAFF]+"); static void testNormalize2() { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; System.out.println("Text:"); System.out.println(toHexString(text)); System.out.println(text); String mnfd = replaceAll(text, EXCLUSION_PATTERN, s -> Normalizer.normalize(s, Form.NFD)); System.out.println("Modified NFD:"); System.out.println(toHexString(mnfd)); System.out.println(mnfd); String mnfc = replaceAll(text, EXCLUSION_PATTERN, s -> Normalizer.normalize(s, Form.NFC)); System.out.println("Modified NFC:"); System.out.println(toHexString(mnfc)); System.out.println(mnfc); } /** * 文字列のパターンにマッチした部分を関数の結果で置き換えます。 * * @param s 文字列 * @param p 正規表現のパターン * @param f 変換関数 * @return 変換後の文字列 */ static String replaceAll(CharSequence s, Pattern p, Function<String, String> f) { if (s == null || "".equals(s)) { return (String) s; } return p.matcher(s).replaceAll(r -> f.apply(r.group())); }
実行結果:
Text: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AC 4E9C 00A5 FFE5 FFE3 212B 2460 5F45 000D 000A D842 DF9F D84D DD94 FA19 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å①彅 𠮟𣖔神神︀カ゚˩˥ Modified NFD: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AB 3099 4E9C 00A5 FFE5 FFE3 212B 2460 5F45 000D 000A D842 DF9F D84D DD94 FA19 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å①彅 ← 「ガ」が分解 𠮟𣖔神神︀カ゚˩˥ ← 「Å」「神」の変換なし ○ Modified NFC: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AC 4E9C 00A5 FFE5 FFE3 212B 2460 5F45 000D 000A D842 DF9F D84D DD94 FA19 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å①彅 𠮟𣖔神神︀カ゚˩˥ ← 「Å」「神」の変換なし ○
正規化の対象を指定された文字に限定する
以下の例では、変換対象の文字を指定して、その部分にだけ正規化 (合成・分解、半角・全角変換) を行っています。
// 正規化の対象に含める文字のパターン。 static final Pattern INCLUSION_PATTERN = Pattern.compile( // 平仮名・片仮名。 "[\\u3041-\\u3096\\u3099\\u309A\\u309D\\u309E\\u30A1-\\u30FA\\u30FD\\u30FE" // 全角英数字 (記号は含まない)、半角片仮名。 + "\\uFF10-\\uFF19\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF61-\\uFF9F]+"); static void testNormalize3() { String text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\uD842\uDF9F\uD84D\uDD94神神\uFE00カ\u309A\u02E9\u02E5"; System.out.println("Text:"); System.out.println(toHexString(text)); System.out.println(text); String mnfkd = replaceAll(text, INCLUSION_PATTERN, s -> Normalizer.normalize(s, Form.NFKD)); System.out.println("Modified NFKD:"); System.out.println(toHexString(mnfkd)); System.out.println(mnfkd); String mnfkc = replaceAll(text, INCLUSION_PATTERN, s -> Normalizer.normalize(s, Form.NFKC)); System.out.println("Modified NFKC:"); System.out.println(toHexString(mnfkc)); System.out.println(mnfkc); }
実行結果:
Text: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AC 4E9C 00A5 FFE5 FFE3 212B 2460 5F45 000D 000A D842 DF9F D84D DD94 FA19 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å①彅 𠮟𣖔神神︀カ゚˩˥ Modified NFKD: 0041 0061 0031 30A2 30AB 3099 0041 0031 30A2 30AB 3099 4E9C 00A5 FFE5 FFE3 212B 2460 5F45 000D 000A D842 DF9F D84D DD94 FA19 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å①彅 ← 「ガ」が分解、半角片仮名、全角英数字が正規化 𠮟𣖔神神︀カ゚˩˥ ← 「Å」「神」の変換なし ○ Modified NFKC: 0041 0061 0031 30A2 30AC 0041 0031 30A2 30AC 4E9C 00A5 FFE5 FFE3 212B 2460 5F45 000D 000A D842 DF9F D84D DD94 FA19 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å①彅 𠮟𣖔神神︀カ゚˩˥ ← 「Å」「神」の変換なし ○
3-7. 国際化の考慮不足
最近は少なくなりましたが、国際化の考慮不足により ISO-8859-1 の決め打ちでデコードしている欧米のミドルウェアが以前はかなりありました。
この場合、ISO-8859-1 でエンコードして、いったんバイナリに戻してから、本来の符号化方式でデコードし直すと、期待する文字列が取得できます。
// 国際化の考慮不足で ISO-8859-1 でデコード。 String latin1 = ... System.out.println("ISO-8859-1:"); System.out.println(toHexString(latin1)); System.out.println(latin1); // いったんバイナリに戻してから、Windows-31J でデコード。 String w31j = new String(latin1.getBytes(StandardCharsets.ISO_8859_1), WINDOWS_31J); System.out.println("Windows-31J:"); System.out.println(toHexString(w31j)); System.out.println(w31j);
実行結果:
ISO-8859-1: 0041 0031 00B1 0083 0041 0087 0040 0088 009F 00FA 0067 A1±A@úg Windows-31J: 0041 0031 FF71 30A2 2460 4E9C 5F45 A1アア①亜彅
3-8. 改行
環境によって改行を表す文字が異なる問題については、どちらかに統一するか、あまり重要な問題でなければそのままでもよいかもしれません。
対策案 | |
---|---|
案1 | 入力時に LF に統一する。 |
案2 | 入力時に CR+LF に統一する。 |
案3 | 入力されたまま変換しない。 |
4. まとめと次回の予告
システム開発で必要となる標準規格の話、連載第2回は文字コードの実践編と題して、プログラミングでの扱い方、システム開発において特に注意すべき問題点と対策案を解説しました。
次回は、ローマ字を取り上げる予定です。訓令式、ヘボン式、日本式、いくつかあるローマ字の差異、かな・ローマ字変換を設計・実装する際のポイントなどを説明したいと思います。
A1. Java に関する補足
A1-1. サポートされているエンコーディング (日本語関連)
通常使用するのは、UTF-8、ISO-2022-JP、EUC-JP、Shift_JIS、Windows-31J くらいかと思いますが、改めて調べてみると意外と多くのエンコーディングが定義されていました。場合によっては使えるものがあるかもしれません。ただし、よく分からない仕様のもの (x-MS932_0213) や、ウェブで見つけられる仕様と異なるもの (x-windows-50220、x-windows-50221) があるので注意が必要です。
(表は横にスクロールしてご覧ください)
java.nio API用の正準名 | 別名 (抜粋) | 説明 | 補足 |
---|---|---|---|
UTF-8 | UTF8 | 8ビット Unicode (UCS) Transformation Format | |
CESU-8 | CESU8 | Unicode CESU-8 | サロゲートペアのまま UTF-8 に変換 |
UTF-16 | UTF16, Unicode | 16ビット Unicode (UCS) Transformation Format、オプションのバイト順マークによって識別されるバイト順 | |
UTF-16BE | 16ビット Unicode (UCS) Transformation Format、ビッグエンディアン・バイト順 | ||
UTF-16LE | 16ビット Unicode (UCS) Transformation Format、リトルエンディアン・バイト順 | ||
UTF-32 | UTF32 | 32ビット Unicode (UCS) Transformation Format、オプションのバイト順マークによって識別されるバイト順 | |
UTF-32BE | 32ビット Unicode (UCS) Transformation Format、ビッグエンディアン・バイト順 | ||
UTF-32LE | 32ビット Unicode (UCS) Transformation Format、リトルエンディアン・バイト順 | ||
US-ASCII | ASCII | American Standard Code for Information Interchange | |
ISO-8859-1 | ISO8859_1, latin1 | ISO-8859-1、ラテン・アルファベット No.1 | |
JIS_X0201 | JIS X 0201 | ||
ISO-2022-JP | ISO2022JP | ISO 2022 形式の JIS X 0201、0208、日本語 | 半角片仮名にも対応 (仕様外) |
ISO-2022-JP-2 | ISO2022JP2 | ISO 2022 形式の JIS X 0201、0208、0212、日本語 | 半角片仮名にも対応 (仕様外) |
EUC-JP | EUC_JP, eucjp | JISX 0201、0208、0212、EUCエンコーディング、日本語 | |
Shift_JIS | SJIS | Shift-JIS、日本語 | |
Windows-31J | MS932, csWindows31J | Windows 日本語 | |
x-JISAutoDetect | JISAutoDetect | Shift-JIS、EUC-JP、ISO 2022 JP の検出および変換 (Unicode への変換のみ) | |
x-SJIS_0213 | Shift_JISX0213 | Shift_JIS-2004 相当 | |
x-MS932_0213 | Shift_JISX0213 Windows MS932 拡張機能 | MS932 の隙間を埋めただけで、文字の採用に論理性がない | |
x-windows-iso2022jp | 拡張ISO-2022-JP (MS932ベース) | ISO-2022-JP の亜種、 NEC特殊文字、NEC選定IBM拡張文字に対応 |
|
x-windows-50220 | Cp50220 | Windows Codepage 50220 (7ビット実装) | ISO-2022-JP の亜種、 一般的な Cp50221 とマッピングが異なる |
x-windows-50221 | Cp50221 | Windows Codepage 50221 (7ビット実装) | ISO-2022-JP の亜種、 一般的な Cp50221 とマッピングが異なる |
x-euc-jp-linux | EUCJPLINUX | JISX 0201、0208、EUCエンコーディング、日本語 | EUC-JP の亜種 JIS X 0212 未対応版 |
x-eucJP-Open | EUCJPSolaris | JISX 0201、0208、0212、EUCエンコーディング、日本語 | EUC-JP の亜種 eucJP-ascii 相当と思われる |
x-IBM33722 | Cp33722 | IBM-eucJP - 日本語(5050のスーパー・セット) | EUC-JP の亜種 |
x-PCK | PCK | Solaris版のShift_JIS | Shift_JIS の亜種、 JIS 互換の Windows-31J |
x-IBM942 | Cp942 | IBM OS/2 日本語、Cp932 のスーパー・セット | Shift_JIS の亜種、 JIS C 6226-1978、IBM拡張文字 |
x-IBM942C | Cp942C | Cp942 の拡張機能 | Shift_JIS の亜種、5C 、7E の対応を ASCII に変更 |
x-IBM943 | Cp943 | IBM OS/2 日本語、Cp932 および Shift-JIS のスーパー・セット | Shift_JIS の亜種、 JIS X 0208-1990、NEC特殊文字、IBM拡張文字 |
x-IBM943C | Cp943C | Cp943の拡張機能 | Shift_JIS の亜種、5C 、7E の対応を ASCII に変更 |
IBM1047 | Cp1047 | ラテン文字-1 (EBCDICホスト用) | EBCDIC 英小文字拡張 |
IBM290 | Cp290 | IBM日本語カタカナ・ホスト拡張SBCS | EBCDIC カナ文字拡張 |
x-IBM300 | Cp300 | IBM日本語ラテン・ホスト(ダブルバイト) | EBCDIC IBM漢字 |
x-IBM930 | Cp930 | UDC 4370 文字を含む日本語カタカナ漢字、5026 のスーパー・セット | EBCDIC カナ文字拡張、IBM漢字 |
x-IBM939 | Cp939 | UDC 4370 文字を含む日本語ラテン文字漢字、5035 のスーパー・セット | EBCDIC 英小文字拡張、IBM漢字 |
A2. プログラミング言語での扱い (Ruby)
Java の String の実体は UTF-16 形式の Unicode でしたが、Ruby (1.9 以降) の String は文字コードが固定されておらず、String オブジェクトは自身の文字コード (エンコーディング) を保持しています。この方式を CSI (Code Set Independent) と呼びます。
A2-1. 文字列の走査、エンコード
ソースコードの1行目に # coding: utf-8
のように記述 (Magic Comment) すると、文字列のデフォルトのエンコーディング (Script Encoding) が指定できます。Magic Comment がない場合、Ruby 2.0 以降では UTF-8 が Script Encoding になります。
Java の例で示した、文字列の走査とエンコードの例を Ruby でも書いてみました。Ruby ではバイト単位、コードポイント単位で走査するメソッドが用意されており、正規表現で「書記素クラスタ」も使用できます。
文字列のエンコーディングが固定されていないので、エンコード/デコードの区別はなく、文字コードの変換は String#encode()
で行います。
# coding: utf-8 def char_name(s) if s == "\r\n" "CR+LF" else s end end def bytes_to_hex_string(a) a.bytes.map {|x| sprintf('%02X', x)}.join(' ') end def chars_to_hex_string(s) s.chars.map {|c| sprintf('%04X', c.ord)}.join(' ') end # # 正規表現を用いた文字列の走査。 # def test_regex puts '--------' puts '# test_regex' text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\u{20B9F}\u{23594}神神\uFE00カ\u309A\u02E9\u02E5" # 正規表現にマッチした単位で処理を行う。 text.scan(/\u02E9\u02E5|\u02E5\u02E9|\X/).each {|s| if s[0].ord < 0x20 printf("%s (%s)\n", chars_to_hex_string(s), char_name(s)) else printf("%s [%s]\n", chars_to_hex_string(s), s) end } end # # 各文字コードへの変換。 # def test_encoding puts '--------' puts '# test_encoding' # UTF-8 文字列。 text = "Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n" + "\u{20B9F}\u{23594}神神\uFE00カ\u309A\u02E9\u02E5" puts 'UTF-8:' puts bytes_to_hex_string(text) puts text # UTF-8 から Windows-31J に変換。 w31j = text.encode('Windows-31J', :undef => :replace) puts 'Windows-31J:' puts bytes_to_hex_string(w31j) puts w31j.encode('UTF-8') end test_regex test_encoding
実行結果:
-------- # test_regex 0041 [A] 0061 [a] 0031 [1] FF71 [ア] FF76 FF9E [ガ] ← 半角片仮名の濁音 FF21 [A] FF11 [1] 30A2 [ア] 30AC [ガ] 4E9C [亜] 00A5 [¥] FFE5 [¥] FFE3 [ ̄] 212B [Å] 2460 [①] 5F45 [彅] 000D 000A (CR+LF) ← 改行コード 20B9F [𠮟] ← 追加面 (サロゲートペア) 23594 [𣖔] FA19 [神] 795E FE00 [神︀] ← 異体字シーケンス 30AB 309A [カ゚] ← 結合文字列 02E9 02E5 [˩˥] ← これも結合文字列 -------- # test_encoding UTF-8: 41 61 31 EF BD B1 EF BD B6 EF BE 9E EF BC A1 EF BC 91 E3 82 A2 E3 82 AC E4 BA 9C C2 A5 EF BF A5 EF BF A3 E2 84 AB E2 91 A0 E5 BD 85 0D 0A F0 A0 AE 9F F0 A3 96 94 EF A8 99 E7 A5 9E EF B8 80 E3 82 AB E3 82 9A CB A9 CB A5 Aa1アガA1アガ亜¥¥ ̄Å①彅 𠮟𣖔神神︀カ゚˩˥ Windows-31J: 41 61 31 B1 B6 DE 82 60 82 50 83 41 83 4B 88 9F 3F 81 8F 81 50 81 F0 87 40 FA 67 0D 0A 3F 3F FB 7E 90 5F 3F 83 4A 3F 3F 3F Aa1アガA1アガ亜?¥ ̄Å①彅 ??神神?カ???
A2-2. サポートされているエンコーディング (日本語関連)
よく利用する UTF-8、ISO-2022-JP、EUC-JP、Shift_JIS、Windows-31J などが定義されています。それぞれの亜種がいくつか定義されていますが、詳細は未確認です。
エンコーディング名 | 別名 (抜粋) | 説明 | 補足 |
---|---|---|---|
ASCII-8BIT | BINARY | ASCII 互換オクテット列用のエンコーディング。単なるバイトの列を表現するために用いる。 | |
US-ASCII | ASCII | いわゆる ASCII のことで、ISO 646 IRV と一致。 | |
UTF-8 | CP65001 | Unicode や ISO 10646 を ASCII 互換な形で符号化するための方式。 | |
CESU-8 | |||
UTF-16 | UTF-16 (BOMを含む)。 | ||
UTF-16BE | UTF-16BE (ビッグエンディアン)。 | ||
UTF-16LE | UTF-16LE (リトルエンディアン)。 | ||
UTF-32 | UTF-32 (BOMを含む)。 | ||
UTF-32BE | UTF-32BE (ビッグエンディアン)。 | ||
UTF-32LE | UTF-32LE (リトルエンディアン)。 | ||
ISO-8859-1 | ISO8859-1 | 多くの西欧言語を含むさまざまなラテン文字言語を表現するための 8bitエンコーディング。 | |
ISO-2022-JP | ISO2022-JP | ISO 2022-JP エンコーディング。 | |
ISO-2022-JP-2 | ISO2022-JP2 | ISO-2022-JP の拡張版。 | |
EUC-JP | eucJP | 日本語 EUC 亜種。G0 が US-ASCII、G1 が JIS X 0201 片仮名図形文字集合、G2 が JIS X 0208、G3 が JIS X 0212。 | |
EUC-JIS-2004 | EUC-JISX0213 | ||
Shift_JIS | 基本的には JIS X 0208:1997 の付属書1にある「シフト符号化表現」のことだが、7bit 部分が US-ASCII になっている。 | ||
Windows-31J | CP932, csWindows31J, SJIS, PCK |
Windows で用いられる、シフトJIS 亜種。7bit 部分が論理的には US-ASCII であり、また Windows の機種依存文字を扱うことができる。 | SJIS は Shift_JIS ではなく Windows-31J なので注意。 |
CP50220 | Windows で用いられる、ISO-2022-JP 亜種。 | ||
CP50221 | Windows で用いられる、ISO-2022-JP 亜種。ESC ( I でいわゆる半角カナを許し、Windows の機種依存文字を扱うことができる。 | ||
ISO-2022-JP-KDDI | ISO-2022-JP の亜種。KDDI の携帯電話で使われる絵文字が含まれている。 | ||
stateless-ISO-2022-JP | ISO-2022-JP をステートレスに扱うための方式。Emacs-Mule エンコーディングを元にしている。 | ||
stateless-ISO-2022-JP-KDDI | stateless-ISO-2022-JP の亜種。KDDI の携帯電話で使われる絵文字が含まれている。 | ||
CP51932 | Windows で用いられる、日本語 EUC 亜種。G0 が US-ASCII、G1 が JIS X 0201 片仮名図形文字集合、G2 が JIS X 0208 + Windows の機種依存文字、G3 は未割り当て。 | ||
eucJP-ms | Unix 系で用いられる、日本語 EUC 亜種。 | ||
MacJapanese | Mac OS の 9.x までで用いられていた Shift_JIS 亜種。 | ||
SJIS-DoCoMo | Shift_JIS, CP932 の亜種。DoCoMo の携帯電話で使われる絵文字が含まれている。 | ||
SJIS-KDDI | Shift_JIS, CP932 の亜種。KDDI の携帯電話で使われる絵文字が含まれている。 | ||
SJIS-SoftBank | Shift_JIS, CP932 の亜種。SoftBank の携帯電話で使われる絵文字が含まれている。 | ||
UTF8-MAC | アップルによって修正された Normalization Form D (分解済み) 形式の UTF-8。 | ||
UTF8-DoCoMo | UTF-8 の亜種。DoCoMo の携帯電話で使われる絵文字が含まれている。 | ||
UTF8-KDDI | UTF-8 の亜種。KDDI の携帯電話で使われる絵文字が含まれている。 | ||
UTF8-SoftBank | UTF-8 の亜種。SoftBank の携帯電話で使われる絵文字が含まれている。 |
A3. プログラミング言語での扱い (Python)
Python (3.0 以降) の文字列 (str 型) は Unicode です。内部表現は環境やバージョンにより異なるようなので、抽象的なオブジェクトと理解しておくのがよいと思います。
A3-1. 文字列の走査、エンコード
ソースコードの1行目に # coding: utf-8
のように記述 (Magic Comment) すると、ソースコードのエンコーディング (Script Encoding) が指定できます。Magic Comment がない場合、UTF-8 が Script Encoding になります。
Java や Ruby の例で示した、文字列の走査とエンコードの例を Python で書いてみました。Pyton ではコードポイント単位で操作するメソッドが用意されています。正規表現は利用可能ですが、標準ライブラリでは「書記素クラスタ」はサポートしていないようです。
また、バイト単位に処理する場合は、str#encode()
メソッドでバイト列に変換する必要があります。バイト列を文字列に戻すには bytes#decode()
を使用します。エンコーディングを省略した場合は、どちらも UTF-8 になります。
import re def char_name(s): if s == '\r\n': return 'CR+LF' elif s == '\r': return 'CR' elif s == '\n': return 'LF' else: return s def bytes_to_hex_string(a): return ' '.join(['%02X' % x for x in a]) def chars_to_hex_string(s): return ' '.join(['%04X' % ord(c) for c in s]) # 論理的な文字を表す正規表現。 CHAR_REGEX = re.compile(r'\u02E9\u02E5|\u02E5\u02E9|\r\n|' \ # 結合文字、半角濁点・半濁点。 r'.[\u0300-\u036F\3099\309A\uFF9E\uFF9F' \ # 異体字セレクタ。 r'\uFE00-\uFE0F\U000E0100-\U000E01EF]*') def test_regex(): """正規表現を用いた文字列の走査。 """ print('--------') print('# test_regex') text = 'Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n' \ '\U00020B9F\U00023594神神\uFE00カ\u309A\u02E9\u02E5' # 正規表現にマッチした単位で処理を行う。 for s in CHAR_REGEX.findall(text): if ord(s[0]) < 0x20: print('%s (%s)' % (chars_to_hex_string(s), char_name(s))) else: print('%s [%s]' % (chars_to_hex_string(s), s)) def test_encoding(): """Unicode から各文字コードへの変換。 """ print('--------') print('# test_encoding') # UTF-8 文字列。 text = 'Aa1アガA1アガ亜\u00A5¥ ̄Å①彅\r\n' \ '\U00020B9F\U00023594神神\uFE00カ\u309A\u02E9\u02E5' print('Unicode:') print(chars_to_hex_string(text)) print(text) # Unicode から UTF-8 に変換。 utf8 = text.encode() print('UTF-8:') print(bytes_to_hex_string(utf8)) print(utf8.decode()) # Unicode から Windows-31J に変換。 w31j = text.encode('CP932', 'replace') print('Windows-31J:') print(bytes_to_hex_string(w31j)) print(w31j.decode('CP932')) test_regex() test_encoding()
実行結果:
-------- # test_regex 0041 [A] 0061 [a] 0031 [1] FF71 [ア] FF76 FF9E [ガ] FF21 [A] FF11 [1] 30A2 [ア] 30AC [ガ] 4E9C [亜] 00A5 [¥] FFE5 [¥] FFE3 [ ̄] 212B [Å] 2460 [①] 5F45 [彅] 000D 000A (CR+LF) 20B9F [𠮟] 23594 [𣖔] FA19 [神] 795E FE00 [神︀] 30AB [カ] 309A [゚] 02E9 02E5 [˩˥] -------- # test_encoding Unicode: 0041 0061 0031 FF71 FF76 FF9E FF21 FF11 30A2 30AC 4E9C 00A5 FFE5 FFE3 212B 2460 5F45 000D 000A 20B9F 23594 FA19 795E FE00 30AB 309A 02E9 02E5 Aa1アガA1アガ亜¥¥ ̄Å①彅 𠮟𣖔神神︀カ゚˩˥ UTF-8: 41 61 31 EF BD B1 EF BD B6 EF BE 9E EF BC A1 EF BC 91 E3 82 A2 E3 82 AC E4 BA 9C C2 A5 EF BF A5 EF BF A3 E2 84 AB E2 91 A0 E5 BD 85 0D 0A F0 A0 AE 9F F0 A3 96 94 EF A8 99 E7 A5 9E EF B8 80 E3 82 AB E3 82 9A CB A9 CB A5 Aa1アガA1アガ亜¥¥ ̄Å①彅 𠮟𣖔神神︀カ゚˩˥ Windows-31J: 41 61 31 B1 B6 DE 82 60 82 50 83 41 83 4B 88 9F 3F 81 8F 81 50 81 F0 87 40 ED 4B 0D 0A 3F 3F EE 62 90 5F 3F 83 4A 3F 3F 3F Aa1アガA1アガ亜?¥ ̄Å①彅 ??神神?カ???
A3-2. サポートされているエンコーディング (日本語関連)
よく利用する UTF-8、ISO-2022-JP、EUC-JP、Shift_JIS などが定義されています。Windows-31J は CP932 と指定します。
エンコーディング名 | 別名 (抜粋) | 説明 | 補足 |
---|---|---|---|
utf-8 | utf8, cp65001 | ||
utf-8-sig | BOM 印付き UTF-8 | ||
utf-16 | utf16 | ||
utf-16-be | UTF-16BE | ||
utf-16-le | UTF-16LE | ||
utf-32 | utf32 | ||
utf-32-be | UTF-32BE | ||
utf-32-le | UTF-32LE | ||
ascii | us-ascii | ||
latin-1 | latin1, iso8859-1, iso-8859-1 | ||
iso2022-jp | iso2022jp, iso-2022-jp | ||
iso2022-jp-1 | iso2022jp-1, iso-2022-jp-1 | ||
iso2022-jp-2 | iso2022jp-2, iso-2022-jp-2 | ||
iso2022-jp-3 | iso2022jp-3, iso-2022-jp-3 | JIS X 0213:2000 | |
iso2022-jp-2004 | iso2022jp-2004, iso-2022-jp-2004 | JIS X 0213:2004 | |
iso2022-jp-ext | iso2022jp-ext, iso-2022-jp-ext | ||
euc-jp | eucjp, ujis | ||
euc-jisx0213 | eucjisx0213 | JIS X 0213:2000 | |
euc-jis-2004 | eucjis2004 | JIS X 0213:2004 | |
shift_jis | sjis | ||
shift_jisx0213 | sjisx0213 | JIS X 0213:2000 | |
shift_jis-2004 | shiftjis2004 | JIS X 0213:2004 | |
cp932 | ms932, mskanji | Windows-31J は指定不可 |
A4. 参考文献・URL
- 矢野啓介『プログラマのための文字コード技術入門』(技術評論社)
https://gihyo.jp/book/2019/978-4-297-10291-3 - JIS X 0201 - 日本産業標準調査会
https://www.jisc.go.jp/app/jis/general/GnrJISNumberNameSearchList?show&jisStdNo=X0201 - JIS X 0208 - 日本産業標準調査会
https://www.jisc.go.jp/app/jis/general/GnrJISNumberNameSearchList?show&jisStdNo=X0208 - JIS X 0213 - 日本産業標準調査会
https://www.jisc.go.jp/app/jis/general/GnrJISNumberNameSearchList?show&jisStdNo=X0213 - Publicly Available Standards (ISO/IEC 10646 がダウンロード可能)
https://standards.iso.org/ittf/PubliclyAvailableStandards/index.html - Unicode.org
https://home.unicode.org/ - Unicode Terminology English - Japanese
https://www.unicode.org/terminology/term_en_ja.html - Unicode.org 公開資料
https://unicode.org/Public/ - Character Sets - IANA
https://www.iana.org/assignments/character-sets/character-sets.xhtml - 文字コード - Wikipedia
https://ja.wikipedia.org/wiki/%E6%96%87%E5%AD%97%E3%82%B3%E3%83%BC%E3%83%89 - Microsoftコードページ932 - Wikipedia
https://ja.wikipedia.org/wiki/Microsoft%E3%82%B3%E3%83%BC%E3%83%89%E3%83%9A%E3%83%BC%E3%82%B8932 - Unicode - Wikipedia
https://ja.wikipedia.org/wiki/Unicode - 異体字セレクタ - Wikipedia
https://ja.wikipedia.org/wiki/%E7%95%B0%E4%BD%93%E5%AD%97%E3%82%BB%E3%83%AC%E3%82%AF%E3%82%BF - 参考資料 - CyberLibrarian 図書館員のコンピュータ基礎講座
http://www.asahi-net.or.jp/~ax2s-kmtn/ref/index.html - 文字情報基盤整備事業
https://moji.or.jp/mojikiban/ - 安岡孝一の著作一覧
http://kanji.zinbun.kyoto-u.ac.jp/~yasuoka/publications.html - yasuokaの日記: WAVE DASH問題縁起
https://srad.jp/~yasuoka/journal/357074 - Unicode正規化 用語の混乱について 第4.2版 - ものかの
https://tama-san.com/unicode-normalize-confusion/ - 使いこなそうユニコード
http://nomenclator.la.coocan.jp/unicode/index.htm - 国際化の概要 - Oracle Java Documentation
https://docs.oracle.com/javase/jp/8/docs/technotes/guides/intl/overview.html - サポートされているエンコーディング - Oracle Java Documentation
https://docs.oracle.com/javase/jp/8/docs/technotes/guides/intl/encoding.doc.html - 多言語化 - Ruby 3.0.0 リファレンスマニュアル
https://docs.ruby-lang.org/ja/3.0.0/doc/spec=2fm17n.html - Ruby M17N の設計と実装 - Rubyist Magazine
https://magazine.rubyist.net/articles/0025/0025-Ruby19_m17n.html - Unicode HOWTO - Python 3 ドキュメント
https://docs.python.org/ja/3/howto/unicode.html - 標準エンコーディング - Python 3 ドキュメント
https://docs.python.org/ja/3/library/codecs.html#standard-encodings - HULFT8 コード変換 マニュアル
https://www.hulft.com/help/ja-jp/HULFT-V8/COM-CNV/Content/HULFT_CNV/preface.htm - HTML Living Standard
https://html.spec.whatwg.org/multipage/semantics.html#charset - Webサイトの文字コーディング、90%がUTF-8利用 - Shift JISは0.9% - TECH+
https://news.mynavi.jp/article/20171013-a216/