SheepDev

テキスト・正規表現

文字数以内なのに、バイト数制限にかかるのはなぜ?

使うツール

『20文字まで』と書かれた欄に、見た目では十分短い文字列を入れたのにエラーになる。反対に、画面では入力できたのに保存する段階で拒否される。こんなとき、利用者の数え間違いとは限りません。画面が数える一文字と、プログラムや保存先が数える一単位が一致していない可能性があります。

文字数・バイト数ツールで、同じ入力を複数の物差しにかけてみましょう。ここで分かるのは入力文字列の特徴です。相手のサービスへ接続して制限を調べたり、データベースの設定を読み取ったりする機能ではありません。エラーの原因候補を絞り、仕様や実装と照合するための小さな実験として使います。

ABC、あいう、😀を一つずつ入れる

まず入力を空にしてABCだけを入れます。表示上の文字、コードポイント、UTF-16単位、UTF-8バイトは、いずれも3です。次にあいうへ置き換えると、最初の三つは3のままですが、UTF-8バイトは9になります。英数字だけの試験で問題が見つからなくても、日本語を入れた瞬間にバイト制限へ到達する理由が見えてきます。

同じ入力を四つの単位で数えた結果
input | graphemes | codePoints | utf16CodeUnits | utf8Bytes
ABC   | 3         | 3          | 3              | 3
あいう | 3         | 3          | 3              | 9
😀    | 1         | 1          | 2              | 4

最後に😀だけへ置き換えます。見た目は一つで、コードポイントも一つですが、UTF-16では二単位、UTF-8では四バイトです。『絵文字は二文字』という覚え方では不十分です。何を数える場合に二つなのかを添えなければ、別の言語や保存形式へそのまま当てはめてしまいます。複数の要素で組み立てられた絵文字なら、さらに別の数になります。

同じéでも、中身が一つとは限らない

次は見た目が似た二つの入力を比べます。一つはéを一つのコードポイントで表した文字列、もう一つはeに結合用のアクセントを続けた文字列です。前者はコードポイント1、UTF-8で2バイト。後者はコードポイント2、UTF-8で3バイトです。通常の書記素分割では、どちらも表示上の一文字として扱われます。

それぞれ別々にコピーして確認する二行
é
é

二行をまとめて貼り付けると、改行も測定対象になります。一行ずつ試すのがポイントです。違いを詳しく見たい場合はUnicode文字情報でU+00E9とU+0065・U+0301を確認できます。正規化するとそろう場合がありますが、文字数を減らす目的で利用者の入力を無断に書き換えてよいという意味ではありません。識別子や署名など、元の並びに意味がある用途では、受け入れる側の規則を先に確認します。

12バイトの欄なら、何が入る?

架空の仕様として『UTF-8で12バイト以内』の欄を考えます。ASCIIのAなら12個、日本語のあなら4個、😀なら3個で、どれも境界の12バイトです。それぞれ一つ追加すると13、15、16バイトになります。境界ちょうど、境界の手前、境界の直後を確認する試験にすると、長い文章を適当に貼るよりも原因を説明しやすくなります。

ここから『最大文字数はバイト数を3で割ればよい』とは結論できません。英字、漢字、絵文字、結合文字が混ざれば、一文字当たりのバイト数は一定ではないからです。利用者には表示上の文字数を案内し、保存処理には別のバイト上限があるなら、両方を明示して別々に検証する設計が考えられます。どちらを採用するかはサービスの要件であり、このツールが自動で決定するものではありません。

空白と最後の改行も、入力の一部

ABCの末尾へ半角空白を一つ追加すると、UTF-8は4バイトになります。見た目の変化が小さいため、『同じ文字を貼った』と思っていても結果が違うことがあります。行の整理で前後空白を除去すれば数は変わりますが、それは元の入力を変換した結果です。元データの制限を調べる段階では、まず加工せずに測り、どの処理の後で短くなったかを区別します。

このツールの行数は改行区切りで数えます。Aの後に改行を一つ付けた入力は、末尾の空行を含む2行になります。画面の幅によって文章が折り返されても、入力に改行が増えたわけではありません。またLFとCRLFでは保存されたバイト列が違います。ブラウザーの入力欄へ貼る過程で改行がそろう場合もあるため、元ファイルそのもののバイト数と同じだと決め付けないでください。

文字列のバイト数と、送るデータの大きさは別

名前の文字列が12バイトでも、それをJSONに入れればキー名、引用符、区切り記号などが加わります。URLエンコードした場合は、元のUTF-8バイト数とは異なる長さの表記になります。ファイルならBOMや改行形式、圧縮の有無も関わります。『通信上限に収まるか』を調べるときは、本文の文字列だけを測った値と、実際に送る形式のサイズを混同しないことが大切です。

表示上の文字数はブラウザーのIntl.Segmenterを利用します。未対応の環境では、その項目を推測値で埋めません。絵文字などの境界判定は実行環境が持つUnicode情報にも関係するため、サービス側との完全一致を保証する検査ではありません。問題が特定の端末だけで起きる場合は、同じ入力で端末とブラウザーも記録して比較します。

『短いのにエラー』を、再現できる報告へ

報告には、入力した例、表示上の文字数、UTF-16単位、UTF-8バイト、どの段階で拒否されたかを残します。実在する氏名や秘密の文字列を使う必要はありません。同じ種類の文字で問題を再現する短い例へ置き換えれば、共有しやすくなります。『😀が3個なら通り、4個なら保存時に失敗する』という観察は、『絵文字が使えない』よりも修正すべき条件を絞れます。

数が分かれば、次に確認する場所も決まります。画面のカウンター、入力検証、APIの制約、保存先の定義が、どの単位で一致すべきかを照合します。制限の値だけを大きくして回避する前に、表示と実装が同じ約束をしているかを見る。小さなカウンターですが、利用者へ正確なエラーを返すための仕様確認にも役立ちます。

制限ぴったりの入力と、その次を試す

UTF-8で12byteまでの入力欄を調べるなら、『あいうえ』は12byte、『あいうえお』は15byteです。ASCIIの『ABCDEFGHIJKL』も12byteですが、見た目の長さは大きく違います。この二種類がどちらも上限どおり通り、その次の文字で拒否されるかを試すと、単なる文字数制限なのかbyte制限なのかを切り分けられます。さらに『ABCDEFGHIJK😀』は11byteと4byteで合計15byteです。途中まで英字だったから残り一文字だけ入る、とは限りません。

上限を超えた入力をbyte位置だけで途中切断すると、UTF-8の一文字を構成する途中で切ってしまうことがあります。見た目の文字数だけで切っても、結合文字や絵文字のまとまりを分断する可能性があります。このカウンターは長さを調べる道具で、任意の入力を安全に短縮してくれる機能ではありません。入力システムを直す場合は、黙って切るより上限と現在値を示して利用者に直してもらうのか、どの単位で切る仕様なのかを先に決めます。測った単位と実際に制限する単位が一致していることを確認してください。

参考資料

使うツール

使い方・解説 →