いまさらの php で連続するスペースを一つに
preg_replace()
いまさらのだけれど、php で文字列中の連続するスペースを一つにまとめるときの常套手段は、正規表現を使う preg_replace だと思う。しかるに、どうも正規表現は遅いという規定概念があって、ほかにもっと速い処理の方法はないものだろうか、と考えたりしてた。意外と、for loop で一文字づつチェックしたほうが速かったりはしないのだろうか?などと。
ちなみに preg_replace だとこんな具合で良いんだと思う。
正規表現における、\s は s が小文字の場合、空白文字( 半角スペース、\t、\n、\r、\f )すべてを指定でき、その後の + はその前の文字が少なくとも一つ以上連続するという指定なので、半角のスペースが一つあるか2つ3つ連続してるという文字列にマッチするということになる。
結論から先に言ってしまうと、なんのことはない、普通に preg_replace を使うのが安定して速い。
いや、でもわずかでも速い可能性を追求したい!って場合は、ちょっとややこしいけれど、こんな具合。
*スペースをまとめるだけで全文が1つの文字列のままで良い場合、連続するスペース(何の文字でも同じことではある)の数が4個以下と保証されるなら、strtr の方が高速。( 連続する数が2個までと保証されるなら迷わず str_replace を使うのは言うまでもないこと・・・ strtr でもほぼ同じ。)
*スペースをまとめて、explode などでそのスペースで分割して配列にしたデータが必要なら、初めから explode を使った方が当然のことながら高速。
組み込み関数 str_replace
他の方法はと、つるつる脳みそをフル稼働させて考えついたのが2つ。
explode でスペースで分割して配列にし、空の要素を排除する方法と、あたりまえに文字列を一文字づつ検査していく方法。そしてあと2つ、これらは比較対象としてはずせない組み込み関数の str_replace を使用したものと strtr を使ったもの。
まずは組み込み関数の str_replace を使った方法から。文字列置き換えにおいては最も一般的と思われるし( 変換前と後で文字数がかわらなければ strtr の方が速かったはずではあるけれど )、やはり比較対象としてこれははずせない。ただ、連続するスペースの数を柔軟に指定することはできないのでちょっと工夫が必要ではあるし、対応できる連続数も限定的となる。ただ単に、検索対象にスペースが2個のもの、3個のもの、4個のものと言う具合に並べても意味がない。例えば、見やすくスペースではなく *で考えると。下のようにすると3個の部分も連続している * が残ってしまっている。
これは str_replace は検索対象を複数指定された場合に、その検索対象の数だけ順にループするように変換していくからである。まずは1つ目の検索対象を全文において変換し、そして2つ目をまた全文において変換・・・、といった具合。ゆえに一回目の変換が終えた時点で、3つ連なっていた * は2つになっているため(2個が1個に変換され残った1個とあわせて2個)、二回目の変換の ’***’ は意味をなさないということである。ということでこれは、検索対象の3個と2個の順番を逆にすることで対応できるし、ちょっとへんてこな感じではあるけれど、二度、同じ 2個 を検索対象にすることでも対応できる。
どちらにしても、2回まわせば、連続する4個までは対応できるということになるし、どちらも速度的にもあまりかわらない。しかし、これは連続する数が増えてくると対応できる数に差が出てくる。
と、いう具合で一度目の変換に3個を指定したほうが、対応する連続する文字数が多くなる。ただ、これはいろいろやってみたけれど、もっと良い組み合わせ、いや、方法自体があるのかもしれない。
組み込み関数 strtr
お次は、同じく組み込み関数の strtr。
strtr は str_replace よりも高速ということは頭にあった。ただ、文字列を消去することには使えないとか(これは誤りでありました)思い込んでいたもので、一文字を別の一文字に変換することぐらいにしか使ったことがなかった。でも、改めて調べてみると、ほとんど知らなかっただけということがわかった。
と、いうことで改めて strtr。
この strtr 、引数の渡し方で挙動が全く異なる。
まずは、引数を3つ渡したとき。たとえば、第2引数に文字列を指定したとしても、検索対象はその文字列の各一文字づつであり、第3引数に渡された文字列の、第2引数と対応する順番の各一文字が置換される文字となる。そして、第2、第3で文字数が異なる場合は、多い方の文字で余分な文字が無視される。故に、この引数3つ渡しの方法では、文字を消去することができないというわけなのだ。実際にやってみたほうがわかりやすい。
もう一つは、引数を2つで渡す。第2引数には、連想配列で検索対象と置換文字列を指定する。こちらの動作は str_replace に似てる。一文字づつが検索対象ではなく、文字列として検索される。検索対象文字列と置換文字列の文字数は違ってもいい。あと、ちょっとくせがあり、連想配列で指定した キー の文字数が多いものから変換され、その変換された文字列がさらに置換される対象にはならないとのこと。置換文字列には空文字も指定できるので文字の消去もできる。そして、この関数が最も効率的に働くのは、すべての キー が同じ長さである場合です、とある。
strtr は引数を3つ渡す一文字置換の方式の方は高速だった。2つ渡しの方は、挙動も str_replace と似ているが、検索文字列指定が1つだけの場合、速度でもほとんど同じ数字が出ていた。まぁ、違うのは複数の置換文字列指定がある場合に、置換済みの文字列がさらに置換される対象になるか否かというところ。
explode + array_filter + array_merge
そして explode を使った方法。
explode で 半角スペースでもって文字列を分割して配列にいれる。スペースが連続していた場所は、値が空の要素となっている。それを array_filter で削除する。array_filter は、コールバックを指定しなければ、引数で渡した配列で空のデータがあればそれらを全て消去してくれるというのがデフォルト。まさにここで使うのにうってつけってわけ。ただ、空のデータが消去された key は間抜け状態になっている。それを array_values を使って key を連続した番号に整えるって具合。これは array_merge でもできるし、どちらもパフォーマンス的にはほぼ変わらなかった。で、こんな感じか。
そして最後に 文字列のインデックス アクセス を使って当たり前のように文字を一つづつチェックしていく方法。その文字がスペースならフラグを true にして、次の文字がスペースであり、かつフラグが true なら新しく作っている文字列には入れないってだけのこと。
さぁ、どれが速いのだろう。ベンチをする前は、strtr が本命。一回転なら str_replace もいい勝負なんだろうけれど、回転数が多くなってくるとどうなるか。無制限に対応するなら、なんとなくは、組み込み関数の explode が一番のような気がしてた。でも、どうかな、explode はそんなに速くはなかったような気もするしで for roop index access だろうか。
ということで、ベンチです。
使った文字列はアクセスログのデータ。そう、このアクセスログのデータをいろいろと使いたくてスペースを一つにまとめる必要があったわけね。php を学び始めた頃に作って、そのまま使い続けているアクセスログのプログラムなんだけれど、その当時、なぜか、各データを区切るのに複数のスペースを使っているのが問題の根源。今になって、その複数のスペースをいかに速く処理するかということで楽しんでいる?ってことですな。
恥をさらしつつ、そのアクセスログのデータというのはこんな具合。見ての通り applebot のアクセスログ。こういうログデータが 1000行ほどあるログファイルをつかった。
で、このファイルを読み込んだ後に、各行にランダムに連続するスペースの文字列を1つ、付加した。と、いうのはこのログの文字列にはスペースが連続しても2つまでと決まっているから。テスト用により多くの連続する文字列を存在させたかったわけ。まぁ、今にして思えば連続数を固定したほうがよかったかも。ちなみにテスト環境は、
Windows10 Pro 64
Apache/2.4.48 (Win64)
PHP/8.1.8
Firefox Developer Edition 103.0b9
strtr と str_replace においてはそれぞれに対応できる連続数に制限があるので、スペースの連続数によっては、ひとつにまとめることはできず不完全な結果状態。で、スペースの連続数の多さで処理速度に影響がありそうなのが preg_replace 以外全部。しかしやってみた結果、preg_replace と strtr もあまりその影響を感じさせず、安定した数値が出ていたと思う。
存在する連続するスペースの数で影響がありそうなことと、str_replace の場合、一回目の検索対象を連続2個にするか3個にするかでも、処理回数に違いがでて処理速度にも影響がありそうなのでその辺を変えてテストしてみた。合計7回やって、最速、最遅のデータを抜いた5個のデータの平均を出した。表題で rp2 となっているのは str_replace で一回目の検索対象が2個ということ。3なら3個。それにハイフンで続く値は対応できる連続文字数。sp は付加した連続するスペースの最大連続数(付加したスペース連続数はランダムなので行によってさまざま)。
rp2 sp16 | rp2 sp4 | rp3 sp16 | rp3 sp4 | rp3 sp5 | |
preg_replace | .00095 | .00103 | .00090 | .00094 | .00091 |
explode | .00137 | .00127 | .00141 | .00120 | .00128 |
for loop | .01466 | .01436 | .01462 | .01427 | .01404 |
str_replace 4回 rp2-16 rp3-22 | .00159 | .00107 | .00177 | .00116 | .00122 |
str_replace 3回 rp2-8 rp3-10 | .00133 | .00095 | .00144 | .00103 | .00108 |
str_replace 2回 rp2-4 rp3-4 | .00103 | .00082 | .00107 | .00082 | .00092 |
str_replace 1回 rp2-2 rp3-無 | .00061 | .00051 | .00064 | .00049 | .00053 |
strtr 3space | .00079 | .00079 | .00072 | .00070 | .00074 |
strtr 4space | .00090 | .00087 | .00081 | .00087 | .00084 |
strtr 5space | .00110 | .00110 | .00094 | .00107 | .00099 |
strtr 6space | .00124 | .00119 | .00106 | .00114 | .00104 |
preg_replace と explode は連続する文字列の個数に制限はなく、str_replace の rp3 で最大22個まで、strtr では最大6個までというテストなので、それを考えると最速なのは、なんと、予想に反して preg_replace ということになるだろう。explode もいい勝負だったけれど、少しづつ遅く、for loop は桁が違うほどの遅さで・・・、あれっ!
ただし、explode においては結果が配列の状態になっているので、まとまった一つの文字列にしたい場合は、さらに implode なんかを使うことになる。よって間違いなくこれよりさらに遅くなる。
と、いうことで、過去の思い込みはあっさりと捨て去って、常套手段の preg_replace を普通に使っているのが一番速いということがわかった。正規表現はこんなにも速くなっているのだなぁと。
そして、限定的ではあるけれど、連続する文字の数が4以下と保証できるならば、strtr が最速ということになる。連続数がそれ以上の場合は、もう preg_replace を使うしかない。
ではあるのだけれど、今回のアクセスログのデータを利用したい、というような場合においてはちょっと話が違ってくるかもしれない。そもそもスペースを一つにまとめたいというのは、そのスペースで各データをバラバラに分割したいからにほかならない。と、いうことは、その後にデータをバラバラにするという処理が必然的にあるのである。まぁ、後づけで言うと、そもそも explode を使ってどうぜバラバラにするのなら!ということで思いついたことではある。
なので explode の場合は、すでにデータは分割ずみであり、一方の preb_replace のほうは、一つの文字列のままなのでこれから分割しなくてはならない。で、その分の処理が上のコードにおいてコメントアウトされている部分となる。
preg_replace の方は、この処理で explode の処理が追加されるわけで、そうなるとコメントアウトを外してベンチにかけてみると、当然のこと、逆転ということになる。
rp2 sp16 | rp2 sp16 array | |
preg_replace | .00095 | .00164 |
explode | .00137 | .00149 |
for loop | .01466 | .01449 |
str_replace 4回 rp2-16 rp3-22 | .00159 | .00231 |
str_replace 3回 rp2-8 rp3-10 | .00133 | .00211 |
str_replace 2回 rp2-4 rp3-4 | .00103 | .00186 |
str_replace 1回 rp2-2 rp3-無 | .00061 | .00138 |
strtr 3space | .00079 | .00148 |
strtr 4space | .00090 | .00156 |
strtr 5space | .00110 | .00167 |
strtr 6space | .00124 | .00182 |
単純にその処理だけのことなら、preg_replace を使えばいいのであるけれど、その後、どういった形式でデータが欲しいかということも合わせ考えると、この場合は explode を使ったほうが結果速いということになる。僅かな差ではあるのだけれど・・・。まぁ、もともとのログを保存するときに、むやみにスペースなど使わないで、chr(1) などを使っていれば、事は簡単に済んでいたことなのではある。
と、いいつつも・・・。
スペースでデータを分割して、個々にデータを扱えるようにしたい・・・と。
そして、正規表現の preg_replace が実は高速に動くようになっている・・・。
と、いうことならもう一つ思いついたものがある。preg_match_all は使えないだろうか。preg_match_all は3つめの引数に変数を指定すれば、マッチしたものをその変数で取得できる。スペースを頭にしてデータをマッチさせれば、3つも関数を通さなくてはならない explode に対して、これだけで済みそうな preg_match_all はより高速なのではないか?
と、いうことならさっそくやってみるしかない。
下のような具合でできたのではあるが、少し下準備が必要ではある。スペースを頭にして、という正規表現のパターンなので、元データの頭にスペースを付ける必要がある。それをしないと先頭のデータを取得できない。前記のベンチのコードに下のコードを入れ込んでベンチしてみた。
結果は、まぁ、残念なことに explode よりも遅かったのである。それどころか preg_replace + explode + list より速くもなかった。if 文をなくしたりといろいろとやってみたが、あまり結果は変わらなかった。preg_replace とやってることはそんなに違わないように思えたのだけれど、でも、検索のパターンが少し複雑になるだけでも負荷が大きくなるだろうし、そのあたりなのか。
まぁ、思ったよりも手間と時間のかかった検証だったけれど、あまり処理速度的に速くなるような事はみつけることはできなかった。ただ、正規表現はもう十分に速く、使えるということがわかったし、strtr はこれから str_replace に変わってよく使うようになるであろうし、array_filter なんて関数の使い方も学べて楽しかったという気がする。array_filter は結構、使い所のある便利な関数なのではあるまいか。
Post : 2022/07/20 21:06
Talking
while(0)のような人生 さん、コメントありがとうございます。
なんとか少しでも速くできる処理がどこかにあるのではないかと、自分の書いたコードを見て、そんなことばかり考えてましたね。まぁ、今でもプラグインの事もあるのであまり変わらないのですけれど。
私はとりあえずpreg_replaceを使っておこうと心が決まりました。
非常に参考になりました。感謝いたします。
Comments feed
Trackback URL : https://strix.main.jp/wp-trackback.php?p=171449