Twitter時代の文字の数え方

正確には、「Unicode 3.1時代の文字の数え方」なのでしょうが、Unicodeの最新バージョンが6.0の今、それではぱっとしないので。

はじめに

一言で言えば、「𠮷野家」は3文字なのか4文字なのか、という話です。これはもちろん3文字で、Twitterの1回のつぶやきに46個含められます。しかし、4文字だとみなされるケースがたくさんあるのです。

30過ぎて数も数えられない、なんてことになるとは思っていなかったのですが、文字を数えるのはけっこう難しいです。とはいえ、Twitterがこれだけ普及しているのに、「難しい」と言って済ますわけにもいかないので、ちょっとまとめておきましょう(拙著『Webアプリケーション構築入門』で使ったもの+αで)。

注意:ちゃんとやるためには、Unicode正規化の話とか、もしかしたらIVSの話とか、いろいろやらなければいけないのかもしれませんが、ここでは話を少し単純にしています。

まとめのまとめ

Unicodeの登録文字数が少ない時代に作られたシステムが、登録文字数の多い時代に対応できていないという問題があります。例えば「𠮷」(U+20BB7)はもちろん1文字ですが、UTF-8・UTF-16ともに、想定を超える4バイトで表されるため、2文字とみなされることがあります(UTF-8のナイーブな想定は3バイト、UTF-16のそれは2バイト)。

𠮷
コードポイント U+20BB7 U+91CE U+5BB6
UTF-8 F0 A0 AE B7 E9 87 8E E5 AE B6
4バイト 3バイト 3バイト
UTF-16 D842 DFB7 91CE 5BB6
4バイト 2バイト 2バイト

対策方法は以下の通りです。

  • HTML5のフォーム検証:あきらめる
  • JavaScript:注意が必要(ふつうの方法ではダメ)
  • PHP:問題なし
  • Java:注意が必要(ふつうの方法ではダメ)
  • MySQL:5.1以前ではあきらめる。5.5以降ではutf8mb4を使う
  • Perl 5.?:問題なし(コメントでの指摘)
  • C#:注意が必要(ふつうの方法ではダメ。正規表現も要注意)
  • Visual Basic:注意が必要(ふつうの方法ではダメ。正規表現も要注意)
  • ASP.NET:注意が必要(ふつうの方法ではダメ。正規表現も要注意)

PerlやPHPは問題ないとは言っても、MySQLを使うようなアプリケーションでは問題が起こることに注意してください。

この結論に至る理由を、少し詳しく説明します。


クライアント側

HTML5

HTML5のフォームでは入力されたデータの検証ができるようになっているのですが、この機能を使って文字数をチェックするのは、やめたほうがよさそうです。

maxlength属性

HTML5のフォームでは、input要素やtextarea要素において、入力できる文字数をmaxlength属性で指定できるようになっています。たとえば、次のようなinput要素では、3文字までしか入力できません。

<input type="text" maxlength="3" />

ところが、ブラウザによっては、ここに「𠮷野家」という3文字を入力できません。そもそもこの3文字を正しく表示できないブラウザもあるようなので、手元のWindows上のブラウザで調べてみました(環境に依ります。たとえば、MacのSafariでは表示が○になります)。

ブラウザ 表示 入力
Chrome 12.0.742.100
Safari 5.0.5 ×
Firefox 21.0 ×
IE 8 ×
IE 9 ×
IE 10
Opera 12.15 × ×

入力「×」のブラウザでは、「𠮷」が2文字とみなされるため、2文字目まで、つまり「𠮷野」までしか入力できません。

Mozillaの文書には、Unicode code pointsで数えると書いてあるので、そのうち改善されるのかもしれませんが、現時点ではTwitterのために「maxlength="140"」を使うことはできません。

pattern属性

Firefox 21とChrome 27、IE 10、Opera 12.15は、「pattern=".{0,3}"」(任意の文字からなる0から3文字)のような正規表現を使った検証にも対応していますが、やはり「𠮷野家」は4文字とみなされてしまいます。


JavaScript

追記:javascript – でBMP以外のUnicode文字をきちんと扱う(404 Blog Not Found)

JavaScriptでは、文字列strの長さをstr.lengthで取得できることになっていますが、残念ながらこの方法では、上記のすべてのブラウザで「𠮷野家」は4文字とみなされてしまいます。

かつて、Twitterのウェブサイトではこの方法で文字数を数えていたと思われます。APIでならつぶやける文字列が、ウェブサイトからでは文字数超過でつぶやけなかったのです。Twitterに報告して直してもらいましたが(参考(https://support.twitter.com/tickets/1279115)リンク切れ)、「𠮷野家」を4文字とみなすクライアントはまだたくさんあるでしょう(手元のHootSuiteとついっぷるはそうでした)。

ちなみに、「.{0,3}」という正規表現で検証することもできません。「𠮷野家」はやはり4文字だとみなされます。

次のような関数を使えば、JavaScriptでも文字列の長さを正しく測ることができます(Stringオブジェクトをそのまま拡張する方法が、「サロゲート・ペアに対応した文字列操作関数を書いてみた」で紹介されています)。

function strlen(str) {
  var i = 0, len = str.length, result = 0;
  while (i < len) {
    result++;
    var x = str.charCodeAt(i++);
    if (0xD800 <= x && x < 0xDC00) i++;
  }
  return result;
}

この方法で数えた文字数が140以下なら、Twitterでつぶやくことができるでしょう(サーバ側も自分で作るときは、サーバ側でのチェックも必要です)。


サーバ側

PHP

mb_strlen()

関数mb_strlen()で正しく長さを測れます。ただし、クライアントから送信されたデータを処理する際には、関数mb_check_encoding()を使って、文字以外の不正なものがないことをまず確認するといいでしょう(拙著『Webアプリケーション構築入門』でも触れました)。

正規表現

長さがある範囲にあることをチェックするだけなら、徳丸浩『体系的に学ぶ 安全なWebアプリケーションの作り方』(ソフトバンククリエイティブ, 2011)で紹介されている方法が洒落ています。

if (preg_match('/\A\P{Cc}{1, 140}\z/u', $str) == 1) {
  // OK

Java

codePointCount()

Javaでは、文字列strの長さをstr.length()で取得できることになっていますが、残念ながらこの方法では「𠮷野家」は4文字とみなされてしまいます(JavaScriptの場合と同じです)。

拙著でも紹介したように、長さを正しく測りたい場合は、「str.codePointCount(0, str.length())」としなければなりません。ただし、クライアントから送信されたデータを処理する際には、条件「str.indexOf(0xFFFD) < 0」をチェックして、文字以外の不正なものがないことをまず確認するといいでしょう。

正規表現

長さがある範囲にあることをチェックするだけなら、徳丸浩『体系的に学ぶ 安全なWebアプリケーションの作り方』(ソフトバンククリエイティブ, 2011)で紹介されている方法が洒落ています(PHPの場合と同じです)。

if (str.matches("\\P{Cc}{1,140}")) {
  // OK

MySQL

MySQL 5.1以前

UTF-8で4バイトになるような文字を文字として扱うようにはなっていないので、文字列はbinaryとして保管し、文字列の長さを関数char_length()で測ったりするのはあきらめましょう(PHPやJavaなど、MySQLの外で測るのが簡単)。n文字まで格納したいなら、VARCHAR(4n)にしなければなりませんが、使う文字によってはn文字より多く格納できてしまうのが困りものです(全部ASCIIなら4n文字)。

MySQL 5.5以降

UTF-8で4バイトになるような文字を文字として扱うためのutf8mb4が導入されたので、これを使うのが簡単です。文字列の長さは関数char_length()で正しく測れます。n文字まで格納したいときも、VARCHAR(n)で大丈夫です。


おわりに

「𠮷野家」は単なる例です。「𠮷野家」でも「吉野家」を出してくるGoogleは偉いとは思いますが、拙著でも述べたように、それでも「吉野家」と書くべきだと私は思います。しかし、ここで述べたような話は、絵文字が大量に導入されたUnicode 6.0の普及が進めば、深刻な問題になるかもしれません。Javaが虐げられる理由が増えなければいいのですが。

想定外(あるいは無視された想定)の津波が原子力発電所を襲うほどの深刻度ではありませんが、ITの世界では、過去に2000年問題がありましたし、現在もこういう問題が発生していますし、将来もおそらく似た問題は起こるのでしょう(例えば2038年問題とか)。

先祖を笑えるようにはならず、先祖からは笑われ続けるのでしょうか。

文字を数えるのはけっこう難しいです。


追記

Perl

Perlは問題ないということをコメントで教えてもらいました。しかし、まさにここで紹介した問題のせいでコメントを壊してしまったので、本文にコードを追記します。

$ perl -v
This is perl 5, version 14, subversion 0 (v5.14.0) built for darwin-2level

$ perl -Mutf8 -E 'say length(𠮷野家)'
3

.NET Framework 4.0

追記

.NET Framework 4.0で文字列の長さを知りたい時は、StringInfo.LengthInTextElementsを使います。String.Lengthではダメです。PHPやJavaでは有効だった正規表現による長さの検査が使えないことにも注意してください。参考:田丸健三郎『UnicodeによるJIS X0213実装入門』(日経BPソフトプレス, 2008)

C#

String str = "𠮷野家";
System.Console.WriteLine(str.Length);
//4

StringInfo si = new StringInfo("𠮷野家");
System.Console.WriteLine(si.LengthInTextElements);
//3

Regex pattern = new Regex("^\\P{Cc}{4}$");
System.Console.WriteLine(pattern.IsMatch("𠮷野家"));
//True(「4文字」にマッチ)

Visual Basic

Dim str As String = "𠮷野家"
Console.WriteLine(str.Length)
'4

Dim si As New StringInfo("𠮷野家")
Console.WriteLine(si.LengthInTextElements)
'3

Dim pattern As New Regex("^\P{Cc}{4}$")
Console.WriteLine(pattern.IsMatch("𠮷野家"))
'True(「4文字」にマッチ)

ASP.NET

拙著『Microsoft Visual Web Developer 2008 Express Edition入門』で紹介した、正規表現による検証コントロールであるRegularExpressionで「.{0,3}」のような正規表現を設定しても、文字列の長さを正しく検証することはできません。「𠮷野家」は4文字とみなされてしまうからです。

PHPやJavaで有効だった「\P{Cc}{0,3}」という正規表現は機能しないようです(機能したとしても、上述のC#やVisual Basicの結果を見ると役には立たなそうですが)。

そこで、CustomValidatorを使うことになります。しかし、MSDNで公開されている「方法 : ASP.NET サーバー コントロールをカスタム関数で検証する」はダメです。文字列の長さを正しく測れません。正しく検証するためには、次のようなコードを使うといいでしょう。


StringInfo si = new StringInfo(args.Value);
args.IsValid = si.LengthInTextElements <= 3; [/csharp] クライアント側で処理したいときも、MSDNの方法はダメで、次のようなJavaScriptが必要になるでしょう(関数strlen()の定義は上述)。

function validateLength(oSrc, args) {
    args.IsValid = (strlen(args.Value) <= 3);
}

テキスト処理のための正規表現に計算理論はあるのかな

なるほど。これはおもしろい。

正規表現で素数判定

「C言語で素数判定」や「Rubyで素数判定」はそうでもないのに、「正規表現で素数判定」と言われるとおもしろいと思うのはなぜだろう。

4320122070計算の理論について勉強したことのある人は皆、正規表現で素数を記述することはできないことを知っている(たとえばSipser『計算理論の基礎』を参照)。だから、「正規表現で素数判定」と言われると、一瞬不思議な感じがするのだろう。

正規表現の表現力はもともとそんなに高くない。だから、

正規表現とは元々数学の概念だけあって、数学の問題を解くのにもってこいですね!(regexp – でエラトステネスのふるい)

なんていうのは、かなりミスリーディングなジョークだ。

とはいえ、テキスト処理のための正規表現は、計算理論における正規表現よりもかなり強力なものになっている。その原因の一つに、括弧でくくられた部分文字列を参照するために「\数字」という記法が導入されたことがあり、今話題にしている素数判定を可能にしたのも、この記法だ(拙著『Webアプリケーション構築入門』でも違いを強調している)

大事なことだから繰り返す。計算理論の正規表現では、素数判定はできない。テキスト処理の正規表現では、素数判定ができる。

素数判定というと、次のようなものをそうぞうする。

sub isPrime {
  my $n=shift;
  my $imax=sqrt($n);
  for (my $i=2; $i<=$imax; $i++) {
    if ($n % $i == 0) { return 0; }
  }
  return 1;
}

たとえば、isPrime(5)を評価すれば、5が素数かどうかがわかる。

これと同じことを、1を5個並べた文字列に対するマッチング「’11111′!~m/^1?$|^(11+?)\1+$/」で調べられる。

なるほど。これはおもしろい。

さて、素数判定には、「エラトステネスのふるい」という有名な方法がある。

「2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20」という数列が与えられたときに、2の倍数を消去、3の倍数を消去、・・・としていくと、素数列つまり「2,3,5,7,11,13,17,19」が残る。

今話題にしている記法では、「11,111,1111,11111,111111,1111111,11111111,111111111,1111111111,11111111111,111111111111,1111111111111,11111111111111,111111111111111,1111111111111111,11111111111111111,111111111111111111,1111111111111111111」という文字列が与えられたときに、「11,111,11111,1111111,11111111111,1111111111111,11111111111111111,1111111111111111111」が残るといい。

正規表現でエラトステネスのふるいはさすがに無理かな(正規表現で素数判定)

という問いで期待されている解答は、次のようなものなのだろう。

これと同じことを、「・・・」という文字列に対する置換「’・・・’=~s/???/???/」で調べられる。

最初に思いついたのはこんなもの。

#!/usr/bin/perl -w
use strict;
use warnings;

my $max=shift || 20;
my $nums=join ',',map {'1' x $_} (2 .. $max);
print "$nums\n";

while ($nums =~ m/(^|,)(1+)(,|$)/) {
  printf "$2,";
  $nums=~s/(^|,)($2)+(,|$)/,/g;
}

これは、理想の形式からはほど遠い。whileなんてものを許したら、表現力が正規表現と比べてどうなのか、まったくわからなくなってしまうのだから。

だから、正規表現とワイルドカードは違うんだって

以前「正規表現とワイルドカードは違います」というタイトルの記事で書いた話ですが、間違いは至る所にあるようです。

もはや手遅れなのかもしれません。間違ったレポートを書いていた学生に教えてもらったページは、Googleで「perl 正規表現」を検索した結果の、最初の10件に入っていました。曰く、

UNIXやWindowsでは、文字列を検索するときに使用される正規表現として、「*」を任意の文字列(空き文字も含む)に、「?」を任意の1文字として使用することができます。パターンを、app* と表現すると、 apple application appeal などの文字列がマッチすることになります。 (http://www.kent-web.com/perl/chap7.htmlのarchive

例えば、「*」を任意の文字列、「?」を任意の 1文字として表す事が出来ます。 (http://www.site-cooler.com/kwl/perl/8.htmのarchive

なりません、出来ません。そうなるのはワイルドカードであって正規表現ではありません。

「何を信じたらいいの?」という向きは、とりあえず、「正規表現では『*』が任意の文字列を表す」と書いてあるサイトは信用しないことにしたらどうでしょう(例:このブログ)。

お約束ですが、実際にコードを書きながら正規表現を学びたいという方には、拙著『Webアプリケーション構築入門 実践!Webページ制作からマッシュアップまで 』(森北出版, 2011)がおすすめです。

正規表現とワイルドカードは違います

注意:ここでは正規表現とワイルドカード自体については説明しません。ここで紹介するような間違いに注意して、もう一度検索してください。

「・・・がわかっていない人は、情報関係の本なんて書くべきではない」なんてことを言うと自分に跳ね返ってきそうで怖いのですが、正規表現とワイルドカードの違いがわかっていない人は、お願いですから情報関係の教科書は書かないでください。

計算機概論の講義のネタを集めているときに、「正規表現とワイルドカードをごっちゃにする」というとんでもない間違いが世に蔓延っているのを知りました。影響力の小さいブログとかなら別にいいのですが、情報処理技術者試験の教科書や、IT用語辞典が間違っているのはまずいでしょう。

たとえば、手元にあった『ソフトウェア開発技術者完全教本』には、次のような記述がありました。

正規表現は、特定の文字列ではなく文字列の一部を一般化して表現するための手法。プログラム言語理論の分野において、字句(変数名や予約語、その他の識別子)を一般化して定義するために考案された表現である。

たとえば、文字列検索において、”*”を任意の文字列(空文字も含む)、”?”を任意の1文字として指定できるとすると、検索文字列に”abc*”と指定すると、次のような文字列が検索の対象となる。

abc, abcd, abcded, abcdefghijklmnなど

などが検索の対象となる。また、”a?c”と指定した場合は、次のような文字列が検索の対象となる。

abc, aac, axcなど

最初の説明は変ですし、後の例は正規表現ではなくワイルドカードのものです。開いた口がふさがらないというやつです。

もう1冊、『情報処理技術者試験学習書 情報処理技術者』には次のような記述がありました。

文字列に対して、特定の文字(”$”、”*”など)を組み合わせて表現することを正規表現という。この特定の文字を、メタキャラクタという。メタキャラクタは特別な意味を持っている。UnixやWindowsなどでファイルを検索するとき、”*.jpg”と指定すると、拡張子が”jpg”であるファイルが検索される。このときの”*”がメタキャラクタである。

メタメタですね。ちょっとした誤解というわけではなさそうです。

辞典も信用なりません。SL900Xに収録されている情報処理技術者用語辞典によれば、

字句をパターンの集合で表す式で定義する表現方式。(中略)「*」はワイルドカードとして利用される。

されません。正規表現とワイルドカードは違います。なんか、やばいことになってませんか?

ASCIIのデジタル用語辞典(Glossary Help)には、先の『ソフトウェア開発技術者完全教本』とほとんど同じ説明が載ってますね。やれやれ(著作権とかは大丈夫なんでしょうか。ウソを護ってもしょうがないかもしれませんが)。

同じくASCIIのデジタル用語辞典(PC EXPLORER)によれば、

文字列の一部をパターン化して記述する方法。正規表現を用いた検索では、特定の単語そのものだけではなく条件に一致する複数の単語を検索できる。UNIXなどで一般的な文字列検索プログラム「grep」では、キーワード中の「*」を任意の文字列、「?」を任意の1文字として検索できる。

できません。それはワイルドカードです。

教科書も辞典もダメだとすると、学生は何に頼ったらいいんでしょう。

学問の場では批判的に見られることが多いようですが、Wikipediaの説明はけっこういいと思いますよ(同じような話は拙著にも書きましたが)。「Wikipediaは信頼性が・・・」なんて乱暴な言い方はできなくなりますね。

拡張された正規表現には正規言語ではない文字列も表せるものも多く、ゆえに正規表現という名前は実態に即していない面もあるが、伝統的に正規表現と呼ばれ続けている。

詳説 正規表現 第3版正規表現の解説書は、Friedl『詳説 正規表現』(第3版)が定番です(最近、新しい翻訳が出ました)。

言語理論の教科書は、Hopcroft, Ullman, Motwani『オートマトン言語理論 計算論』が定番ですが、Sipser『計算理論の基礎』(原著第2版)という新しい教科書もお薦めです(学生だったときに、これの第1版を読みました。1冊のほうが扱いやすくていいのですが)。

432012207043201220894320122097

ちゃんと勉強したい学生の皆さんは、定評のある教科書を選ぶと良いでしょう。

追記:だから、正規表現とワイルドカードは違うんだって

お約束ですが、実際にコードを書きながら正規表現を学びたいという方には、拙著『Webアプリケーション構築入門 実践!Webページ制作からマッシュアップまで 』(森北出版, 2011)がおすすめです。