LoginSignup
55
49

More than 5 years have passed since last update.

文字エンコーディングを変換するとき、対応していない文字を数値実体参照に変換する

Last updated at Posted at 2015-10-11

前書き

昔、以下の質問をしました。

PHPでShift-JISにある文字かどうか判定する方法を教えて下さい... - Yahoo!知恵袋

この質問で聞きたかった事は、UTF-8(Unicode)をShift_JISに変換するとき、UnicodeにはあるがShift_JISには無い文字をHTMLの数値文字参照に変換したいというものでした。
要は、以下の様なことがしたかったわけです。

before(UTF-8)
神と神
艗神社
御鎽神社
ホッケの漢字は𩸽
after(Shift_JIS + 数値文字参照 10進数指定)
神と神
艗神社
御鎽神社
ホッケの漢字は𩸽
after(Shift_JIS + 数値文字参照 16進数指定)
神と神
艗神社
御鎽神社
ホッケの漢字は𩸽

参考: 神名地名難読漢字・ユニコード対照表

この投稿では、この処理を実現する方法について紹介します。

前提知識:対応していない文字とは?

世界には様々な文字が存在し、それをコンピュータで扱う場合は対応する数値に変換する必要があります。
この、数値に変換するときのルールがいわゆる文字コードで、この投稿でいうところの「文字エンコーディング」です。

このルールには様々な方式があります。例えばUTF-8やShift_JIS、EUC-JP、JIS(ISO-2022-JP)など。
これ以外にも、Big5、GBK、US-ASCIIなど、様々です。

このルールは、Unicode(後述)が登場するまでは国ごとにバラバラで、自国の文字を表示するために新しいルールを作ったり、既存のルールを拡張したりしていました。
そしてこのルールを作る時、データ量を減らすため、また文字の処理を容易にするために対応する文字の数を制限することになります。
特に既存のルールを拡張する場合、使用できる数値の数が少ない影響で対応する文字の数を制限する必要がありました。

この結果、Shift_JISでは難読漢字や他国の文字が扱えない、などのような「対応していない文字」が発生することになってしまったわけです。
Shift_JISで書かれたテキストファイルには、インドの文字は書けません。
ただし、最初のコンピュータが英数字をベースとして動いていた関係から、どのルールでもASCIIに該当する半角英数字は扱えるようになっています。

Unicode(ユニコード)はこれを解決するために作られた新しいルールで、世界中のすべての文字を同じルールで扱えるようになっています。
(なお、UTF-8はこのUnicodeを表現する方式の1つです)
そして、HTMLの数値文字参照は、このUnicodeの番号をASCIIにある文字だけで指定することで、それに対応する文字が表示されるよう決められています1

例えば、以下の文字列をShift_JISのHTML内で書いたとします。
すると、そのHTMLの文字エンコーディングがShift_JISであるにも関わらず、ブラウザが対応している場合、Unicodeにしか存在しないうんこの絵文字が表示されるというわけです。

💩

この文字列は全てASCIIにある文字だけで記述されています。
そして、ASCIIにある文字は全てのルールで対応しています。
これを利用することで、「対応していない文字」を全てのルールで利用できるというわけです。

ちなみに、Webブラウザがフォームから文字列を送信する場合も、これと同じことが行われています。
Shift_JISのHTMLページからUnicodeの文字を送信すると、サーバには該当の文字が数値文字参照に変換されて届きます。

HTML Standard

For each character in the entry's name and value that cannot be expressed using the selected character encoding, replace the character by a string consisting of a U+0026 AMPERSAND character (&), a U+0023 NUMBER SIGN character (#), one or more ASCII digits representing the Unicode code point of the character in base ten, and finally a U+003B SEMICOLON character (;).

古いCGIの掲示板などでは、送信された数値文字参照がHTMLエスケープされ、数値文字参照がそのまま表示される文字化け?を起こしていたりします。

方法1:変換に失敗した文字を1つずつ数値文字参照に変換する

前書きでリンクした質問にて、解答にあった方法です。
コードは以下になります2

$str_utf8 = '神と神?';

$str_sjis = utf8_to_escaped_sjis($str_utf8);
// $str_sjis: "神と神?"

function utf8_to_escaped_sjis($str_utf8) {
    $char_utf8_list = preg_split('//u', $str_utf8, -1, PREG_SPLIT_NO_EMPTY);
    $char_sjis_list = $char_utf8_list;
    mb_convert_variables('SJIS', 'UTF-8', $char_sjis_list);
    $char_sjis_list_escaped = array_map(function($char_sjis, $char_utf8){
        if ($char_sjis === '?' && $char_utf8 !== '?') {
            return mb_convert_encoding($char_utf8, 'HTML-ENTITIES', 'UTF-8');
        } else {
            return $char_sjis;
        }
    }, $char_sjis_list, $char_utf8_list);
    $str_sjis_escaped = implode('', $char_sjis_list_escaped);
    return $str_sjis_escaped;
}

PHPで文字エンコーディングを変換する関数mb_convert_encodingmb_convert_variablesは、変換できない文字を?に置換します3
このため、予め入力文字列を1文字ずつの配列に変換し、文字エンコーディングを変換対象のものに変換した後で、?を対応の文字の数値文字参照に変換します。

…文章では分かりにくいので、順を追って処理内容を説明すると:

$str_utf8 = '神と神?';

/**
 * preg_split関数を利用し、1文字ずつに分割した配列に変換します。
 * 本来はstr_split関数で処理するべきものですが、str_split関数はマルチバイト文字に非対応です。
 */
$char_utf8_list = preg_split('//u', $str_utf8, -1, PREG_SPLIT_NO_EMPTY);
// $char_utf8_list: ['神', 'と', '神', '?']

$char_sjis_list = $char_utf8_list;

/**
 * UTF-8の文字列の配列を、Shift_JISに変換します。
 * mb_convert_variables関数は、引数に指定された配列の値の文字に対しエンコーディング変換を行う関数です。
 */
mb_convert_variables('SJIS', 'UTF-8', $char_sjis_list);
// $char_utf8_list: ['神', 'と', '神', '?']
// $char_sjis_list: ['神', 'と', '?', '?']

/**
 * Shift_JISに変換された文字で"?"のものを、対応するUTF-8文字を数値文字参照に変換したものに置き換える。
 * ただし、元のUTF-8においても"?"だったものは置き換えない。
 */
$char_sjis_list_escaped = array_map(function($char_sjis, $char_utf8){
    if ($char_sjis === '?' && $char_utf8 !== '?') {
        return mb_convert_encoding($char_utf8, 'HTML-ENTITIES', 'UTF-8');
    } else {
        return $char_sjis;
    }
}, $char_sjis_list, $char_utf8_list);
// $char_utf8_list:         ['神', 'と', '神', '?']
// $char_sjis_list:         ['神', 'と', '?', '?']
// $char_sjis_list_escaped: ['神', 'と', '神', '?']

$str_sjis_escaped = implode('', $char_sjis_list_escaped);
// $str_sjis_escaped: "神と神?"

以上の処理により、変換できない文字を数値文字参照にします。
ただし、マルチバイト文字列の配列化処理がUTF-8の文字列に対してしか出来ないため、入力はUTF-8に限られます

方法2:mb_substitute_characterentityを指定する

ぶっちゃけこれが一番簡単なんじゃないかな。
コードは以下になります。

$str_utf8 = '神と神?';

mb_substitute_character('entity');
$str_sjis = mb_convert_encoding($str_utf8, 'SJIS', 'UTF-8');
// $str_sjis: "神と神?"

mb_substitute_character関数は、mb_convert_encoding関数やmb_convert_variables関数で変換できない文字の扱いを設定する関数です。
初期値は63で、これは10進数値のUnicode値で?に対応する指定値です。
方法1で変換できない文字が?になるのは、この設定値のためです。
これにentityを指定することで、変換できない文字が数値文字参照に変換されるようになります。

マルチバイト文字列関数の機能を利用するため、方法1とは異なり、PHPが変換できる文字エンコーディングであれば全ての入力エンコーディングに対応できるものと考えられます。
(未検証)

ただし、mb_substitute_character関数の設定はコード内で使用されている全てのmb_*系関数に影響があります4
このため、変換した後は元の設定値に戻したほうが良いでしょう。

$str_utf8 = '神と神?';

$default_substitute_char = mb_substitute_character();

mb_substitute_character('entity');
$str_sjis = mb_convert_encoding($str_utf8, 'SJIS', 'UTF-8');
// $str_sjis: "神と神?"

mb_substitute_character($default_substitute_char);

旧バージョンのPHPにおける問題

古いバージョンのPHPでは、mb_substitute_character関数の動作が不安定なようです。

PHP-users - [PHP-users 34591] 変換不能文字の数値エンティティ化

さらに自己レスです。しつこくてすみません。
手近にあったPHPで試したところ以下のようになりました
下の2つはフランス語のカフェを変換したものです。

PHP4.3.9 "entity"未対応
PHP5.1.6 "entity"未対応
PHP5.2.6 caf&#E9;
PHP5.2.8 café

5.2.6で16進を示すxが付かないようなので気を付けましょう。

ただし、このバージョンのPHPは公式のサポートが切れており、セキュリティアップデートも行われていません。

PHP: Supported Versions

今後、化石と言っても過言ではないこのバージョンに遭遇することは、極めて稀でしょう。よって、この問題は無視できるものと考えられます。


  1. 正確にはUnicodeではなく、UCS(ISO/IEC 10646)の番号です。ただ、UCSはUnicodeとおおむね互換性があるため、区別する必要はあまり無いでしょう。 

  2. 元のコードと中核処理は同じですが、一部改変を行っています。元のコードは質問、またはPastebinの投稿を参照して下さい。 

  3. mb_substitute_characterで指定値を63以外のものに変えていた場合はこの限りではありません。 

  4. 正確には「全てのmb_*系関数」ではなく、mb_convert_encodingmb_convert_variablesmb_output_handlermb_send_mailに影響があります。 

55
49
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
49