JavaScriptと絵文字(サロゲートペア)でハマる

前回の記事「PHPでHTMLを簡単に解析できるDOMDocument」で、PHPによるHTML解析がらくちんになったのを良いことに、いろんなWebサイトをスクレイピングしてちゃんと動作するか楽しんで確認していました。

そこで、Yahooブログの<meta name=”description”>におかしな文字列があることを発見。

<meta name=”description” content=”寿司&amp;#127843;”>

実際のコードを持ってくると問題なので、独自に書いていますが、要するに絵文字の数値文字参照がHTMLエンティティ化されてしまっているのです。

これ、あれですね。htmlspecialcharsを2回かけてしまった的な。

PHPで例えるとこんな感じ。

  • echo htmlspecialchars(“<いえーい>”); とした場合 HTML → &lt;いえーい&gt; 表示→ <いえーい>
  • echo htmlspecialchars(htmlspecialchars(“<いえーい>”)); HTML → &amp;lt;いえーい&amp;gt; 表示→ &lt;いえーい&gt;

後者みたいなことするやつおるわけないやろ、と思われるかも知れませんが、何も考えず $_POST を全部htmlspecialchars! そしてそのままDBに格納! 表示するときもhtmlspecialchars! とか、ぶったまげるようなコードを書く人は実在するのです。

天下のYahooブログでお目にかかるとは思いませんでしたが。

対処方法を考える

入力されている文字がおかしいのだから、ほっとけば良い気もしますが、興味本位で対処してみたくなりました。

まず考えたのはこんな感じ。

$target_html = file_get_contents($url);
$target_html = mb_convert_encoding($target_html, "HTML-ENTITIES", 'auto');

$dom = new DOMDocument;
@$dom->loadHTML($target_html);
$xpath = new DOMXPath($dom);

$description = @$xpath->query('//meta[@name="description"]/@content')->item(0)->nodeValue;
//Yahooブログのバグっぽいのの対策(絵文字)
$description = str_replace('&amp;#', '&#', $description);

ブログ紹介欄に&と#を連続して書く機会なんてないやろ、という強引かつ安直な対応。

お仕事ではちょっとアレですが、趣味のプログラミングなので良しとしましょう。

これで寿司の絵文字は文字参照コード &#127843; として取得できるようになりました。

なぜか数値文字参照の絵文字が表示されない

全体の流れとしてはこんな感じなのですが、なぜか絵文字が表示されません。

  1. Webページ上でURLを入力してボタンをポチ(onclick発動)
  2. JavaScriptのXMLHttpRequestを使い、先述のPHPをキック
  3. PHPはDOMDocumentで対象URLのdescriptionを取得して、json_encodeしてレスポンスを返す
  4. JavaScript側は受け取ったresponseTextをJSON.parseして画面に表示する (但し、出力はinputタグのvalue)

どこでおかしくなっているのかが問題です。

PHPが取得するdescriptionがちゃんと &#127843; となっているのは確認しました。

となるとJSONでおかしくなっているのか、とも思いましたが、JavaScriptの受信側もちゃんと &#127843; になっており、通信経路上で文字化けが発生したわけでもないようです。

数値文字参照の基本をおさらい

なんだか、数値文字参照の表記方法自体に不安を覚えてきたので、念の為おさらいもしました。

◎絵文字をHTMLで表示

入力:寿司🍣 ビール🍺
出力:寿司🍣 ビール🍺

◎絵文字コード番号をHTMLで表示

入力:寿司&#127843; ビール&#127866;
出力:寿司🍣 ビール🍺

うんうん、やっぱり表記方法は問題ない。続いてINPUTタグで検証。

◎絵文字をINPUTタグのVALUEに入れた場合

入力:<input type=”text” value=”寿司🍣 ビール🍺”>
出力:

◎絵文字コード番号をINPUTタグのVALUEに入れた場合

入力:<input type=”text” value=”寿司&#127843; ビール&#127866;”>
出力:

えー、やっぱり問題ないじゃーん。

JavaScriptで操作するとおかしいのかも?と考える

◎絵文字をJavaScriptでVALUEに入れた場合

入力: <input type=”text” id=”test1″><script>test1.value=’寿司🍣 ビール🍺’;</script>
出力:

あれ、大丈夫じゃん。

じゃあもしかして…

×絵文字コード番号をJavaScriptでVALUEに入れた場合

入力: <input type=”text” id=”test2″><script>test2.value=’寿司&#127843; ビール&#127866;’;</script>
出力:

はい、きた!!

そういうことかー。そりゃあ考えてみれば当たり前の話でしたね…。

数値文字参照に関わらず、HTMLエンティティ化したタグとか、例えばこんなふうに test2.value = ‘&lt’;
valueに代入した場合も、そのまんまの値で入りますもんね。JavaScript側が勝手に&lt;を<に変換してくれるなんてことはありません。

そうなると困ったぞ。

PHPのDOMDocumentでdescriptionを取得したときの値には &127843; のような数値文字参照がある。それをちゃんとデコードしてあげないといけません。JavaScriptでUnicodeのコードポイントを文字列に変換するとなると、fromCharCode関数が思い浮かびます。

×絵文字コード番号をfromCharCodeで変換した場合

入力:

<input type="text" id="test3">
<script>
test3.value='寿司' + String.fromCharCode(127843) + ' ビール' + String.fromCharCode(127866);
</script>

表示:

寿司とビールじゃなくて豆腐や…(;´Д`)

なんでだ。

試しに String.fromCharCode(12354); とかやると、ちゃんと「あ」に変換されます。

これあれだ。絵文字とかのいわゆるサロゲートペアに対応してないんだ。

fromCodePointという関数が追加されているらしい

ぼくは今回ググってはじめて知ったのですが、2014年頃から String.fromCodePoint というサロゲートペアに対応した関数が追加されているようです。

◎絵文字コード番号をfromCodePoint で変換した場合

入力:

<input type="text" id="test4">
<script>
test4.value='寿司' + String.fromCodePoint (127843) + ' ビール' + String.fromCodePoint (127866);
</script>

表示:

いやっほぉぉう!!

プログラミングで不具合が解消したときの気持ちよさってアハ体験ですよね。

プログラマーに見た目若い人が多いのってこういうのも関係していると思うんだ。

まぁそれはそうと、

自前でコード変換は面倒なので簡単な方法を探ってみる

そもそもHTMLに &#127843; と書くだけでちゃんと表示してくれる仕組みがあるのに、JavaScript内でコツコツとコード変換するのも面倒じゃないかなぁと思うわけです。fromCodePointで変換できるのはわかったけれど、それって自前で &# を検索して~、セミコロンまでの数値を読み取って~、fromCodePointで変換して~、というのを文字の終端まで繰り返すわけじゃないですか。

実際コーディングしてみたらそんなに大変じゃないかもだけど、できれば楽したいよね。
(あとIE11とかfromCodePointをサポートしていない)

そこで考えました。

ダミーのSPANタグを用意して、そのinnerHTMLに突っ込んでから、valueにコピーすれば良いのでは?と。

■コード例

<input type="text" id="test5">
<span id="dummy" style="display:none;"></span>
<script>
dummy.innerHTML = '寿司&#127843; ビール&#127866;';
test5.value = dummy.innerHTML;
</script>

■表示

いやっほぉぉう!! part 2

というわけで、この仕組みを使うことにより、無事ぼくのプログラムではYahooブログのdescription中に含まれる絵文字もちゃんと取ってこれるようになりました。

めでたしめでたし。