文字コード入門
文字コードが何もわからないので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符号位置を文章中などに記す場合は "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の違いは?
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?)に対応するようです。
JIS X 0208-1997:通称「97JIS」。Shift_JIS, ISO-2022-JP, EUC-JP(に似たもの)の符号化方式を定義。
文字集合的には、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(ユーティーエフはち、ユーティーエフエイト)はISO/IEC 10646 (UCS) とUnicodeで使える8ビット符号単位(1–4バイトの可変長)の文字符号化形式および文字符号化スキーム。
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に変換していそうです(たぶん)。
あ
は e38182
UTF-8の 下記のUTF-8の表を見ながらUnicodeのID→バイト列を計算することができます。
Unicodeでは あ
は U+3042
です。
U+3042
は U+0800~U+FFFF
の範囲なので、変換前ビット yyyyyyyy xxxxxxxx
から変換後ビット 1110yyyy 10yyyyxx 10xxxxxx
になります。
実際やってみます。
- 16進数
3042
を2進数にすると00110000 01000100
- 上記変換後ビットに当てはめると
11100011 10000001 10000010
- 変換後ビットを16進数にすると
E3 81 82
というわけでバイト列は E38182
になります。
あ
は 4230
UTF-16(LE)の UTF-16にはバイトオーダーという概念があり、ビッグエンディアン(UTF-16BE)とリトルエンディアン(UTF-16LE)があります。UTF-8にはこの概念はないようです。
16ビットや32ビットの値をファイルやネットワークに書き出す際に、上位バイトから書き出す方式(ビッグエンディアン)と下位バイトから書き出す方式(リトルエンディアン)があります。例えば「あ(U+3042)」を UTF-16 で符号化し、ビッグエンディアンで書き出すと 0x30 0x42 の順序となりますが、リトルエンディアンで書き出すと 0x42 0x30 の順序となります。
iconv-liteではUTF-16においてデフォルトでリトルエンディアンになるようです。
U+3042
は U+0000~U+FFFF
の範囲なので、 yyyyyyyy xxxxxxxx
から yyyyyyyy xxxxxxxx
になります。つまりそのままです。そしてリトルエンディアンのルールにより 3042
が 4230
となります。
あ
は 82a0
Shift_JISの Shift_JISは半角文字はJISのまま、全角文字はシフトさせているようです。
この表の半角の部分はJISコードのまま。
全角文字の部分をJISコードからずらし(Shift)て作ったコード。実際には細かなところが異なるいくつかの規格があるがここでは単にShift_JISとしている。
半角文字(制御文字・英数記号・半角カナ)は 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
です。
続編
Discussion