Unicode は文字集合のことであるが、そもそもプログラムにおける文字コードとはどのようなものかというところから解説していきたい。

参考:
http://equj65.net/tech/charcode/
https://atmarkit.itmedia.co.jp/fxml/askxmlexpert/024utf/24utf.html

文字コードとは

文字コードとは、コンピュータ上で文字を扱うために、文字に対して割り当てられた数値のことであり、文字と数値の対応付けと呼ぶことができる。

文字コードは文字集合と符号化形式で大別されるが、符号化形式のことを文字コードと呼ばれることが多い。

一般的に文字コードを表現するには 16 進数を用いる( http://www.isc.meiji.ac.jp/~re00079/EX2.2005/2.html )。
文字を 16 進数に戻して表示することを 16 進数バイナリ文字列変換(HEX 変換)という。
符号化形式(文字エンコーディング)によってバイナリー値(16進数で表された文字列)が異なる

文字集合とは

文字集合(文字セットとも呼ばれる)は日本語の「あ」「い」といった文字の集合体のこと。
よく聞く Unicode は文字集合のひとつで、世界中の文字や記号を網羅することを目的とした国際的な文字コード規格である。

符号化形式とは

符号化形式(エンコーディングとも呼ばれる)とは文字集合を構成する文字をコンピュータ上でどういった数値で表現するかを定義のことである。
よく聞く UTF-8 と UTF-16 は符号化方式である。

UTF-8は8ビットの可変長マルチバイトで文字を表現し、UTF-16は16ビットの可変長マルチバイトで文字を表現している。

UTF-8 は、可変長(1〜4バイト)で文字を表現することができる形式である。
ASCII 文字は1バイトで表現され、その他の文字は2バイト以上で表現される(英数は1バイトで表現し、日本語は3バイトか4バイト)。

一方、UTF-16 は2バイトで表現できる文字(0x0000~0xD7FF、0xE000~0xFFFF)はそのまま2バイトで表し、それ以降(0x00000000~0x0010FFFF)の文字は4バイトで表している。
このため英数も日本語も全て2バイトで表現される。

どちらも可変長エンコーディングを採用しているため、UTF-16 で使える日本語の文字で UTF-8 では使えないものはない(ただし、エンコードされたバイト表現は異なる)。
どちらも UCS-4(別名 Unicode で表現できるすべての文字を含む UTF-32)の文字もエンコードでき、その手順は以下のとおりである。

  1. 使用したい文字の Unicode コードポイント(UCS-4 での表現)を調べる
  2. そのコードポイントを UTF-8 に変換する。変換方法は以下のとおり。
    • U+0000 から U+007F(7ビット): 0xxxxxxx
    • U+0080 から U+07FF(11ビット): 110xxxxx 10xxxxxx
    • U+0800 から U+FFFF(16ビット): 1110xxxx 10xxxxxx 10xxxxxx
    • U+10000 から U+10FFFF(21ビット): 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  3. 変換した UTF-8 エンコーディングをテキストデータに適用する

多くのプログラミング言語とエディタは、UTF-8 エンコーディングを自動的に処理できる。したがって、通常は手動で変換する必要はありません。
ただし、特定の文字をプログラム内で使用する場合や、特定の文字を含むデータを扱う場合には、変換の方法を理解しておくと役立つだろう。

英数の割合が多い場合はUTF-8の方が効率がよいが、日本語が多い場合はUTF-16の方が効率がよくなる。

予備知識として符号化形式のなかにも分類があり、エンディアンという概念がある。

エンディアンとは

エンディアンはファイルの先頭2バイトのことで、この2バイトの事をBOM(Byte Order Mark)という。

以下、参考にしている Web ページから引用させていただく。

エンディアンとは複数バイトで構成されるデータの並べ方の事で、ビッグエンディアンとリトルエンディアンがある。例えば「0xABCD」という、2バイトのデータがあったとき、これを「ABCD」と並べるか「CDAB」と並べるかが異なる。前者がビッグエンディアン、後者がリトルエンディアンである。人の目から見ると「ABCD」の方が分かりやすいけど、コンピュータ視点で見ると「CDAB」の方が操作しやすい。

(計算は下の桁から始めるから、下位バイトが先に読み込めた方がコンピュータ的には都合が良い。人には見にくいけど)

http://equj65.net/tech/charcode/

odコマンドでファイルをバイナリ出力すると、以下のようになる。

# リトルエンディアン環境の場合、-t x1オプションを付与しないとリトルエンディアンを意識した出力(前後逆)となってしまう
$ od -t x1 リトルエンディアン.txt
0000000 ff fe 11 11 11 ...
 
$ od -t x1 ビッグエンディアン.txt
0000000 fe ff 11 11 11 ...

BOMが「fffe」の場合はリトルエンディアン、「feff」の場合はビッグエンディアンとなり、BOMが付与されていない場合はビッグエンディアンとして扱われる。

コードポイントとは

コードポイントは符号化形式とは異なる文字集合における個々の文字の符号位置であり、「文字集合を構成する文字を並べて、頭から順番に振った数値」である。
コードポイントはあくまで「その文字の文字集合内での位置」であり、符号化方式ではない。
たとえば Unicode の “A” という文字には、 U+0041 というコードポイントが割り当てられている。

Unicode の場合、Unicode で定義されたひとつの文字や記号は 6 つのASCII文字で表した文字列(コードポイント)で示される。
Unicode のコードポイントは「\u(または\U、あるいはU+)」という 2 文字と、その後に続く 4 桁の 16 進数で構成される。

予備知識として、最近では絵文字など多くの記号があるが、Unicodeには結合文字という種類の文字があり,これを使って複数の符号位置を組み合わせて1文字を表現していることがある。

以下例を引用させていただく。

最近では絵文字にも複数の符号位置を組み合わせて表現するものがあります。次の図のように,女性の絵文字とコンピュータの絵文字とを組み合わせて「女性の技術者」を表すような表現がUnicode絵文字に定義されています。ここでZWJ(Zero-Width Joiner)という制御文字の一種が合成表現のために使用されています。

https://gihyo.jp/book/pickup/2019/0006

プログラムを書いているとコードポイントをよく見かけるが、これはプログラムファイルで文字コードに制限があるからである(たとえば Java のプロパティファイルは文字コード「ISO-8859-1」で記述するというルールがあったりする)。

このコードポイントをユーザーが読めるようにする場合は、そのコードポイントを UTF-8 などの符号化形式で変換するように実装を行う必要がある。

表題の Unicode エスケープとは UTF-8 などでエンコードされた日本語文字列をコードポイントに戻す(エスケープ)ことであり、そのコードポイントに戻されたひとつひとつの文字を Unicode エスケープシーケンスと呼ぶ。

Unicode エスケープのメリットとデメリット

Web API が返す JSON データは Unicode エスケープされていることが多い。
ただし、受信側が適切なエンコーディングをサポートしている場合や、セキュリティリスクが低い状況下では、エスケープを省略されていることもある。

Unicode エスケープには以下のようなメリットとデメリットがある。

メリット

エンコーディングの互換性

JSON の標準 (RFC 8259) では、UTF-8、UTF-16、および UTF-32 エンコーディングをサポートしているが、受信側がこれらのエンコーディングを適切に処理できない場合がある。
Unicode 文字をエスケープすることで、受信側が正しく解釈できるようになる。

不正なバイトシーケンスとデータ損失の防止

以下のように、不正なバイトシーケンスの発生やデータ損失のリスクがある。

エンコーディングの不一致

異なるエンコーディング間での変換の際に、データ損失や不正なバイトシーケンスが発生する可能性がある。
とくに UTF-8 から UTF-16 や UTF-32 などへの変換で問題が生じることがある。

文字の切り捨て

文字列の最大長に制限がある場合、エスケープされていない Unicode 文字が切り捨てられることがあります。これにより、データの一部が失われることがある。

ソフトウェアのバグや制約

エスケープされていない Unicode 文字を正しく処理できないソフトウェアやライブラリが存在する場合、データの損失や不整合が発生することがある。

さまざまな環境下での可読性

Unicode エスケープされた JSON データは、多くの場合 ASCII 文字のみで構成されているため、様々な環境で表示やデバッグが容易になる。

セキュリティ対策

特殊文字や制御文字を含むデータは、セキュリティ上のリスクを引き起こす可能性がある。
Unicode エスケープを使うことで、これらのリスクを回避し、データの安全性を確保できる。

具体的には以下のような攻撃に対してのセキュリティ対策となる。

XSS 攻撃

エスケープされていない特殊文字や制御文字を含むデータは、Web アプリケーションで悪意あるスクリプトを実行するリスクがある。
たとえば、ユーザーが入力した情報を直接出力するような API において、ブラウザから直接 JSON にアクセスした場合である(本質的には Unicode エスケープのみでは足りないが)。

その場合、IE では JavaScript が仕込まれた JSON にブラウザから直接アクセスすると、Content-Type ヘッダを無視してファイルの内容によりコンテンツの形式を決定するという仕様があったため、JSON が HTML(text/html) として解釈されてしまい JavaScript が実行されてしまうというセキュリティホールがある。

これを防ぐには、以下のような HTTP レスポンスヘッダを返す必要がある。

# Content-Type を JSON(application/json)に設定
Content-Type:application/json; charset=UTF-8

# IE8以降において「ファイルの内容によりコンテンツの形式を決定する」ことを禁止(IE7以前は効果なし)
X-Content-Type-Options: nosniff

(いまさらではあるが)IE7以前や、そもそも JSON に直接アクセスさせない場合は、POST パラメーターにトークンを渡すか、HTTPヘッダに固有の文字列を埋め込んだりするなどで、XHR以外からの呼び出しを制限する。

<?php 
// jQuery などでは X-Requested-With: XMLHttpRequest という HTTP リクエストヘッダを付与するため、それを用いて判断する
if ( $_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest' ) {
  exit;
}

※ HTTP ヘッダーでは非標準ヘッダーに X- をつけるという慣習が長らく利用されているが、現在では非推奨とされているため、使われていないものであれば自由に(ただし将来性を考えて)命名するよう推奨されている

参考:https://zenn.dev/ys/articles/a58b02e3cbc2f839f7f1

そもそも JSON 取得するときにエスケープしたいといった場合は、PHP では json_encode 関数でオプションなどを利用するとよい。
その際は、Unicode エスケープだけでなく、ASCII 文字であっても「/」「<」「>」「+」も同様にエスケープする必要がある。

var_dump( json_encode($json, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) );
インジェクション攻撃

ユーザーが入力した情報を直接出力するような API において、SQLインジェクションやコマンドインジェクションなどで、エスケープされていないデータを利用して、攻撃者がデータベースやシステムに不正なアクセスや操作を行うことができるようになる(こちらも本質的には Unicode エスケープのみでは足りないが)。

デメリット

エンコーディングをサポートしている環境下での可読性

JSON のエンコーディングをサポートしている環境下では、適切に文字を読むことができるため、可読性が高くなる。

サイズの増加

識別子ひとつひとつが、1byte の文字列なので、全体的なサイズ増加となる。

PHP を使った各種処理方法

Unicode エスケープされた文字列を UTF-8 文字列に変換する方法

Unicode エスケープされた JSON 文字列を変換する場合、以下のように json_decode 関数を使用する。

$jsonString = '{"hoge":"\u(4桁の文字列)\u(4桁の文字列)"}';
$json = json_decode( $jsonString, true );
var_dump( $json[ 'hoge' ] );
// ほげ

Unicode エスケープされた文字列を変換する場合、json_decode 関数は JSON 文字列を引数に取るため、元の文字列をダブルクォートで囲んで JSON 文字列に変換する必要がある。

$string = '\u(4桁の文字列)\u(4桁の文字列)';
var_dump( json_decode('"' . $string . '"') );
// ほげ

Unicode エスケープされた JSON 文字列を出力する方法

json_encode 関数をそのまま利用すると、日本語が Unicode でエスケープされる。

var_dump( json_encode( [ 'hoge' => "ほげ" ] ) );
// {"hoge":"\u(4桁の文字列)\u(4桁の文字列)"}

UTF-8 文字列のまま JSON 文字列を出力する方法

json_encode 関数で JSON_UNESCAPED_UNICODE オプションを使用する。

var_dump( json_encode( [ 'hoge' => "ほげ" ], JSON_UNESCAPED_UNICODE ) );
// [ 'hoge' => "ほげ" ]

おまけ:UTF-8 の文字列を UTF-16 に変換する場合

// UTF-8 文字列
$utf8String = 'ほげ';

// UTF-8 文字列を UTF-16 文字列に変換
$utf16String = iconv( 'UTF-8', 'UTF-16', $utf8String );

// この例では、UTF-16 文字列をバイナリデータとして扱っているため、
// bin2hex 関数を使用して 16 進数表現に変換して表示
echo bin2hex( $utf16String );