バイト列を16進数の文字列にするには
文字列を UTF-8 のバイト列にして 16 進数の文字列へ変換し、元の文字列へ戻す方法を各言語で示す。
桁のそろえ方、大文字と小文字の違い、不正な入力を渡したときの挙動の差までを扱う。
公開:
選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。
Python
text = "Aあ\n"
data = text.encode("utf-8")
hex_str = data.hex()
print(hex_str) # 41e381820a
print(data.hex(" ")) # 41 e3 81 82 0a
print(bytes.fromhex(hex_str).decode("utf-8") == text) # True
print(format(10, "02x"), hex(10)) # 0a 0xa
try:
bytes.fromhex("zz")
except ValueError as e:
print(e) # non-hexadecimal number found in fromhex() arg at position 0bytes.hex() でバイト列を小文字の 16 進数文字列にでき、区切り文字を渡せば 41 e3 81 82 0a のように読みやすく整形できる。
戻すときは bytes.fromhex を使い、不正な文字を渡すと ValueError で止まるため、壊れた入力に気づける。
JavaScript
const text = "Aあ\n";
const data = Buffer.from(text, "utf8");
const hex = data.toString("hex");
console.log(hex); // 41e381820a
console.log(Buffer.from(hex, "hex").toString("utf8") === text); // true
// Buffer を使えないブラウザでは、1 バイトずつ 2 桁にそろえて連結する
const hex2 = Array.from(new TextEncoder().encode(text), (b) => b.toString(16).padStart(2, "0")).join("");
console.log(hex2 === hex); // true
console.log((10).toString(16)); // a(桁がそろわない)
console.log(Buffer.from("zz", "hex").length); // 0(例外にならず空になる)
console.log(Buffer.from("41e", "hex").length); // 1(端数の桁は黙って捨てられる)Node.js では Buffer の toString("hex") と Buffer.from(hex, "hex") で往復できる。
ブラウザには Buffer が無いので、toString(16) の結果を padStart(2, "0") で 2 桁にそろえて連結する。Buffer.from は不正な 16 進数を例外にせず、黙って空や短い結果にする点に注意する。
TypeScript
const text: string = "Aあ\n";
const data: Uint8Array = new TextEncoder().encode(text);
const hex: string = Array.from(data, (b) => b.toString(16).padStart(2, "0")).join("");
console.log(hex); // 41e381820a
const pairs: string[] = hex.match(/../g) ?? [];
const back: Uint8Array = Uint8Array.from(pairs, (h) => parseInt(h, 16));
console.log(new TextDecoder().decode(back) === text); // true
console.log((10).toString(16)); // a(padStart を付けないと桁がそろわない)
console.log(parseInt("zz", 16)); // NaN(例外にならない)
console.log(Uint8Array.from(["zz"], (h) => parseInt(h, 16))[0]); // 0(NaN は 0 のバイトになる)TextEncoder と toString(16) と padStart だけで書くので、Buffer に頼らずブラウザと Node.js の両方で動く。
2 桁ずつに分けて parseInt(h, 16) で戻せるが、不正な文字は NaN になり、Uint8Array に入れると 0 のバイトに化けるため、必要なら自分で検証する。
Go
package main
import (
"encoding/hex"
"fmt"
)
func main() {
text := "Aあ\n"
hexStr := hex.EncodeToString([]byte(text))
fmt.Println(hexStr) // 41e381820a
data, err := hex.DecodeString(hexStr)
if err != nil {
panic(err)
}
fmt.Println(string(data) == text) // true
_, err = hex.DecodeString("zz")
fmt.Println(err) // encoding/hex: invalid byte: U+007A 'z'
_, err = hex.DecodeString("41e")
fmt.Println(err) // encoding/hex: odd length hex string
}標準ライブラリの encoding/hex の EncodeToString と DecodeString で往復できる。DecodeString は不正な文字や奇数桁を error として返すので、戻り値の err を必ず確かめる。
Rust
use std::fmt::Write;
fn main() {
let text = "Aあ\n";
let mut hex = String::new();
for b in text.as_bytes() {
write!(hex, "{:02x}", b).unwrap();
}
println!("{}", hex); // 41e381820a
let data: Vec<u8> = (0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
.collect();
println!("{}", String::from_utf8(data).unwrap() == text); // true
println!("{:x} {:02x}", 10, 10); // a 0a
match u8::from_str_radix("zz", 16) {
Ok(_) => println!("ok"),
Err(e) => println!("{}", e), // invalid digit found in string
}
}標準ライブラリには 16 進数の変換関数が無いため、この例では format! と同じ書式指定の {:02x} で 1 バイトずつ 2 桁にそろえて連結した。
戻すときは 2 文字ずつ u8::from_str_radix(…, 16) で読み、不正な文字は Err になる。
Java
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
public class Main {
public static void main(String[] args) {
String text = "Aあ\n";
byte[] data = text.getBytes(StandardCharsets.UTF_8);
String hex = HexFormat.of().formatHex(data);
System.out.println(hex); // 41e381820a
System.out.println(HexFormat.of().withUpperCase().formatHex(data)); // 41E381820A
byte[] back = HexFormat.of().parseHex(hex);
System.out.println(new String(back, StandardCharsets.UTF_8).equals(text)); // true
try {
HexFormat.of().parseHex("zz");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // not a hexadecimal digit: "z" = 122
}
}
}HexFormat の formatHex と parseHex で往復できる。
出力は既定で小文字で、withUpperCase() を付けると大文字になる。
不正な文字は IllegalArgumentException になる。HexFormat は Java 17 以降で使える。
C#
using System;
using System.Text;
var text = "Aあ\n";
byte[] data = Encoding.UTF8.GetBytes(text);
string hex = Convert.ToHexString(data);
Console.WriteLine(hex); // 41E381820A(大文字で返る)
Console.WriteLine(hex.ToLowerInvariant()); // 41e381820a
byte[] back = Convert.FromHexString(hex);
Console.WriteLine(Encoding.UTF8.GetString(back) == text); // True
try { Convert.FromHexString("zz"); }
catch (FormatException e) { Console.WriteLine(e.GetType().Name); } // FormatException
try { Convert.FromHexString("41e"); }
catch (FormatException e) { Console.WriteLine(e.GetType().Name); } // FormatExceptionConvert.ToHexString と Convert.FromHexString で往復できる。ToHexString は大文字で返すため、小文字にしたいときは ToLowerInvariant を通す。
不正な文字や奇数桁は FormatException になる。
つまずき
16 進数の文字列は、バイト列を 1 バイトにつき 2 桁で書き並べたものである。
「Aあ」と改行を UTF-8 にすると 41 e3 81 82 0a の 5 バイトで、連結すると 41e381820a になった。
ここで 0a のように上の桁が 0 になるバイトを「a」と 1 桁で出力してしまうと、全体の桁が合わず、元に戻せなくなる。
数値を 16 進数にする toString(16) や hex() は桁をそろえてくれないので、2 桁にそろえる処理を自分で足す必要がある。
桁をそろえる書き方は言語で違う
同じ 10(0x0a)でも、素直に 16 進数にした結果は言語ごとに違った。
- Python:
hex(10)は0xa(接頭辞が付き、桁もそろわない)。format(10, "02x")で0aになる - JavaScript・TypeScript:
(10).toString(16)はa。padStart(2, "0")を足して0aにする - Rust:
{:x}はa、{:02x}で0a
Go の hex.EncodeToString、Java の HexFormat、C# の Convert.ToHexString、Python の bytes.hex() は最初から 2 桁ずつ出力するので、自分でそろえる必要がない。
バイト列全体を変換するときは、これらの専用の関数を使うほうが安全である。
大文字と小文字の違い
出力の大文字と小文字も言語で違った。41e381820a と小文字で返すのは Python・JavaScript・Go・Rust・Java で、C# の Convert.ToHexString だけは 41E381820A と大文字で返した。
16 進数の文字列を比較するときは、大文字と小文字をそろえてから比べる。
Java は withUpperCase() で大文字に切り替えられ、C# は ToLowerInvariant で小文字にそろえられる。
不正な入力を渡したとき
16 進数の文字列を戻すとき、zz のように 16 進数にならない文字を渡すと、言語ごとに挙動が分かれた。
- Python:
ValueError(non-hexadecimal number found in fromhex() arg at position 0) - Go:
errorを返す(encoding/hex: invalid byte: U+007A 'z'。桁が奇数のときはodd length hex string) - Rust:
from_str_radixがErr(invalid digit found in string) - Java:
IllegalArgumentException(not a hexadecimal digit: "z" = 122) - C#:
FormatException(桁が奇数のときも同じ) - JavaScript:
Buffer.from("zz", "hex")は例外にならず長さ 0 のバッファになり、41eのように桁が奇数なら端数のeを捨てて 1 バイトになった - TypeScript:
parseInt("zz", 16)はNaNで、Uint8Arrayに入れると 0 のバイトになった
黙って通す書き方では、壊れた入力に気づかないまま処理が進む。
外部から受け取った 16 進数の文字列は、長さが偶数か、16 進数の文字だけかを先に検証してから戻す。