はじめに
以前、grapheme_extract() を使って、絵文字や結合文字を途中で壊さずに抜粋する方法について記事を書きました。
その記事では、たとえば絵文字、濁点付きの文字、結合文字を含むテキストを、見た目の文字のまとまりに近い単位で扱う方法を考えました。
ただし、その話には前提があります。
対象の文字列が、UTF-8 として正しく扱える状態であることです。
WordPress 6.9 では、この前提に関わる関数として、wp_is_valid_utf8() と wp_scrub_utf8() が導入されました。
この記事では、この2つの関数を、grapheme_extract() のような文字列処理の前段階として整理します。
この記事は、Unicode や UTF-8 の仕様を厳密に説明するものではありません。WordPress のテーマやプラグインを触るときに、壊れた文字列をどう考えるか。その実務上の判断を中心に書きます。
文字化けではなく「壊れたバイト列」として考える
まず、画面上の文字と PHP の文字列は、少し違うものとして考える必要があります。
画面上では、私たちは次のような文字を見ています。
あé😊
しかし、PHP の文字列は内部的にはバイト列です。
バイト列とは、コンピューターが扱うデータの並びです。人間から見ると文字に見えていても、PHP にとってはまずデータの並びとして存在しています。
通常の WordPress サイトでは、テキストは UTF-8 として扱われます。UTF-8 は、さまざまな言語の文字や絵文字を表すための文字エンコーディングです。
ただし、外部 API、古いデータ、CSV、壊れたファイル、文字コードの違う入力などが混ざると、UTF-8 として正しく読めないバイト列が入り込むことがあります。
このとき、単に「文字化けした」と考えるだけでは、少し曖昧です。
実務では、次の3つを分けて考えると整理しやすくなります。
- 見た目の文字
- PHP が扱うバイト列
- UTF-8 として正しく読めない壊れた部分
この記事では、この「UTF-8 として正しく読めない壊れた部分」を、壊れたバイト列として扱います。
難しい仕様に深入りする必要はありません。大事なのは、壊れたバイト列が混ざっていると、後続の文字列処理が安全に動くとは限らない、という点です。
wp_is_valid_utf8() と wp_scrub_utf8() の役割
WordPress 6.9 で導入された UTF-8 関連の関数はいくつかあります。
この記事では、特に次の2つに絞ります。
wp_is_valid_utf8()wp_scrub_utf8()
wp_is_valid_utf8() は、文字列が UTF-8 として正しいかを確認する関数です。
たとえば、フォーム入力や REST API などで「UTF-8 として正しい文字列だけを受け取りたい」と決められる場合は、次のように考えられます。
if ( ! wp_is_valid_utf8( $text ) ) {
return new WP_Error(
'invalid_utf8',
'The text must be valid UTF-8.'
);
}
この例では、文字列が UTF-8 として正しくなければ、無理に直そうとせずにエラーにしています。
入力のルールを決められる場面では、このほうがわかりやすいことがあります。
一方、すでに保存されている古いデータや、外部サービスから受け取った文章など、完全には拒否しにくい文字列もあります。
そのような場合に使えるのが wp_scrub_utf8() です。
$text = wp_scrub_utf8( $text );
wp_scrub_utf8() は、UTF-8 として壊れている部分を、Unicode の置き換え文字である � に置き換えます。
ここで重要なのは、wp_scrub_utf8() は文字化けを元の文字に復元する関数ではない、ということです。
壊れたデータから、元の文字を確実に復元できるとは限りません。
wp_scrub_utf8() の役割は、壊れた部分を安全に扱える置き換え文字にして、後続処理に渡しやすくすることです。
つまり、きれいに直す関数というより、これ以上壊れ方を広げないための関数です。
なぜ削除ではなく置き換えるのか
不正なバイト列を見つけたとき、単純に削除すればよいように見えるかもしれません。
削除すると、見た目にはすっきりします。
しかし、削除には落とし穴があります。
壊れた部分を削除すると、前後の文字がくっついて、もとの意味とは違う文字列になることがあるからです。
たとえば、次のような文字列があったとします。
A [壊れた部分] B
壊れた部分を削除すると、こうなります。
AB
一方、置き換え文字にすると、こうなります。
A�B
削除は、「壊れた部分はなかったことにする」処理です。
置き換えは、「ここに読めない部分があった」という痕跡を残す処理です。
実務では、どちらが正しいかは場面によります。
ただ、壊れた入力を後続処理へ渡す前処理として考えるなら、置き換えのほうが安全側に倒しやすいと私は考えています。
wp_scrub_utf8() は、この後者の考え方に近い関数です。
grapheme_extract() の前段階として見る
前回の記事で扱った grapheme_extract() は、絵文字や結合文字を途中で壊さずに抜粋するための関数です。
ただし、grapheme_extract() を使う前には、文字列が UTF-8 として正しく扱える状態である必要があります。
そのため、WordPress 6.9 以降の文字列処理は、次のような順番で考えると整理しやすくなります。
入力を受け取る
↓
wp_is_valid_utf8() で確認する
↓
必要なら wp_scrub_utf8() で置き換える
↓
grapheme_extract() などで、文字を壊さずに抜粋する
↓
esc_html() などで、出力先に合わせてエスケープする
ここで注意したいことがあります。
wp_scrub_utf8() は、HTML エスケープの代わりではありません。
UTF-8 として安全に扱えるようにすることと、HTML として安全に出力することは別の話です。
たとえば、画面に出すなら、出力時には文脈に応じたエスケープが必要です。
echo esc_html( wp_scrub_utf8( $text ) );
HTML の本文なら esc_html()、属性なら esc_attr()、URL なら esc_url() のように、出力先に合わせて考える必要があります。
wp_scrub_utf8() は、あくまで UTF-8 として扱える状態に近づけるための前処理です。
出力時の安全性は、別途エスケープで確保します。
WordPress が intl に依存できない理由
PHP には、UTF-8 や多バイト文字列を扱うための関数があります。
たとえば、mb_check_encoding() や mb_scrub() があります。
また、intl 拡張が使えれば、grapheme_extract() も使えます。
それなら、WordPress 本体もそれらに依存すればよいのではないか、と思うかもしれません。
しかし、WordPress は世界中のさまざまなサーバーで動くソフトウェアです。
新しいマネージドホスティングもあれば、古い共有レンタルサーバーもあります。
mbstring や intl のような PHP 拡張が使える環境もありますが、WordPress 本体はそれらが常に有効である前提では設計しにくいです。
特に intl 拡張は、Unicode やロケール処理にとても強力です。
しかし、WordPress 本体の必須要件にはなっていません。
これは、WordPress が国際化を軽視しているという意味ではありません。
むしろ、世界中の環境で動く CMS として、必須要件にできるものを慎重に選ぶ必要がある、という話です。
もちろん、一般的な PHP アプリケーションで mbstring が使えるなら、mb_check_encoding() や mb_scrub() は有用です。
それらを否定する必要はありません。
ただし、WordPress 本体や WordPress プラグイン・テーマで広い互換性を意識するなら、WordPress が提供する wp_is_valid_utf8() や wp_scrub_utf8() を使う意義があります。
抜粋処理をもっと安全にできるか
ここからは、少し発展的な話です。
WordPress 6.9 で、不正な UTF-8 バイト列を検査し、置き換えるための基盤が整いました。
しかし、UTF-8 として妥当な文字列であっても、抜粋処理にはまだ別の問題が残ります。
それが、書記素クラスターを途中で壊してしまう問題です。
書記素クラスターとは、画面上で「1文字のように見えるまとまり」と考えるとわかりやすいです。
たとえば、絵文字、濁点付き文字、結合文字、複数のコードポイントで表される文字は、途中で切ると表示が壊れることがあります。
5月18日の記事では、grapheme_extract() の fallback を考えるために、コードポイント数を数える関数と、書記素クラスターを順に取り出すイテレーターを実装しました。
この考え方は、将来的に WordPress 本体の抜粋処理を検討する場合にも使えるはずです。
ただし、この記事では実装の詳細には深入りしません。
ここで押さえたいのは、次の順番です。
まず、UTF-8 として壊れていないかを見る
次に、表示上の文字を途中で壊さないように抜粋する
最後に、HTML など出力先に応じてエスケープする
wp_is_valid_utf8() と wp_scrub_utf8() は、この最初の段階を支える関数です。
そして grapheme_extract() は、その次の段階を支える関数です。
将来の PHP RFC につながるかもしれない話
最後に、さらに発展的な展望を書きます。
PCRE の \X を使うと、正規表現で書記素クラスターに近いまとまりを扱えます。
たとえば、次のように書けます。
preg_match_all( '/\X/u', $text, $matches );
ただし、preg_match_all() はマッチした結果をまとめて配列に展開します。
長い文章から最初の数文字だけ取り出したい場合には、少し大げさです。
現在の PHP で正規表現のマッチを順番に処理しようとすると、preg_match() に offset を渡しながらループを書く必要があります。
将来的に preg_iter() のような関数があれば、正規表現のマッチ結果を foreach で順番に処理できるかもしれません。
これは、巨大な文字列を一度に配列へ展開せず、必要なところまで読んで止めるための小さな部品になります。
もちろん、この記事では PHP RFC の提案そのものには踏み込みません。
ただ、WordPress のような大規模な実務ソフトウェアで必要性を整理できれば、将来の PHP RFC を考える材料にもなるはずです。
まずは、実務で何に困っているのかを丁寧に言語化することが大事だと思っています。
まとめ
WordPress 6.9 の wp_is_valid_utf8() と wp_scrub_utf8() は、文字化けをきれいに直すための関数ではありません。
壊れた UTF-8 バイト列を検出し、後続処理で安全に扱える状態にするための土台です。
この記事で整理した内容は、次のとおりです。
-
wp_is_valid_utf8()は、文字列が UTF-8 として正しいかを確認する関数である -
wp_scrub_utf8()は、壊れた部分を削除せず、�に置き換える関数である -
wp_scrub_utf8()は文字化けを復元する関数ではなく、後続処理で安全に扱うための関数である -
grapheme_extract()のような安全な抜粋処理の前段階として、UTF-8 の妥当性確認や置き換え処理を考える必要がある - WordPress は
intlに依存できないため、独自に UTF-8 の最低限の基盤を持つ意味がある - 将来的には、WordPress の抜粋処理や PHP 本体の正規表現 API まで含めて、絵文字時代の文字列処理をどう支えるかを考える余地がある
前回の grapheme_extract() 記事は、「表示上の文字を途中で壊さない」ための話でした。
今回の記事は、その前にある「そもそも UTF-8 として壊れていないか」を見る話です。
この2つをつなげて考えると、WordPress における文字列処理は、単なる文字数カウントや substr() の置き換えではなくなります。
まず UTF-8 として安全に扱える状態にする。
次に、見た目の文字を壊さずに処理する。
最後に、出力先に合わせてエスケープする。
この順番を意識するだけでも、絵文字や多言語テキストを含むサイト制作で、かなり安全側に倒しやすくなると思います。