読者です 読者をやめる 読者になる 読者になる

Paradigm Shift Design

ISHITOYA Kentaro's blog.

PHPでの文字列の文字数・バイト数・文字幅(見た目の長さ)の数え方

PHPで、見た目の長さで文字列を切って「…」を後につけるなんてことはよくやることなんだけれども、strlenでは意味がない場合があることに、恥ずかしながら最近気がついたのでメモ。


例えば、

123あい45うえお

という文字列があったときに、半角10文字分を超える場合は後ろを切って「…」をつけて表示する時、

123あい45…

が正しいとする*1場合、「123あい45」をstrlenで数えると、UTF-8の場合は11バイトになり、mb_strlenで数えると7になる。欲しいのは9なのに。


一般に、バイト数を知るには、strlenを使う。
PHP: strlen - Manual
また、全角・半角を区別して文字数を数えるにはmb_strlenを使う。
PHP: mb_strlen - Manual
そして、今回のように文字幅(見た目の長さ)を数えるにはmb_strwidthを使う。
PHP: mb_strwidth - Manual


strlenとmb_strlen、mb_strwidthについてもう少し詳しく違いを書いておく。strlenとmb_strlenはともに「文字数」を数える関数。ただし、無印のstrlenは、文字エンコーディングを考慮しない。
例えば、

<?php
$single = "a";
$multi = "";
var_dump(strlen($single), strlen($multi));
var_dump(mb_strlen($single), mb_strlen($multi));
?>

という場合、実行結果は

int(1)
int(3)
int(1)
int(1)

となる。
つまり、strlenは単純に、「与えられた文字列が何バイトか」を取得するための関数だということが分かる。これは最初PHPが世界=西欧な人たちの言語だった名残なのだろう。世の中にマルチバイトの文字文化が存在しなければそれでもかまわないのだけれども。


ところで、もしもこの実行結果がstrlenの結果と全く同じで、

int(1)
int(3)
int(1)
int(3)

のようにバイト数を表しているようなら*2、デフォルト文字エンコーディングの設定がちゃんとできていないので、

<?php
var_dump(mb_strlen($multi, "UTF-8"));
?>

と第2引数に文字コードを設定するときちんと動作する。ここで、注意すべきなのは、"あ"が2バイトか3バイトかあるいはそれ以上かは「PHP文字コードに依存する」という点。だから「全角文字は2バイト」といって決めうちをして、

<?php
$multi = '';
$count = strlen($multi) / 2;
?>

なんて書くと、UTF-8な環境に設置した場合には痛い目を見ることになる。
というわけで、mb_strlenはそういった面倒くさい文字エンコード周りの面倒を見つつ文字数を数えてくれる関数。


そして、strlen系の関数とmb_strwidthの違いは、取得できるものが「文字数」か「文字幅」かという点。詳しくは以下のサンプルを見ていただいたほうが早いと思う。
「aあ」はmb_strlenで数えると2、mb_strwidthで計ると3となる。つまり、mb_strwidthは日本語圏においては半角と全角を考慮して半角の文字幅を1として文字の幅を計算してくれる関数。



しかし、mb_strimwidthなんて関数があるんだな。
自力で作らなきゃいけないものだと思ってた…
PHP: mb_strimwidth - Manual


サンプル

<?php
$hankaku = "1234567890";
$zenkaku = "12345";
$mazegaki = "112233";

$encode = mb_detect_encoding($zenkaku);

echo "バイト数\n";
var_dump(strlen($hankaku),
         strlen($zenkaku),
         strlen($mazegaki));

echo "文字数\n";
var_dump(mb_strlen($hankaku, $encode),
         mb_strlen($zenkaku, $encode),
         mb_strlen($mazegaki, $encode));

echo "文字列の幅\n";
var_dump(mb_strwidth($hankaku, $encode),
         mb_strwidth($zenkaku, $encode),
         mb_strwidth($mazegaki, $encode));

echo "文字の切り出し(mb_substr)\n";
var_dump(mb_substr($hankaku, 0, 5, $encode) . "",
         mb_substr($zenkaku, 0, 5, $encode) . "",
         mb_substr($mazegaki, 0, 5, $encode). "");

echo "文字の切り出し(mb_strimwidth)\n";
var_dump(mb_strimwidth($hankaku, 0, 5, "", $encode),
         mb_strimwidth($zenkaku, 0, 5, "", $encode),
         mb_strimwidth($mazegaki, 0, 5, "", $encode));
?>

実行結果。

バイト数
int(10)
int(15)
int(12)
文字数
int(10)
int(5)
int(6)
文字列の幅
int(10)
int(10)
int(9)
文字の切り出し(mb_substr)
string(8) "12345…"
string(18) "12345…"
string(12) "11223…"
文字の切り出し(mb_strimwidth)
string(7) "1234…"
string(9) "12…"
string(8) "112…"

しかし、mb_strimwidthの丸め幅に半角5文字を指定しているのに半角4文字っぽい結果になってるな。何でだろう。
もしかして、指定する丸め幅に、…の幅が入っているんだろうか。だとしても「…」は文字幅2だからおかしいな。


以上、メモでした。


せんでん

1タップで写真共有tottepost
カメラのついたiPad/iPhoneで撮った写真を、その場でFacebook/Mixi/DropboxなどのサービスにアップロードできるtottepostというiOSアプリを開発しています!詳しくは、iTunes App Storeをご覧ください。


ご購入はこちら!
1タップで写真共有 - tottepost - ISHITOYA Kentaro

*1:切り上げで123あい45う…でもいい

*2:恐らくシステムデフォルトが、iso-8859-1,latin1文字コードになっている