PHP にはしばしば mb_ から始まる文字列に関連する関数(以降 mb関数)があります。この mb とは Multi Byte の略であり、mb関数は日本語などのマルチバイト文字を適切に扱うための関数です。こういったマルチバイトに対応した関数は便利なのですが PHP に含まれる文字列操作関数のすべてに対応しているわけではありません。例えば str_shuffle、word_wrap です。そういった片方にだけあある関数、両方にある関数をPHP8.1時点のPHPドキュメントから集めた結果が次です。
文字列関数とマルチバイト文字列関数に存在する関数の比較
| 名前の共通部分 | 文字列関数名。存在しないなら- | マルチバイト文字列関数名。存在しないなら- |
| addcslashes | addcslashes | – |
| addslashes | addslashes | – |
| bin2hex | bin2hex | – |
| check_encoding | – | mb_check_encoding |
| chop | chop | – |
| chr | chr | mb_chr |
| chunk_split | chunk_split | – |
| convert_case | – | mb_convert_case |
| convert_cyr_string | convert_cyr_string | – |
| convert_encoding | – | mb_convert_encoding |
| convert_kana | – | mb_convert_kana |
| convert_uudecode | convert_uudecode | – |
| convert_uuencode | convert_uuencode | – |
| convert_variables | – | mb_convert_variables |
| count_chars | count_chars | – |
| crc32 | crc32 | – |
| crypt | crypt | – |
| decode_mimeheader | – | mb_decode_mimeheader |
| decode_numericentity | – | mb_decode_numericentity |
| detect_encoding | – | mb_detect_encoding |
| detect_order | – | mb_detect_order |
| echo | echo | – |
| encode_mimeheader | – | mb_encode_mimeheader |
| encode_numericentity | – | mb_encode_numericentity |
| encoding_aliases | – | mb_encoding_aliases |
| ereg | – | mb_ereg |
| ereg_match | – | mb_ereg_match |
| ereg_replace | – | mb_ereg_replace |
| ereg_replace_callback | – | mb_ereg_replace_callback |
| ereg_search | – | mb_ereg_search |
| ereg_search_getpos | – | mb_ereg_search_getpos |
| ereg_search_getregs | – | mb_ereg_search_getregs |
| ereg_search_init | – | mb_ereg_search_init |
| ereg_search_pos | – | mb_ereg_search_pos |
| ereg_search_regs | – | mb_ereg_search_regs |
| ereg_search_setpos | – | mb_ereg_search_setpos |
| eregi | – | mb_eregi |
| eregi_replace | – | mb_eregi_replace |
| explode | explode | – |
| fprintf | fprintf | – |
| get_html_translation_table | get_html_translation_table | – |
| get_info | – | mb_get_info |
| hebrev | hebrev | – |
| hebrevc | hebrevc | – |
| hex2bin | hex2bin | – |
| html_entity_decode | html_entity_decode | – |
| htmlentities | htmlentities | – |
| htmlspecialchars | htmlspecialchars | – |
| htmlspecialchars_decode | htmlspecialchars_decode | – |
| http_input | – | mb_http_input |
| http_output | – | mb_http_output |
| implode | implode | – |
| internal_encoding | – | mb_internal_encoding |
| join | join | – |
| language | – | mb_language |
| lcfirst | lcfirst | – |
| levenshtein | levenshtein | – |
| list_encodings | – | mb_list_encodings |
| localeconv | localeconv | – |
| ltrim | ltrim | – |
| md5 | md5 | – |
| md5_file | md5_file | – |
| metaphone | metaphone | – |
| money_format | money_format | – |
| nl2br | nl2br | – |
| nl_langinfo | nl_langinfo | – |
| number_format | number_format | – |
| ord | ord | mb_ord |
| output_handler | – | mb_output_handler |
| parse_str | parse_str | mb_parse_str |
| preferred_mime_name | – | mb_preferred_mime_name |
| – | ||
| printf | printf | – |
| quoted_printable_decode | quoted_printable_decode | – |
| quoted_printable_encode | quoted_printable_encode | – |
| quotemeta | quotemeta | – |
| regex_encoding | – | mb_regex_encoding |
| regex_set_options | – | mb_regex_set_options |
| rtrim | rtrim | – |
| scrub | – | mb_scrub |
| send_mail | – | mb_send_mail |
| setlocale | setlocale | – |
| sha1 | sha1 | – |
| sha1_file | sha1_file | – |
| similar_text | similar_text | – |
| soundex | soundex | – |
| split | – | mb_split |
| sprintf | sprintf | – |
| sscanf | sscanf | – |
| str_contains | str_contains | – |
| str_ends_with | str_ends_with | – |
| str_getcsv | str_getcsv | – |
| str_ireplace | str_ireplace | – |
| str_pad | str_pad | – |
| str_repeat | str_repeat | – |
| str_replace | str_replace | – |
| str_rot13 | str_rot13 | – |
| str_shuffle | str_shuffle | – |
| str_split | str_split | mb_str_split |
| str_starts_with | str_starts_with | – |
| str_word_count | str_word_count | – |
| strcasecmp | strcasecmp | – |
| strchr | strchr | – |
| strcmp | strcmp | – |
| strcoll | strcoll | – |
| strcspn | strcspn | – |
| strcut | – | mb_strcut |
| strimwidth | – | mb_strimwidth |
| strip_tags | strip_tags | – |
| stripcslashes | stripcslashes | – |
| stripos | stripos | mb_stripos |
| stripslashes | stripslashes | – |
| stristr | stristr | mb_stristr |
| strlen | strlen | mb_strlen |
| strnatcasecmp | strnatcasecmp | – |
| strnatcmp | strnatcmp | – |
| strncasecmp | strncasecmp | – |
| strncmp | strncmp | – |
| strpbrk | strpbrk | – |
| strpos | strpos | mb_strpos |
| strrchr | strrchr | mb_strrchr |
| strrev | strrev | – |
| strrichr | – | mb_strrichr |
| strripos | strripos | mb_strripos |
| strrpos | strrpos | mb_strrpos |
| strspn | strspn | – |
| strstr | strstr | mb_strstr |
| strtok | strtok | – |
| strtolower | strtolower | mb_strtolower |
| strtoupper | strtoupper | mb_strtoupper |
| strtr | strtr | – |
| strwidth | – | mb_strwidth |
| substitute_character | – | mb_substitute_character |
| substr | substr | mb_substr |
| substr_compare | substr_compare | – |
| substr_count | substr_count | mb_substr_count |
| substr_replace | substr_replace | – |
| trim | trim | – |
| ucfirst | ucfirst | – |
| ucwords | ucwords | – |
| utf8_decode | utf8_decode | – |
| utf8_encode | utf8_encode | – |
| vfprintf | vfprintf | – |
| vprintf | vprintf | – |
| vsprintf | vsprintf | – |
| wordwrap | wordwrap | – |
私的には意外と共通している関数が少なかったです。とはいえ文字列関数にのみ存在する関数は大半がそのままマルチバイトでも使える関数であり、マルチバイト文字列関数にのみ存在する関数は色々ですが preg と ereg の名前の違いや日本語特有の処理であったりと納得のものが大半です。この記事では両対応していて欲しかった3つの関数についてのマルチバイト対応コードを紹介します。
一つ目は wordwrap です。これは指定した文字数で文字列を分割する関数です。ただしこの文字数とは例によってバイト数であり、マルチバイト文字を wordwrap にかけるとしばしば折り返し地点で文字化けしたり、期待通りに折り返してくれなかったりします。
これをマルチバイト対応にするには次の様にします。
if (! function_exists('mb_wordwrap')) {
/**
* 日本語対応wordwrap. 指定した文字数で文字列を分割する。行の最大長を決めて整形することを目的に作成。
* @param string $str 入力文字列。分割対象の文字列。
* @param int $width 文字列を分割するときの文字数。新しい一行の長さ。
* @param string $break 分割に用いる文字。改行文字。
* @param string $originalDeliminator 元々分割に用いていた文字。改行文字。
* @return string 受け取った文字列を指定した長さで分割したものを返します
* @see wordwrap()
*/
function mb_wordwrap(string $str, int $width = 25, string $break = "\n", string $originalDeliminator = "\n"): string
{
/** @var string $ret 返り値を格納する変数 */
$ret = '';
// 行に分割
$rows = explode($originalDeliminator, $str);
foreach ($rows as $row) {
// 分割した結果の現在の行の長さをマルチバイト形式で取得
$row_len = mb_strlen($row);
// 分割した行を $width 毎に切り分け
$newRowLenIsWidth = [];
for ($i = 0; $i <= $row_len; $i += $width) {
$newRowLenIsWidth[] = mb_substr($row, $i, $width);
}
// 切り分けた行を構成していた文字列を切り分けた単位で行とする
$ret .= implode($break, $newRowLenIsWidth).$break;
}
return $ret;
}
}
$str = 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほ';
echo mb_wordwrap($str, 25);
/*
あいうえおかきくけこさしすせそたちつてとなにぬねの
はひふへほ
*/
echo wordwrap($str, 25, "\n", true);
/*
あいうえおかきく�
��こさしすせそた�
�つてとなにぬねの
はひふへほ
*/
マルチバイト対応の関数を文字数を数える時や切り分ける時に使って文字列操作をすると通常の文字列関数相当の挙動を再現できます。
二つ目は str_shuffle です。こちらもその実態は byte_shuffle とでもいうべきものであり、日本語を入れると文字化けします。これは次の様にしてマルチバイト文字対応版を作れます。
<?php
if (!function_exists('mb_str_shuffle')) {
/**
* 日本語対応 str_shuffle
* @param string $str 並び替え対象文字列
* @return string 並び替え結果文字列
*/
function mb_str_shuffle(string $str): string
{
$arr = mb_str_split($str);
shuffle($arr);
return implode("", $arr);
}
}
mt_srand(0);
echo mb_str_shuffle('アローラロコン')."\n";
echo mb_str_shuffle('アローラロコン')."\n";
echo mb_str_shuffle('アローラロコン')."\n";
echo mb_str_shuffle('アローラロコン')."\n";
/*
ンーロアコラロ
コアロラーロン
ンーロアコラロ
ンアーコロラロ
*/
適宜マルチバイト対応の関数を使うのが手間なら mb_str_split がおすすめです。mb_str_split を使ってマルチバイト文字の一文字単位で配列化して配列操作で処理を再現するというのは応用がしやすいです。
三つ目は trim です。これは文字列の先頭および末尾にあるホワイトスペースを取り除く関数です。ホワイトスペース相当の削除対象文字には任意の文字を指定できますがここがマルチバイト文字に対応していません。もしマルチバイト文字にを指定すると大体正常に動いて時々異常を起こします。どういうことかというとマルチバイト文字を構成するバイトの一部分に引っかかっただけでもそこが削除されるからです。全角スペースで先頭の日本語が壊れるコードは稀に見ます。
<?php
if (!function_exists('mb_trim')) {
/**
* 日本語対応 trim
* @param string $string トリム対象文字列
* @param string $characters トリムで除去する文字列
* @return string トリミング済み文字列
*/
function mb_trim(string $string, string $characters = " \t\n\r\0\x0B"): string
{
$strArr = mb_str_split($string);
$trimCharArr = mb_str_split($characters);
// 先頭から順に見ていく。削除対象文字が見つかれば削除。見つからなければ終了
foreach ($strArr as $i => $char) {
if(in_array($char, $trimCharArr, true)){
unset($strArr[$i]);
}else{
break;
}
}
// index を 0,1,2...に揃える
$strArr = array_values($strArr);
// 末尾から順に見ていく。削除対象文字が見つかれば削除。見つからなければ終了
$len = count($strArr);
for($i = $len-1; $i > 0; $i--){
if(in_array($strArr[$i], $trimCharArr, true)){
unset($strArr[$i]);
}else{
break;
}
}
return implode($strArr);
}
}
var_dump(trim(' あ い あ ',' '));
var_dump(mb_trim(' あ い あ ',' '));
/*
string(12) "�� い あ"
string(13) "あ い あ"
*/
先ほどの mb_str_shuffle 同様にマルチバイト文字を配列化して削っていく方式です。ltrim, rtrim を再現するならばループの片方を消せば再現になります。この方式ですとしばしば見る正規表現方式に比べて自由度を確保したまま DOS やインジェクションなどの問題が起きないのが利点です。代わりに全体を配列化して変数かする都合上速度やメモリ量にはあまり自信のない方式でもあります。少なくとも短い文字列をトリミングする点では便利です。
この様な感じで既存のマルチバイト関数を駆使すればマルチバイト関数に対応していない文字列関数の再現がわりとできます。速度面ではどうしても本来の文字列関数に大きく劣るのが欠点ですし、できれば PHP 組み込み関数に追加して欲しいものですがこういった関数を用意できると日本国内向けのサービスを作りやすいです。
余談ですが普通の日本語対応ならばここまでの記事の通りの対応でいい感じにできますが絵文字はそううまくいかない時が多いです。そういった時はGrapheme関数という文字列を見た目の長さで取り扱う関数を駆使する必要があります。こちらはマルチバイト文字列関数よりはるかに対応範囲が狭いですが、より期待通り動作します。
PHP: Grapheme 関数 – Manual