コンテンツへスキップ

「1文字」とは何を数えるのか

同じテキストでも、4つのシステムは4通りに数えます。英語の文章ならどれも一致するため、ある日突然ずれるまで、この問題は表に出てきません。

最終確認日:

絵文字1つに、4つの答え

肌の色を指定したサムズアップや、家族の絵文字を例にとります。4通りに数えると4つの異なる数字になり、しかもどれも間違いではありません。

読む人には1文字に見えます。 1つのもので、Backspaceキーを1回押せば消えます。

正規表現では7文字に見えることがあります。 絵文字は、目に見えない結合文字でつながれた複数のコードポイントでできています。ベースの絵文字、修飾子、結合子、別の絵文字……という具合です。

JavaScriptは11と報告します。 テキストを16ビット単位で保持し、基本範囲の外にある文字は2単位を使うため、各コードポイントが2つ分に数えられることがあるからです。

データベースの列では25バイトになります。 保存時にUTF-8でエンコードされ、各コードポイントが最大4バイトを占めるからです。

普通の英語の文章なら、この4つの数字はすべて同じです。だからこそ、誰かがアクセント付きの名前や日本語の文、あるいは絵文字を1つ入力するまで問題は見えず、3年間問題なく動いていた欄が突然データを切り捨て始めるのです。

制限が意味している数

実際に問うべきは「このテキストの長さはいくつか」ではありません。「何が制限をかけているのか」です。よくある答えは4つあります。

データベースの列をVARCHARで宣言した場合、ほとんどのエンジンではバイト数を数えます。英語なら255文字が楽に入る欄に、1文字3バイトの日本語だとはるかに少ない文字数しか入らないのはこのためです。アクセントなしなら収まった名前が、アクセント付きだと収まらないことがあるのも同じ理由です。

SMSは独自の7ビットの文字セットで160文字ですが、その範囲外の文字が1つでも入った瞬間に1通あたり70文字に下がります。ワープロから貼り付けたカーリークォート1つ、あるいは絵文字1つで、1通のメッセージが3通に分かれ、料金が3倍になることもあります。

フォームの入力チェックは、JavaScriptではほぼ必ずUTF-16の単位で数えます。280文字までと書かれた欄が、絵文字だらけの文字列を、見た目はずっと短いのに拒否することがあるのはこのためです。

人間は目に見えるものを数えます。見出し、titleタグなど、見た目の長さに制約があるものにとって意味があるのは、この定義だけです。

数え方がずれる場面

理論を知るより、どんな入力が問題を起こすかを知っておくほうが役に立ちます。そうした入力は予測できるからです。

絵文字、特に組み合わせで作られるもの。肌の色、家族、国旗、職業などです。国旗は2つの地域指示文字、職業は多くの場合、人物と結合子と物の組み合わせです。

アクセント付きの文字と結合文字。 éは1つのコードポイントでも、eの後に結合用のアキュートアクセントを続けた形でも書けます。見た目は同じなのに、並べ替えの順序が異なり、比較すると不一致になり、文字数も違います。しかも実際のデータには両方の形が現れ、同じ列に混在していることもよくあります。

ラテン文字以外の文字。 中国語・日本語・韓国語の文字はUTF-8で1文字3バイトです。ヒンディー語、タイ語、タミル語、アラビア語は、読む人には1つの単位に見えても、実際には複数のコードポイントからなるまとまりを作ります。

数学記号や音楽記号。 これらは基本範囲の外にあるため、UTF-16では2つ分に数えられます。

どう対処するか

制限が数えているものを数えましょう。 4つの数値をすべて表示するカウンターを使えば、あれこれ考えるより早く直接答えが分かります。コードでは、目的に合った関数を使ってください。JavaScriptのIntl.Segmenterは読む人に見える文字を、Pythonのlen(s.encode("utf-8"))はバイト数を数えます。PHPではstrlenではなくmb_strlenを使います。

フォームではなく列を直しましょう。 データベースの欄が制約になっているなら、根本的な解決は上流にあります。MySQLならutf8mb4、PostgreSQLならtextで宣言してください。MySQLの古い「utf8」は1文字3バイトまでしか扱えず、絵文字をまったく保存できないことで有名です。この長年の落とし穴が、実在する大量のデータを黙って切り捨ててきました。フォーム側で慎重に数えるのは、本来別の宣言をすべきだった列に対する応急処置にすぎません。

入力の時点で正規化しましょう。 入力を受け付ける時点でテキストを1つの正規形に変換すれば、éの2通りの書き方が1つになり、比較も並べ替えも文字数も一致するようになります。どの言語にもそのための関数があり、入口で一度正規化するほうが、あらゆる場所で両方の形に対応するよりはるかに簡単です。

関連するケース:Base64

同じような驚きを生むので触れておきます。データをBase64にエンコードすると、3バイトずつ取り出して4文字で書き表すため、結果は約3分の1大きくなります。 これは非効率ではなく単なる計算です。6 MBの添付ファイルは約8 MBのテキストになるため、サイズ制限を十分に下回っていたファイルでも、送信用にエンコードされると拒否されることがあります。

メールの添付ファイルが、制限内に見えるのに送り返されてくる理由も同じ計算で説明できます。上限に合わせてサイズを考えるなら、エンコード後のサイズで考えてください。

よくある質問

255文字の欄に、もっと短いテキストが入らないのはなぜですか?

ほぼ間違いなく、文字数ではなくバイト数を数えているからです。UTF-8ではアクセント付きの文字は2バイト、CJKの文字は3バイトなので、255バイトは日本語なら85文字かもしれません。列をutf8mb4またはtextで宣言すれば、根本的に解決します。

絵文字1つで本当にSMSが3通分になるのですか?

なることがあります。GSM文字セットの文字だけで書かれたメッセージは1通あたり160文字ですが、範囲外の文字が1つでもあると、メッセージ全体が1通70文字のエンコードに切り替わります。そのため、150文字のメッセージに絵文字が1つ入ると、1通ではなく3通になります。

titleタグにはどの数え方を使えばいいですか?

厳密にはどれでもありません。検索エンジンは文字数ではなくピクセル幅で切り詰めるため、幅の広い大文字のタイトルは幅の狭い小文字のタイトルより早く切られます。一般的な目安は60文字前後ですが、これは上限ではなく目安です。

見た目が同じ2つの文字列が一致しないのはなぜですか?

多くの場合、アクセント付きの文字が2通りの方法で書かれているからです。一方は1つのコードポイント、もう一方はベースの文字と結合記号の組み合わせです。比較する前に両方を同じ正規形に揃えれば一致するようになります。データがシステムに入る時点で行っておく価値があります。

単語数は文字数より安定していますか?

英語ならおおむねそうです。ただし、どの言語でもそうとは限りません。中国語や日本語は単語をスペースで区切らず、タイ語も同様です。そのためこれらの言語で単語を数えるには分かち書きの処理が必要で、ツールによって答えが変わります。