文字エンコーディングのガイド
1. 概要
このチュートリアルでは、文字エンコードの基本と、Javaでの処理方法について説明します。
2. 文字エンコーディングの重要性
多くの場合、ラテン語やアラビア語などのさまざまな記述スクリプトを使用して、複数の言語に属するテキストを処理する必要があります。 すべての言語のすべての文字は、何らかの形で1と0のセットにマッピングされる必要があります。 本当に、コンピューターが私たちのすべての言語を正しく処理できるのは不思議です。
これを適切に行うために、we need to think about character encoding.を行わないと、データの損失やセキュリティの脆弱性につながることがよくあります。
これをよりよく理解するために、Javaでテキストをデコードするメソッドを定義しましょう。
String decodeText(String input, String encoding) throws IOException {
return
new BufferedReader(
new InputStreamReader(
new ByteArrayInputStream(input.getBytes()),
Charset.forName(encoding)))
.readLine();
}
ここで入力する入力テキストは、デフォルトのプラットフォームエンコーディングを使用していることに注意してください。
If we run this method with input as “The façade pattern is a software design pattern.” and encoding as “US-ASCII”、出力:
The fa��ade pattern is a software design pattern.
まあ、私たちが期待していた通りではありません。
何が間違っていたのでしょうか。 このチュートリアルの残りの部分では、これを理解して修正しようとします。
3. 基礎
ただし、深く掘り下げる前に、encoding、charsets、およびcode pointの3つの用語を簡単に確認しましょう。
3.1. エンコーディング
コンピューターは、1や0のようなバイナリ表現しか理解できません。 それ以外のものを処理するには、実世界のテキストからそのバイナリ表現への何らかのマッピングが必要です。 This mapping is what we know as character encoding or simply just as encoding。
たとえば、US-ASCIIencodes to「01010100」のメッセージの最初の文字「T」。
3.2. 文字セット
文字のバイナリ表現へのマッピングは、含まれる文字の点で大きく異なります。 マッピングに含まれる文字の数は、実際に使用される少数の文字からすべての文字までさまざまです。 The set of characters that are included in a mapping definition is formally called a charset。
3.3.コードポイント
コードポイントは、文字を実際のエンコードから分離する抽象化です。 A code point is an integer reference to a particular character.
整数自体を、単純な10進数または16進数や8進数などの代替基数で表すことができます。 多数の参照を容易にするために、代替ベースを使用します。
たとえば、UnicodeのメッセージTの最初の文字には、コードポイント「U + 0054」(または10進数で84)があります。
4. エンコーディングスキームを理解する
文字エンコードは、エンコードする文字の数に応じてさまざまな形式をとることができます。
エンコードされた文字数は、各表現の長さと直接的な関係があり、通常はバイト数として測定されます。 Having more characters to encode essentially means needing lengthier binary representations.
今日実際に行われている一般的なエンコーディングスキームのいくつかを見ていきましょう。
4.1. シングルバイトエンコーディング
ASCII(情報交換用の米国標準コード)と呼ばれる最も初期のエンコード方式の1つは、シングルバイトのエンコード方式を使用します。 これは基本的に、each character in ASCII is represented with seven-bit binary numbers.を意味します。これにより、すべてのバイトで1ビットが空いたままになります。
ASCIIの128文字セットは、小文字と大文字の英語のアルファベット、数字、および一部の特殊文字と制御文字をカバーしています。
特定のエンコード方式で文字のバイナリ表現を表示するための簡単なメソッドをJavaで定義しましょう。
String convertToBinary(String input, String encoding)
throws UnsupportedEncodingException {
byte[] encoded_input = Charset.forName(encoding)
.encode(input)
.array();
return IntStream.range(0, encoded_input.length)
.map(i -> encoded_input[i])
.mapToObj(e -> Integer.toBinaryString(e ^ 255))
.map(e -> String.format("%1$" + Byte.SIZE + "s", e).replace(" ", "0"))
.collect(Collectors.joining(" "));
}
現在、文字「T」のコードポイントはUS-ASCIIで84です(JavaではASCIIはUS-ASCIIと呼ばれます)。
また、ユーティリティメソッドを使用すると、そのバイナリ表現を確認できます。
assertEquals(convertToBinary("T", "US-ASCII"), "01010100");
これは、予想どおり、文字「T」の7ビットバイナリ表現です。
The original ASCII left the most significant bit of every byte unused.同時に、ASCIIは、特に英語以外の言語の場合、表現されていない文字をかなり多く残していました。
これにより、その未使用ビットを活用し、128文字を追加する努力が必要になりました。
There were several variations of the ASCII encoding scheme proposed and adopted over the time.これらは大まかに「ASCII拡張」と呼ばれるようになりました。
ASCII拡張機能の多くは異なるレベルの成功を収めましたが、明らかに、多くの文字がまだ表現されていないため、これを広く採用するには十分ではありませんでした。
One of the more popular ASCII extensions was ISO-8859-1、「ISOラテン1」とも呼ばれます。
4.2. マルチバイトエンコーディング
ますます多くの文字に対応する必要性が高まるにつれて、ASCIIのようなシングルバイトエンコーディングスキームは持続不可能でした。
これにより、スペース要件が増加しますが、はるかに優れた容量を持つマルチバイトエンコーディングスキームが生まれました。
BIG5とSHIFT-JISは、multi-byte character encoding schemes which started to use one as well as two bytes to represent wider charsetsの例です。 これらのほとんどは、文字数が非常に多い中国語や類似のスクリプトを表す必要があるために作成されました。
ここで、メソッドconvertToBinaryを呼び出し、inputを「語」、漢字、encodingを「Big5」とします。
assertEquals(convertToBinary("語", "Big5"), "10111011 01111001");
上記の出力は、Big5エンコーディングが2バイトを使用して文字を表すことを示しています。
文字エンコーディングのcomprehensive listとそのエイリアスは、International NumberAuthorityによって管理されています。
5. Unicode
エンコードは重要ですが、表現を理解するにはデコードも同様に重要であることを理解することは難しくありません。 This is only possible in practice if a consistent or compatible encoding scheme is used widely.
別々に開発され、地域で実践されているさまざまなエンコーディングスキームが困難になり始めました。
この課題はa singular encoding standard called Unicode which has the capacity for every possible character in the worldを引き起こしました。 これには、使用中のキャラクターや、機能していないキャラクターも含まれます!
さて、それは各文字を保存するために数バイトを必要としますか? 正直なところ、Unicodeには独創的なソリューションがあります。
Unicode as a standard defines code points for every possible character in the world.Unicodeの文字「T」のコードポイントは10進数で84です。 通常、Unicodeでこれを「U + 0054」と呼びます。これは、U +の後に16進数が続くだけです。
ユニコードには1,114,112ポイントあるため、Unicodeのコードポイントのベースとして16進数を使用します。
How these code points are encoded into bits is left to specific encoding schemes within Unicode.これらのエンコード方式のいくつかについては、以下のサブセクションで説明します。
5.1. UTF-32
UTF-32は、Unicodeで定義されたan encoding scheme for Unicode that employs four bytes to represent every code pointです。 明らかに、すべての文字に4バイトを使用することはスペース効率が悪いです。
「T」のような単純な文字がUTF-32でどのように表されるかを見てみましょう。 前に紹介したメソッドconvertToBinaryを使用します。
assertEquals(convertToBinary("T", "UTF-32"), "00000000 00000000 00000000 01010100");
上記の出力は、最初の3バイトが無駄なスペースである文字「T」を表す4バイトの使用法を示しています。
5.2. UTF-8
UTF-8はanother encoding scheme for Unicode which employs a variable length of bytes to encodeです。 通常、1バイトを使用して文字をエンコードしますが、必要に応じてより多くのバイトを使用できるため、スペースを節約できます。
入力を「T」、エンコードを「UTF-8」として、メソッドconvertToBinaryをもう一度呼び出します。
assertEquals(convertToBinary("T", "UTF-8"), "01010100");
出力は、1バイトのみを使用したASCIIとまったく同じです。 実際、UTF-8はASCIIと完全に下位互換性があります。
入力を「語」、エンコードを「UTF-8」として、メソッドconvertToBinaryをもう一度呼び出します。
assertEquals(convertToBinary("語", "UTF-8"), "11101000 10101010 10011110");
ここでわかるように、UTF-8は3バイトを使用して文字「語」を表します。 This is known as variable-width encoding.
UTF-8は、スペース効率が高いため、Webで使用される最も一般的なエンコーディングです。
6. Javaでのエンコーディングサポート
Javaは、さまざまなエンコーディングと相互の変換をサポートしています。 クラスCharsetは、Javaプラットフォームのすべての実装でサポートが義務付けられているset of standard encodingsを定義します。
これには、US-ASCII、ISO-8859-1、UTF-8、およびUTF-16が含まれます。 A particular implementation of Java may optionally support additional encodings。
Javaが処理する文字セットを選択する方法には、いくつかの微妙な点があります。 それらについて詳しく見ていきましょう。
6.1. デフォルトの文字セット
Javaプラットフォームは、the default charsetというプロパティに大きく依存しています。 The Java Virtual Machine (JVM) determines the default charset during start-up。
これは、JVMが実行されている基盤となるオペレーティングシステムのロケールと文字セットに依存します。 たとえば、MacOSでは、デフォルトの文字セットはUTF-8です。
デフォルトの文字セットを決定する方法を見てみましょう。
Charset.defaultCharset().displayName();
このコードスニペットをWindowsマシンで実行すると、出力は次のようになります。
windows-1252
現在、「windows-1252」は英語のWindowsプラットフォームのデフォルトの文字セットであり、この場合、Windowsで実行されているJVMのデフォルトの文字セットを決定します。
6.2. デフォルトの文字セットを使用するのは誰ですか?
Java APIの多くは、JVMによって決定されるデフォルトの文字セットを使用します。 いくつか例を挙げると:
-
InputStreamReaderおよびFileReader
-
OutputStreamWriterおよびFileWriter
-
FormatterおよびScanner
つまり、文字セットを指定せずに例を実行すると、次のようになります。
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();
次に、デフォルトの文字セットを使用してデコードします。
また、デフォルトでこれと同じ選択を行うAPIがいくつかあります。
したがって、デフォルトの文字セットは、安全に無視できない重要性を前提としています。
6.3. デフォルトの文字セットに関する問題
これまで見てきたように、Javaのデフォルトの文字セットは、JVMの起動時に動的に決定されます。 これにより、異なるオペレーティングシステムで使用した場合、プラットフォームの信頼性が低くなり、エラーが発生しやすくなります。
たとえば、実行する場合
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();
macOSでは、UTF-8を使用します。
Windowsで同じスニペットを試すと、Windows-1252を使用して同じテキストをデコードします。
または、macOSでファイルを作成し、Windowsで同じファイルを読み取ることを想像してください。
エンコード方式が異なるため、データの損失や破損につながる可能性があることを理解するのは難しくありません。
6.4. デフォルトの文字セットをオーバーライドできますか?
Javaでデフォルトの文字セットを決定すると、2つのシステムプロパティが得られます。
-
file.encoding:このシステムプロパティの値は、デフォルトの文字セットの名前です
-
sun.jnu.encoding:このシステムプロパティの値は、ファイルパスのエンコード/デコード時に使用される文字セットの名前です。
これで、コマンドライン引数を使用してこれらのシステムプロパティを上書きするのが直感的になりました。
-Dfile.encoding="UTF-8"
-Dsun.jnu.encoding="UTF-8"
ただし、これらのプロパティはJavaでは読み取り専用であることに注意することが重要です。 Their usage as above is not present in the documentation。 これらのシステムプロパティをオーバーライドすると、望ましい動作や予測可能な動作が得られない場合があります。
したがって、we should avoid overriding the default charset in Java。
6.5. Javaがこれを解決しないのはなぜですか?
ロケールとオペレーティングシステムの文字セットに基づく代わりに、Java Enhancement Proposal (JEP) which prescribes using “UTF-8” as the default charset in Javaがあります。
このJEPは現在ドラフト状態であり、(願わくば!)通過すると、前述の問題のほとんどが解決されます。
java.nio.file.Filesのような新しいAPIは、デフォルトの文字セットを使用しないことに注意してください。 これらのAPIのメソッドは、デフォルトの文字セットではなくUTF-8の文字セットで文字ストリームを読み書きします。
6.6. 私たちのプログラムでこの問題を解決する
通常はchoose to specify a charset when dealing with text instead of relying on the default settingsにする必要があります。 文字からバイトへの変換を処理するクラスで使用するエンコーディングを明示的に宣言できます。
幸いなことに、この例ではすでに文字セットを指定しています。 We just need to select the right one and let Java do the rest.
これで、「…」のようなアクセント記号付き文字がエンコーディングスキーマASCIIに存在しないため、それらを含むエンコーディングが必要であることに気付くはずです。 おそらく、UTF-8?
それを試してみましょう。ここで、メソッドdecodeText を、同じ入力で「UTF-8」としてエンコードして実行します。
The façade pattern is a software-design pattern.
ビンゴ! 今見たいと思っていた出力を見ることができます。
ここでは、InputStreamReaderのコンストラクターでニーズに最も適していると思われるエンコードを設定しました。 これは通常、Javaで文字とバイト変換を処理する最も安全な方法です。
同様に、OutputStreamWriterや他の多くのAPIは、コンストラクタを介したエンコードスキームの設定をサポートしています。
6.7. MalformedInputException
バイトシーケンスをデコードする場合、指定されたCharsetに対して正当でない場合や、正当な16ビットUnicodeではない場合があります。 つまり、指定されたバイトシーケンスには、指定されたCharsetにマッピングがありません。
入力シーケンスに不正な形式の入力がある場合、3つの事前定義された戦略(またはCodingErrorAction)があります。
-
IGNOREは不正な形式の文字を無視し、コーディング操作を再開します
-
REPLACEは、出力バッファー内の不正な形式の文字を置き換え、コーディング操作を再開します
-
REPORTはMalformedInputExceptionをスローします
CharsetDecoder is REPORT,のデフォルトのmalformedInputActionとInputStreamReaderのデフォルトデコーダーのデフォルトのmalformedInputActionはREPLACE.です
指定されたCharset、CodingErrorActionタイプ、およびデコードされる文字列を受け取るデコード関数を定義しましょう。
String decodeText(String input, Charset charset,
CodingErrorAction codingErrorAction) throws IOException {
CharsetDecoder charsetDecoder = charset.newDecoder();
charsetDecoder.onMalformedInput(codingErrorAction);
return new BufferedReader(
new InputStreamReader(
new ByteArrayInputStream(input.getBytes()), charsetDecoder)).readLine();
}
したがって、「ファサードパターンはソフトウェアデザインパターンです」とデコードすると、 US_ASCIIを使用すると、各戦略の出力は異なります。 まず、不正な文字をスキップするCodingErrorAction.IGNOREを使用します。
Assertions.assertEquals(
"The faade pattern is a software design pattern.",
CharacterEncodingExamples.decodeText(
"The façade pattern is a software design pattern.",
StandardCharsets.US_ASCII,
CodingErrorAction.IGNORE));
2番目のテストでは、不正な文字の代わりに�を挿入するCodingErrorAction.REPLACEを使用します。
Assertions.assertEquals(
"The fa��ade pattern is a software design pattern.",
CharacterEncodingExamples.decodeText(
"The façade pattern is a software design pattern.",
StandardCharsets.US_ASCII,
CodingErrorAction.REPLACE));
3番目のテストでは、CodingErrorAction.REPORTを使用します。これにより、MalformedInputException:がスローされます。
Assertions.assertThrows(
MalformedInputException.class,
() -> CharacterEncodingExamples.decodeText(
"The façade pattern is a software design pattern.",
StandardCharsets.US_ASCII,
CodingErrorAction.REPORT));
7. エンコーディングが重要なその他の場所
プログラミング中に文字エンコードを考慮する必要はありません。 テキストは、他の多くの場所で誤って終端する可能性があります。
most common cause of problems in these cases is the conversion of text from one encoding scheme to another。これにより、データが失われる可能性があります。
テキストのエンコードまたはデコード時に問題が発生する可能性のあるいくつかの場所を簡単に見ていきましょう。
7.1. テキストエディタ
ほとんどの場合、テキストエディターはテキストの生成元です。 vi、メモ帳、MS Wordなど、多くのテキストエディターが一般的に選択されています。 これらのテキストエディターのほとんどでは、エンコードスキームを選択できます。 したがって、それらが処理するテキストに適切であることを常に確認する必要があります。
7.2. ファイルシステム
エディターでテキストを作成した後、それらを何らかのファイルシステムに保存する必要があります。 ファイルシステムは、実行しているオペレーティングシステムによって異なります。 ほとんどのオペレーティングシステムは、複数のエンコードスキームをサポートしています。 ただし、エンコード変換によりデータが失われる場合があります。
7.3. ネットワーク
ファイル転送プロトコル(FTP)などのプロトコルを使用してネットワーク経由で転送されるテキストには、文字エンコード間の変換も含まれます。 Unicodeでエンコードされているものはすべて、変換で失われるリスクを最小限に抑えるために、バイナリとして転送するのが最も安全です。 ただし、ネットワークを介したテキストの転送は、データ破損の頻度が低い原因の1つです。
7.4. データベース
OracleやMySQLのような人気のあるデータベースのほとんどは、データベースのインストール時または作成時に文字エンコード方式の選択をサポートしています。 データベースに保存する予定のテキストに従って、これを選択する必要があります。 これは、エンコード変換によりテキストデータの破損が発生する頻度の高い場所の1つです。
7.5. ブラウザ
最後に、ほとんどのWebアプリケーションでは、ブラウザーのようなユーザーインターフェイスでテキストを表示するために、テキストを作成して異なるレイヤーに渡します。 ここでも、文字を適切に表示できる適切な文字エンコーディングを選択することが不可欠です。 ChromeやEdgeなどの最も人気のあるブラウザーでは、設定から文字エンコードを選択できます。
8. 結論
この記事では、プログラミング中にエンコードが問題になる可能性について説明しました。
エンコードと文字セットを含む基本についてさらに説明しました。 さらに、さまざまなエンコーディングスキームとその使用方法を検証しました。
また、Javaでの不適切な文字エンコードの使用例を取り上げ、それを正しく行う方法を確認しました。 最後に、文字エンコードに関連する他の一般的なエラーシナリオについて説明しました。
いつものように、例のコードはover on GitHubで利用できます。