🔠

文字コード入門

2024/06/11に公開

文字コードが何もわからないのでNode.js(TypeScript)を使って手を動かしながら入門するメモです。

文字コード全体の俯瞰としては

JSで文字コードを扱う方法は

がそれぞれわかりやすそうでした。上記資料を中心に読みながら進めていきます。

string.codePointAt

.codePointAt は該当文字のCode Pointを得ることができるメソッドです。

const s = "あ";
console.log(s.codePointAt(0)); // => 12354
console.log(s.codePointAt(0)?.toString(16)); // => 3042

例えば、 という文字列に .codePointAt(0) すると 12354 が出力され、 .codePointAt(0).toString(16) すると 3042 が出力されます。

Code Pointとは?

Unicodeにおいて、それぞれの文字について対応するID(例えば、 なら U+3042)のようなものが定められており、それをCode Point(符号位置)と呼びます。

文字列とUnicode · JavaScript Primer #jsprimer

Unicodeはすべての文字(制御文字などの画面に表示されない文字も含む)に対してIDを定義する目的で策定されている仕様です。 この「文字」に対する「一意のID」のことをCode Point(符号位置)と呼びます。

U+3042 は何の数字?

Unicodeにおいて、 には U+3042 のIDが対応します。

U+ って何?

Unicode符号位置の文脈で U+ をprefixにつけると、16進数であることを示します。

Unicode - Wikipedia

Unicode符号位置を文章中などに記す場合は "U+" の後に十六進法で符号位置を4桁から6桁続けることで表す。

つまり 3042 は16進法の値です。

12354 は何の数字?

3042 を10進数に変換すると 12354 になります。

言い換えると .toString(16) で16進数に変換する前の10進数の値が 12354 です。

String.fromCodePoint

String.fromCodePoint でCode Pointから文字列に逆変換できます。

const codePoint = 0x3042;
console.log(String.fromCodePoint(codePoint)); // => あ

0x3042 では が出力されます。

0x って何?

0x をprefixとすることで16進数であることを示します。

0xとは - 意味をわかりやすく - IT用語辞典 e-Words

C言語では先頭が「0x」という接頭辞(プレフィクス)から始まる数値表記は16進数を表すと規定されている。C++言語やJava、JavaScriptなど、C言語系の仕様や記法を参考にした多くの言語でこの表記法が受け継がれている。

string.charCodeAt

.charCodeAt は該当文字のCode Unitを得ることができるメソッドです。

const s = "あ";
console.log(s.charCodeAt(0)); // => 12354
console.log(s.charCodeAt(0)?.toString(16)); // => 3042

例えば、 という文字列に .codePointAt(0) すると 12354 が出力され、 .codePointAt(0).toString(16) すると 3042 が出力されます。

Code Unitとは?

UTF-8やUTF-16の構成要素をCode Unit(符号単位)と呼びます。

Code unit (コード単位) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

コード単位 とは、文字エンコーディングシステムで用いられる(UTF-8 や UTF-16 などの)基本的構成要素です。

筆者はUnicodeのIDをUTF-8やUTF-16で表した値(?)のようなものであると認識しました。

なお、Node.jsは内部的にはUTF-16を採用しています。

文字列とUnicode · JavaScript Primer #jsprimer

JavaScriptは文字コードとしてUnicodeを採用し、エンコード方式としてUTF-16を採用しています。

UnicodeとUTF-8・UTF-16の違いは?

とほほの文字コード入門 - とほほのWWW入門

Unicodeは文字集合、UTF-8やUTF-16は符号化方式というカテゴリになり、それぞれ別物です。

文字集合は文字とID(番号)の対応を決めるものであり、符号化方式は文字集合のIDをどのようなバイト列に変換するかを決めます。文字集合=仕様で符号化方式=実装みたいな印象を受けました(的外れかもしれません)。

そのため、筆者はCode Pointは文字集合上、Code Unitは符号化方式上の値であるという理解をしました。

そして文字集合のID(Code Point)とバイト列(Code Unit)は一致することもあります。

文字列とUnicode · JavaScript Primer #jsprimer

ある範囲の文字列については、Code Point(符号位置)とCode Unit(符号単位)は結果として同じ値となります。

ちなみに、例えばHTMLの <meta charset="" /> なんかはcharset(つまり文字集合)と言っていますが、実際には utf-8 shift_jis などの符号化方式を指定します。

(プログラマのための) いまさら聞けない標準規格の話 第1回 文字コード概要編 | オブジェクトの広場

なお、HTML の文字コード指定の charset や Java のクラスの Charset は、どちらかというと (その名に反して)「文字符号化方式」の方に近いと思います。

このように文字集合と符号化方式の扱いは標準化された仕様においても混同されている場合があり、それがより(筆者のような)初学者の理解を妨げている面がありそうです😇

Shift_JISとは?

実務をしていてUTF-8↔️Shift_JISの変換に遭遇した経験がある方は多い気がします。

Shift_JISは符号化方式であり、文字集合JIS X 0208(とJIS X 0201?)に対応するようです。

とほほの文字コード入門 - とほほのWWW入門

JIS X 0208-1997:通称「97JIS」。Shift_JIS, ISO-2022-JP, EUC-JP(に似たもの)の符号化方式を定義。

XML用語事典 [シフトJIS(Shift_JIS)]

文字集合的には、JIS X 0201(いわゆる半角英数字と半角カナ)と、JIS X 0208(いわゆる第1水準と第2水準の漢字と非漢字)を表現可能である。

UTF-8・UTF-16・Shift_JISのバイト列を比較してみる

まずiconv-liteをインストールします。

npm i iconv-lite@0.6.3
import iconv from "iconv-lite";

const toHex = (buf: Buffer): string => {
    return buf.toString('hex');
};

const s = "あ";
const addBOM = false;
console.log(toHex(iconv.encode(s, "UTF-8", { addBOM }))); // => e38182
console.log(toHex(iconv.encode(s, "UTF-16", { addBOM }))); // => 4230
console.log(toHex(iconv.encode(s, "Shift_JIS", { addBOM }))); // => 82a0

BOM(バイトオーダーマーク)は addBOM: false として付与されないようにします。BOMについては別の記事で書こうと思います。

UTF-{数字}の数字は何?

UTF-8では8ビット単位、UTF-16では16ビット単位で構成されるようです。そのためUTF-{数字}の数字は何ビット単位かを表しているということになります。

UTF-8 - Wikipedia

UTF-8(ユーティーエフはち、ユーティーエフエイト)はISO/IEC 10646 (UCS) とUnicodeで使える8ビット符号単位(1–4バイトの可変長)の文字符号化形式および文字符号化スキーム。

UTF-16 - Wikipedia

UTF-16では、1文字が、16ビットの符号単位が1つまたは2つで符号化される。これが「-16」の名の由来

encodeとdecodeって何?

encode(エンコード)とdecode(デコード)とはそれぞれ何をする操作でしょうか?

JavaのStringを例に採っていますが、下記の説明がしっくりきました。

(プログラマのための)いまさら聞けない標準規格の話 第2回 文字コード実践編 | オブジェクトの広場

Unicode (String) から各文字コード (byte[]) への変換をエンコード (encode)、各文字コード (byte[]) から Unicode (String) への変換をデコード (decode) と呼びます。

iconv-liteのREADMEではサンプルコードと共に次のように記載があります。

// Convert from an encoded buffer to a js string.
str = iconv.decode(Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]), 'win1251');

// Convert from a js string to an encoded buffer.
buf = iconv.encode("Sample input string", 'win1251');

encodeはstringを指定した文字コードのBuffer型にし、decodeはBuffer型からUnicodeのstring型にするという点で上記のJavaでの説明を概ね流用できそうです。JSは内部的にはUTF-16を採用しているため、decode時は符号化方式でいうとUTF-16に変換していそうです(たぶん)。

UTF-8の e38182

下記のUTF-8の表を見ながらUnicodeのID→バイト列を計算することができます。

とほほの文字コード入門 - とほほのWWW入門

Unicodeでは U+3042 です。

U+3042U+0800~U+FFFF の範囲なので、変換前ビット yyyyyyyy xxxxxxxx から変換後ビット 1110yyyy 10yyyyxx 10xxxxxx になります。

実際やってみます。

  1. 16進数 3042 を2進数にすると 00110000 01000100
  2. 上記変換後ビットに当てはめると 11100011 10000001 10000010
  3. 変換後ビットを16進数にすると E3 81 82

というわけでバイト列は E38182 になります。

UTF-16(LE)の 4230

UTF-16にはバイトオーダーという概念があり、ビッグエンディアン(UTF-16BE)とリトルエンディアン(UTF-16LE)があります。UTF-8にはこの概念はないようです。

とほほの文字コード入門 - とほほのWWW入門

16ビットや32ビットの値をファイルやネットワークに書き出す際に、上位バイトから書き出す方式(ビッグエンディアン)と下位バイトから書き出す方式(リトルエンディアン)があります。例えば「あ(U+3042)」を UTF-16 で符号化し、ビッグエンディアンで書き出すと 0x30 0x42 の順序となりますが、リトルエンディアンで書き出すと 0x42 0x30 の順序となります。

iconv-liteではUTF-16においてデフォルトでリトルエンディアンになるようです。

U+3042U+0000~U+FFFF の範囲なので、 yyyyyyyy xxxxxxxx から yyyyyyyy xxxxxxxx になります。つまりそのままです。そしてリトルエンディアンのルールにより 30424230 となります。

Shift_JISの 82a0

Shift_JISは半角文字はJISのまま、全角文字はシフトさせているようです。

Shift_JIS 文字コード表

この表の半角の部分はJISコードのまま。
全角文字の部分をJISコードからずらし(Shift)て作ったコード。実際には細かなところが異なるいくつかの規格があるがここでは単にShift_JISとしている。

とほほの文字コード入門 - とほほのWWW入門

半角文字(制御文字・英数記号・半角カナ)は JIS X 0201 の文字をそのまま使用し、全角文字は、1バイト目を JIS X 0201 で未使用の 0x81~0x9F、0xE0~0xEF、2バイト目を 0x40~0x7E、0x80~0xFC の領域に計算式でシフトして符号化します。

JISからShift_JISへの変換計算は(筆者が力尽きたため😇)割愛します。

JIS X 0208コード表 - CyberLibrarian

上記の表を見ると、829E+2 とあります(SJISの場合)。

このとおりに 829E に2を足すと、 82A0 となります。よってバイト列は 82a0 です。

続編

https://zenn.dev/dyoshikawa/articles/nodejs-charset-introduction-mojibake

参考

Discussion