文字列をUTF-8のバイト列にするには
文字列を UTF-8 のバイト列に変換し、元の文字列へ戻す方法を各言語で示す。
文字数とバイト数が一致しない理由と、壊れたバイト列を読んだときの挙動の違いまでを扱う。
公開:
選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。
Python
text = "あA😀"
data = text.encode("utf-8")
print(len(text), len(data)) # 3 8
print(data.hex(" ")) # e3 81 82 41 f0 9f 98 80
print(data.decode("utf-8")) # あA😀
broken = data[:2] # 「あ」の途中で切れたバイト列
try:
broken.decode("utf-8")
except UnicodeDecodeError as e:
print(e.reason) # unexpected end of data
print(broken.decode("utf-8", errors="replace")) # �文字列の encode("utf-8") でバイト列(bytes)になり、len は文字列なら文字数、バイト列ならバイト数を返す。
戻すときは decode("utf-8") を使う。
壊れたバイト列は既定で UnicodeDecodeError になり、errors="replace" を渡すと不正な部分が U+FFFD に置き換わる。
JavaScript
const text = "あA😀";
const data = new TextEncoder().encode(text);
console.log([...text].length, text.length, data.length); // 3 4 8
console.log(Array.from(data, (b) => b.toString(16).padStart(2, "0")).join(" ")); // e3 81 82 41 f0 9f 98 80
console.log(new TextDecoder().decode(data)); // あA😀
const broken = data.slice(0, 2); // 「あ」の途中で切れたバイト列
console.log(new TextDecoder().decode(broken)); // �
try {
new TextDecoder("utf-8", { fatal: true }).decode(broken);
} catch (e) {
console.log(e.name); // TypeError
}TextEncoder は常に UTF-8 でバイト列(Uint8Array)を作り、TextDecoder で戻せる。
文字列の length は UTF-16 の単位数なので、絵文字を含むと見た目の文字数より大きくなる。TextDecoder は壊れたバイト列を既定で U+FFFD に置き換えるが、fatal: true を渡すと TypeError で止まる。
TypeScript
const text: string = "あA😀";
const data: Uint8Array = new TextEncoder().encode(text);
console.log([...text].length, text.length, data.length); // 3 4 8
console.log(Array.from(data, (b) => b.toString(16).padStart(2, "0")).join(" ")); // e3 81 82 41 f0 9f 98 80
console.log(new TextDecoder().decode(data)); // あA😀
const broken = data.slice(0, 2); // 「あ」の途中で切れたバイト列
console.log(new TextDecoder().decode(broken)); // �
try {
new TextDecoder("utf-8", { fatal: true }).decode(broken);
} catch (e) {
console.log((e as Error).name); // TypeError
}JavaScript と同じ TextEncoder と TextDecoder を使い、戻り値は Uint8Array と型が付く。catch の例外は unknown として扱われるため、name を読むには Error に型を絞る。
ブラウザでも Node.js でも Buffer に頼らず同じコードで動く。
Go
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
text := "あA😀"
data := []byte(text)
fmt.Println(utf8.RuneCountInString(text), len(text)) // 3 8
fmt.Printf("% x\n", data) // e3 81 82 41 f0 9f 98 80
fmt.Println(string(data)) // あA😀
broken := data[:2] // 「あ」の途中で切れたバイト列
fmt.Println(utf8.Valid(broken)) // false
fmt.Println(string(broken) == "\xe3\x81") // true(置き換えは起きず、不正なバイトがそのまま残る)
fmt.Println([]rune(string(broken))[0] == utf8.RuneError) // true
}Go の文字列は元から UTF-8 のバイト列なので、[]byte(text) は変換というよりバイト列としての見方を変えるだけである。len が返すのはバイト数で、文字数は utf8.RuneCountInString で数える。
壊れたバイト列を string にしても例外にはならないため、utf8.Valid で自分で確かめる。
Rust
fn main() {
let text = "あA😀";
let data: &[u8] = text.as_bytes();
println!("{} {}", text.chars().count(), data.len()); // 3 8
let hex: Vec<String> = data.iter().map(|b| format!("{:02x}", b)).collect();
println!("{}", hex.join(" ")); // e3 81 82 41 f0 9f 98 80
println!("{}", std::str::from_utf8(data).unwrap()); // あA😀
let broken = &data[..2]; // 「あ」の途中で切れたバイト列
match std::str::from_utf8(broken) {
Ok(_) => println!("ok"),
Err(e) => println!("{}", e), // incomplete utf-8 byte sequence from index 0
}
println!("{}", String::from_utf8_lossy(broken)); // �
}Rust の str も UTF-8 で保持されており、as_bytes でそのままバイト列として借りられる。
バイト列から str に戻す std::str::from_utf8 は不正だと Err を返し、String::from_utf8_lossy なら U+FFFD に置き換えて通す。
文字数は chars().count() で数え、len はバイト数である。
Java
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
public class Main {
public static void main(String[] args) {
String text = "あA😀";
byte[] data = text.getBytes(StandardCharsets.UTF_8);
System.out.println(text.codePointCount(0, text.length()) + " " + text.length() + " " + data.length); // 3 4 8
System.out.println(HexFormat.ofDelimiter(" ").formatHex(data)); // e3 81 82 41 f0 9f 98 80
System.out.println(new String(data, StandardCharsets.UTF_8)); // あA😀
byte[] broken = java.util.Arrays.copyOf(data, 2); // 「あ」の途中で切れたバイト列
System.out.println(new String(broken, StandardCharsets.UTF_8)); // �
}
}getBytes(StandardCharsets.UTF_8) で文字コードを明示してバイト列にし、戻すときも同じ文字コードを new String に渡す。
引数なしの getBytes() は実行環境の既定の文字コードに結果が左右されるため避ける。String.length() は UTF-16 の単位数で、絵文字は 2 と数えられる。HexFormat は Java 17 以降で使える。
C#
using System;
using System.Linq;
using System.Text;
var text = "あA😀";
byte[] data = Encoding.UTF8.GetBytes(text);
Console.WriteLine($"{text.EnumerateRunes().Count()} {text.Length} {data.Length}"); // 3 4 8
Console.WriteLine(BitConverter.ToString(data).Replace('-', ' ').ToLower()); // e3 81 82 41 f0 9f 98 80
Console.WriteLine(Encoding.UTF8.GetString(data)); // あA😀
var broken = data[..2]; // 「あ」の途中で切れたバイト列
Console.WriteLine(Encoding.UTF8.GetString(broken)); // �
var strict = new UTF8Encoding(false, true);
try { strict.GetString(broken); }
catch (DecoderFallbackException e) { Console.WriteLine(e.GetType().Name); } // DecoderFallbackExceptionEncoding.UTF8.GetBytes でバイト列にし、Encoding.UTF8.GetString で戻す。string.Length は UTF-16 の単位数なので、絵文字を 1 文字と数えるには EnumerateRunes を使う。
既定のデコーダーは壊れたバイト列を U+FFFD に置き換え、例外にしたいときは new UTF8Encoding(false, true) を使う。
つまずき
文字列の長さとバイト数は別の値である。
「あA😀」は 3 文字だが、UTF-8 にすると「あ」が 3 バイト、「A」が 1 バイト、「😀」が 4 バイトの計 8 バイトになった。
ファイルやネットワークに書き出すときの長さ(Content-Length やバッファの大きさなど)にはバイト数を使い、文字数と混同しない。
長さの数え方は言語で違う
同じ「あA😀」でも、length の返す値は言語で変わった。
- Python の
len(text):3(コードポイントの数) - JavaScript・TypeScript の
text.length:4(UTF-16 の単位数。絵文字が 2 と数えられる) - Java の
text.length()・C# のtext.Length:4(同じく UTF-16 の単位数) - Go の
len(text)・Rust のtext.len():8(バイト数)
見た目どおりの 3 文字を得るには、JavaScript は [...text].length、Java は codePointCount、C# は EnumerateRunes、Go は utf8.RuneCountInString、Rust は chars().count() を使う。
壊れたバイト列を読んだとき
「あ」の 3 バイト(e3 81 82)のうち先頭の 2 バイトだけを UTF-8 として読むと、言語ごとに挙動が分かれた。
- Python:
UnicodeDecodeError(unexpected end of data) - Rust:
from_utf8がErr(incomplete utf-8 byte sequence from index 0) - JavaScript・TypeScript:既定では U+FFFD に置き換え、
fatal: trueでTypeError - Java:U+FFFD に置き換えて例外にならない
- C#:既定では U+FFFD に置き換え、
UTF8Encoding(false, true)でDecoderFallbackException - Go:例外も置き換えもなく、不正なバイトがそのまま残る(
utf8.Validは false)
置き換えて通す言語では、壊れたデータに気づかないまま処理が進む。
外部から受け取ったバイト列は、例外にするか検証するかを自分で選ぶ。
文字コードを取り違えると読めない
「あA」を UTF-8 にすると e3 81 82 41 の 4 バイトで、Shift_JIS(cp932)にすると 82 a0 41 の 3 バイトになった。
cp932 で作ったバイト列を Python で UTF-8 として読むと、先頭の 0x82 が不正な開始バイトとして UnicodeDecodeError になる。
逆に「😀」は cp932 で表せないため、encode("cp932") は UnicodeEncodeError(illegal multibyte sequence)になった。
受け渡す相手の文字コードを確かめ、迷ったら UTF-8 にそろえる。