【PHP】対応する mb_xxxx がない文字列関数相当の動作をするマルチバイト文字列対応コードの例

  • 2022年8月2日
  • 2022年8月5日
  • PHP

 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
print print
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 にかけるとしばしば折り返し地点で文字化けしたり、期待通りに折り返してくれなかったりします。

PHP: wordwrap – Manual

 これをマルチバイト対応にするには次の様にします。


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

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG