こんにちは、晴れて2020新卒になったmipsparcです。最近は趣味の鉄道技術系同人誌の新版が出来上がって喜んでいます。
本記事では、入力値には必ずと言っていいほど混入する不必要な制御文字への対処方法をご紹介します。ユーザーに文字列を入力してもらうことのあるすべてのサービスで活用できる話かと思います。
不要な制御文字が入ることで生じる問題
前提として、この記事は制御文字類が必ずしも邪魔と言いたいわけではありません。 制御文字は多言語対応(特に右から左方向に記述する言語)などで重要なときもありますが、今回は問題が発生しうるケースのお話をします。
- 「腐向け」
- 「メリークリスマス」
- 「ゾンビ」
なんの変哲もない3つのイラストタグですが、どれも不可視の制御文字が混入しています。
$ php -r "var_dump(json_encode('腐向け'));" string(26) ""\u202a\u8150\u5411\u3051""
先頭にLRE(Left-to-Right Embedding)と呼ばれる制御文字が混入しています。なお、厳密に言うとUnicode制御文字の1種類であるフォーマット文字ですが、本記事では広義の制御文字として扱います。
$ php -r "var_dump(json_encode('メリークリスマス'));" string(56) ""\u30e1\u30ea\u30fc\u30af\u30ea\u30b9\u30de\u30b9\u202c""
末尾にPDF(Pop Directional Formatting)と呼ばれる制御文字が混入しています
$ php -r "var_dump(json_encode('ゾンビ'));" string(26) ""\u200e\u30be\u30f3\u30d3""
先頭にLRM(Left-to-Right Mark)と呼ばれる制御文字が混入しています
これではせっかくの作品が検索に表示されず、見てもらえないかもしれません。
pixivは英語や中国語などでも多く利用いただいていますが、現状では右から左方向に記述する言語圏からの投稿はほとんど見られず、これらの制御文字はユーザーが意図したものではないでしょう。IMEやブラウザの挙動により混入したものだと推測されます。
特に、ユーザーが入力するタグなどの文字列は検索対象になるため正規化が重要で、入力値から適切に取り除いてあげる必要があります。
制御文字の定義 (ASCII/Unicode)
文字コードにASCIIコードやUnicodeといった種類があるのはご存知のところでしょう。
上記のASCIIコード表で、色がついているところがASCIIコードにおける制御文字です。改行文字(LF, CR)など、身近なものも含まれていますね。なお、本記事ではスペース文字は制御文字とはみなさないことにします。
さて、pixivをはじめ、現代の多くのWebサービスでは多言語の取扱にUnicodeを使用しています。Unicodeでの制御文字はどう識別すればいいでしょうか。
便利な識別方法として、Unicode Character CategoryがCc(Other, control)またはCf(Other, format)かどうかを見るのをおすすめします。未定義領域(Cn: not assigned)や私的領域(Co: private use)、単体のサロゲート(Cs: surrogate)もあわせて検出すると良いでしょう。サブカテゴリCc, Cf, Cn, Co, Csを全部合わせてカテゴリC(Other)と表記できます。
定義マップは以下です。
Unicode - http://www.unicode.org/Public/UNIDATA/Scripts.txt
こちらのサービスは具体的に一覧していて、検索もできて便利です。
Unicode Character Categories - https://www.compart.com/en/unicode/category
正規表現による制御文字の判定方法
上記のようにUnicode Character Categoryにより分類できますが、PCREなどの一部の正規表現ライブラリでは特定のUnicode Character Categoryにマッチする文法が以下のようにあります。
\p{C}
また、\p{C}以外のすべての文字は
\P{C}
と指定できます。
入力値から制御文字を除去する方法
PHPを例に紹介します。こちらのサンプルコードは、与えられた文字列の中の制御文字(例外の制御文字を除く)を置換するものです。
/** * 制御文字を置換する * * @param string $original_str 置換元の文字列 * @param string $replace_to 制御文字の置換先の文字(列) * @param string[] $excepts 例外的に置換しない文字 * @return string */ function replaceControlChar(string $original_str, string $replace_to, array $excepts = ["\u{200D}"]): string { // 例外文字を正規表現に展開する $regexp = preg_quote(implode('', $excepts), '/'); // 置換処理をする // UTF-8として解釈できない文字列だった場合preg_replaceからnullが返るため、空文字にする return preg_replace("/[^\P{C}{$regexp}]/u", $replace_to, $original_str) ?? ''; }
このような処理を入力に適用することで、先述のようなトラブルを未然に防ぐことが可能です。
なお、コード中のU+200DはZero Width Joiner(ZWJ)と呼ばれる制御文字で、接合する文字を構成するのに使用されます。これがないと、たとえば家族絵文字がひとりひとりに解体されてしまいますので、置換しないのが望ましいケースが多いかと思います。
ほかにもカラー絵文字に使われる領域を通すコードや、改行文字を通すコードなどを加えても良いかもしれません。なお、今回は一律削除することで対応していますが、右から左方向に記述する言語などのサポートが必要になった際には別のアプローチが必要になるかもしれません。
混入してしまった制御文字の探し方
RDBMSなどの永続化したデータストアにすでに入ってしまった制御文字を探すには、同様に検索することになります。クエリをGoogle Cloud BigQueryを例に紹介します。
SELECT `id` FROM `tag_names` WHERE REGEXP_CONTAINS(`tag_name`, r"\p{C}")
このように簡単に検索することができます。詳しくは、正規表現ライブラリgoogle/re2のドキュメントを参照してください。
おわりに
以上の方法で、入力値から制御文字を検出・置換することができます。
制御文字はディスプレイに表示されるときは不可視であることが多いですが、意図せず検索や分類の妨げになってしまうことがあります。
ただし、多言語にサービスを展開すれば、制御文字と全く無縁ではいられないでしょう。適切に付き合っていきたいものです。
お読みいただきありがとうございました。