はじめに
#FF0000 というカラーコード、0xE38182 というバイト列、%E3%81%82 というURLエンコード。これらに共通する「16進数(hex)」は、Web開発・組み込み・暗号・文字コードと、エンジニアの仕事のあらゆる場所に顔を出します。
ただ、なんとなく使えてはいるものの、
- 「なぜカラーコードは2ケタずつ区切られているのか」
- 「なぜ文字化けが起きるのか」
- 「
escapeとencodeURIComponentは何が違うのか」
を人に説明できるかと聞かれると、意外と詰まる人も多いはずです。
本記事では、16進数の基本から、文字コード・URLエンコード・カラーコードまで横断的に整理し、 「とりあえず動く」から「説明できる」 にステップアップすることを目指します。
想定読者
- 「
#FF0000とか0xE38182って何?」をふんわりわかってる人 - バイナリ・文字コード・URLエンコードで一度はハマったことがある人
- 「なんとなく動いてるけど、仕組みは説明できない」状態を脱したい人
1. 前提知識「そもそも16進数(hex)とは?」
数の数え方は「何個でケタが上がるか」で決まる
- 10進数 ... 0〜9 の10個でケタが上がる(普段使う数字)
- 2進数 ... 0, 1 の2個でケタが上がる(コンピュータ内部)
- 16進数 ... 0〜9 + A〜F の16個でケタが上がる ← これがhex
| 10進数 | 16進数 |
|---|---|
| 0 | 0 |
| 9 | 9 |
| 10 | A |
| 15 | F |
| 16 | 10 |
| 255 | FF |
A=10, B=11, …, F=15。1ケタで0〜15を表せます。
なぜ開発で16進数が便利なのか
コンピュータは内部で 0と1(ビット) を扱います。8ビット = 1バイトで、表せる数は0〜255の256通り。
ここで重要なのが、
1バイト(0〜255) は、ちょうど16進数2ケタ(00〜FF) で表せる
ということ。2進数で 11100011 と書くより、16進数で E3 と書く方が圧倒的に短く、人間にも読みやすい。だからメモリ・色・文字コードなど、 バイトを扱う場面ではhexが標準 なのです。
2進数の4bit(1ニブル)が16進数の1ケタに対応します。8bit/16bit/32bitといった長いビット列も、16進数なら短く表現できるのがポイントです1。
# や 0x の意味
10 と書かれていたら、10進数の「十」か16進数の「十六」か区別できません。そのため、 「これはhexですよ」と示す印 を付けます。
| 記法 | 例 | よく使われる場面 |
|---|---|---|
0x |
0xFF |
C / Java / JavaScript / Python |
# |
#FF0000 |
HTML / CSSの色指定 |
% |
%E3%81%82 |
URLエンコード |
$ / h
|
$FF / 0FFh
|
アセンブラ |
2. hexは「数」というより「バイトの並び」
ここから少し踏み込みます。
開発で出てくるhexは、計算するための「数値」というより、 バイトの並びを文字で書き表したもの と捉えると理解が早いです。
例えば「あ」は UTF-8 という規則で表すと3バイトで、それをhexで書くと
E3 81 82
このとき大事なのは、
hexからもとの文字に戻すには、「どの規則(エンコーディング)で並んでいるか」を知らないとダメ
hex文字列 ──分割──▶ バイト列 ──デコード(UTF-8など)──▶ 文字列
▲
ここで規則を間違えると文字化け
UTF-8だと思って読めば「あ」、Shift_JISだと思って読めばまったく別物。 hex単体には「意味」がなく、エンコーディングとセットで初めて文字になる のです。
3. ブラウザのコンソールで試す
一番手軽な実験場はブラウザの開発者ツールです。F12(Macは Cmd + Option + I)を押して「Console」タブを開いてください。
hex → 日本語
const hex = "E38182E38184E38186";
// 2文字ずつに分けて、それぞれを数値(バイト)に変換
const bytes = new Uint8Array(hex.match(/.{2}/g).map(b => parseInt(b, 16)));
// バイト列をUTF-8として文字に戻す
new TextDecoder("utf-8").decode(bytes);
// → "あいう"
Uint8Array は U nsigned int eger 8 -bit Array の略。Unit8Array のようにタイポすると is not defined エラーになります。(よくやってた)
日本語 → hex
const text = "あいう";
[...new TextEncoder().encode(text)]
.map(b => b.toString(16).padStart(2, "0").toUpperCase())
.join(" ");
// → "E3 81 82 E3 81 84 E3 81 86"
URLの %E3%81%82 を戻す
URLに含まれる日本語はパーセントエンコーディングという形式(hexの前に % を付けたもの)。これは専用関数があるので一発です。
decodeURIComponent("%E3%81%82%E3%81%84%E3%81%86");
// → "あいう"
encodeURIComponent("あいう");
// → "%E3%81%82%E3%81%84%E3%81%86"
Node.jsなどのインストール不要で、ブラウザだけで完結します。業務データを変換するときも、外部のオンラインツールに貼らずに済むので安全です。
参考 ... MDN『TextDecoder』 / MDN『encodeURIComponent()』
4. 初心者がハマりやすい落とし穴 5選
❶ Java の getBytes() は環境依存
"あ".getBytes(); // 環境次第でUTF-8だったりShift_JISだったり
"あ".getBytes(StandardCharsets.UTF_8); // ← 必ず文字コードを指定
「自分のPCでは動いたのに、本番(Linux)で文字化け」の典型パターン。文字列⇄バイト変換では必ずエンコーディングを明示しましょう。
参考 ... Oracle『String#getBytes()』
❷ JavaScript の escape / unescape は使わない
escape("あ"); // → "%u3042" ← 独自仕様、非推奨
encodeURIComponent("あ"); // → "%E3%81%82" ← 標準(UTF-8)
escape は古い関数で非推奨。URLエンコードは encodeURIComponent 一択です2。
❸ Python の open() も環境依存
"あ".encode().hex() # 'e38182' (encode()のデフォルトはUTF-8)
open("a.txt", "w").write("あ") # OS設定次第で挙動が変わる
open("a.txt", "w", encoding="utf-8") # ← 明示するのが安全
参考 ... Python公式『open()』
❹ + は空白?プラス記号?
URLの + には2つの解釈があります。
- フォーム送信(
application/x-www-form-urlencoded) → 空白 - URLパス・フラグメント → ただのプラス記号
URLSearchParams と decodeURIComponent で挙動が違うのはこれが原因。
new URLSearchParams("q=a+b").get("q"); // → "a b" (空白扱い)
decodeURIComponent("a+b"); // → "a+b" (そのまま)
参考 ... MDN『URLSearchParams』
❺ ハッシュの大文字小文字
SHA-256などのhexは A1B2... と a1b2... で同じ値です。比較する前に必ず大文字 or 小文字に揃えましょう。
hash1.toLowerCase() === hash2.toLowerCase();
5. 文字コードの補足
UTF-8 と UTF-16 でバイト数が違う
| 文字 | UTF-8(hex) | UTF-16(hex) |
|---|---|---|
| A |
41 (1バイト) |
00 41 (2バイト) |
| あ |
E3 81 82 (3バイト) |
30 42 (2バイト) |
| 𩸽 (ホッケ) |
F0 A9 B8 BD (4バイト) |
D8 67 DE 3D (4バイト) |
同じ文字でも、どの規則で書くかでバイト数が違う
絵文字や一部の漢字は「1文字 ≠ 1コード」
"𩸽".length; // 2 ← JavaScriptは内部UTF-16なので、4バイト文字は2カウント
[..."𩸽"].length; // 1 ← スプレッド構文ならコードポイント単位で1
家族絵文字(👨👩👧👦)のように複数のコードポイントが結合した「書記素クラスタ」は、スプレッド構文でも1にならないことがあります。「文字数を数える」処理を書くときは、何の単位で数えているかを意識しないとバグります。
参考 ... MDN『String.prototype.length』
BOM(バイトオーダーマーク)について
ファイル先頭に付くおまじないバイト ...
- UTF-8 ...
EF BB BF - UTF-16 BE ...
FE FF - UTF-16 LE ...
FF FE
CSVをExcelで開くと文字化けする問題は、UTF-8のCSVにBOMが付いていないことが原因のことが多いです。
6. URLエンコードの「微妙な仕様差」
encodeURIComponent は便利ですが、実は RFC 3986(URLの仕様書) と微妙に違う部分があります。
| 記号 | encodeURIComponent | RFC 3986 厳密 |
|---|---|---|
! ' ( ) * |
エンコードしない | エンコードすべき |
普段は気にしなくてOKですが、 OAuth 1.0 や AWS の署名計算 ではここがズレると認証エラーになります。
そのときは厳密版を自作します。
const strictEncode = s => encodeURIComponent(s).replace(
/[!'()*]/g,
c => '%' + c.charCodeAt(0).toString(16).toUpperCase()
);
参考 ... RFC 3986 §2.3 / MDN『encodeURIComponent()』
7. カラーコード #FF0000 の意味
#RRGGBB は赤(R)・緑(G)・青(B)を、それぞれ16進数2ケタ(00〜FF = 0〜255)で表したもの。
-
#FF0000= R:255, G:0, B:0 → 赤 -
#00FF00= R:0, G:255, B:0 → 緑 -
#FFFFFF= 全部最大 → 白 -
#000000= 全部0 → 黒
CSSでは #RGB (3ケタ短縮) や #RRGGBBAA (アルファ値付き8ケタ)もサポートされています。#F09 は #FF0099 と同じ意味、#FF009980 は約50%の透明度になります3。
色を「単純に平均する」と知覚的にズレる
// NG ... 単純平均は人間の目には不自然な色になることがある
const avg = (a, b) => (a + b) / 2;
これは sRGB という色空間が、ガンマ補正された非線形な値だから。RGB値の中央(128, 128, 128)は、人間の目には「明るさの中間」には見えません。色を正確に混ぜたいときは、CSS の新機能 color-mix(in oklch, ...) のように知覚的に均等な色空間で計算するのが一番きれいです。
参考 ... MDN『color-mix()』
8. 知っておくと便利なツール
Chrome DevTools の Memory Inspector
Sources パネルでバイナリを右クリック → Reveal in Memory Inspector。hex / ASCII / 整数を切り替え表示でき、エンディアン(バイトの並び順)も切り替え可能です4。
コマンドライン xxd / hexdump
echo -n "あ" | xxd # 00000000: e381 82
echo -n "あ" | xxd -r -p # 逆変換 (hex→文字)
ファイルの中身をhexで覗くときに便利。
WebCryptoでハッシュをhex化(よく使う定型)
const buf = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode("hello")
);
[...new Uint8Array(buf)]
.map(b => b.toString(16).padStart(2, "0"))
.join("");
// → "2cf24dba5fb0a30e..."
参考 ... MDN『SubtleCrypto.digest()』
まとめ
- hexは「数」というより「バイトの並びを書き表す形式」と捉えると理解が早い
- 文字に戻すには、エンコーディング(UTF-8など)が必須
- ブラウザのコンソール +
TextDecoder/TextEncoder/decodeURIComponentで、インストール不要で安全に変換できる -
getBytes()/open()/escapeなど、環境依存・非推奨APIには要注意 - カラーコードもURLエンコードも文字コードも、根っこは同じ「バイトをhexで書いている」だけ
地味だけど、文字コード・URL・暗号・色…と幅広い領域に顔を出すのが16進数。 「とりあえず動く」から「説明できる」 に進むと、デバッグ速度が一段上がります。
参考サイト
UTF-8の符号化アルゴリズムを知りたい
URLエンコードの言語間差異を知りたい
文字コードとバイト数を体系的に整理したい
- 文字コードとバイト数を完全整理|UTF-8・Shift-JIS・半角カナの落とし穴まで - Qiita
- カオス過ぎる Unicode, UTF-8, UTF-16, UTF-32 の違い概要まとめ - Qiita