PHP 从固定格式字符串里解析数据最快的方式

PHP 从固定格式字符串里解析数据最快的方式

Chris Yue No Comment
Posts

为了提升 PHP M3u8 的解析速度,我在上一篇博客里评选出了 PHP 世界里判断是否由某字符串开头最快的冠军,这一次又要举行另外一个比赛了:从某个固定格式的字符串里解析出想要的数据最快的函数。

还是以 M3u8 格式举例子。如果我想从字符串 #EXT-X-VERSION:3 取出 3 这个版本号,可能大家最好想到的是利用正则来实现:

preg_match('/^#EXT-X-VERSION:(\d)$/', '#EXT-X-VERSION:3', $m);

echo $m[1]; // print "3"

不过,既然 PHP 也属于 C Family,也比较容易想到另外一个函数也可以实现同样的需求:

$m = sscanf('#EXT-X-VERSION:3', '#EXT-X-VERSION:%d');

echo $m[0]; // print "3"

从语法上来看,两者都还比较简单,sscanf 略胜一筹,但我们最关心的速度又如何呢?做个实验比一比:

<?php

$iter = 999999;

$f = '#EXT-X-VERSION:%d';
$p = '/^#EXT-X-VERSION:(\d)$/';
$s = '#EXT-X-VERSION:3';

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $m = sscanf($s, $f);
    $version = $m[0];
}

echo microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    preg_match($p, $s, $m);

    $version = $m[1];
}

echo microtime(true) - $start, PHP_EOL;

结果怎样大家可以在自己电脑上实验一番,在我的电脑上 sscanf 胜,但差距也不是很大,在一个数量级内。

另外我不得不补充一点,sscanf 用法还是很灵活的,比如在解析 #EXT-X-BYTERANGE:1000@500 时,因为 @ 以及它后面的参数是可以省略的,如果用 preg_match 来处理:

preg_match('/^#EXT-X-BYTERANGE:(\d+)(@(\d+))?$/', $line, $matches);
$length = (int) $matches[1];

if (!empty($matches[3])) {
    $offset = (int) $matches[3];
}

而使用 sscanf 的话:

list($length, $offset) = sscanf('#EXT-X-BYTERANGE:1000@500', '#EXT-X-BYTERANGE:%d@%d');

当没有 @500 时,$offset 自动就是 null,所以一行代码就可以搞定,就冲这点 sscanf 也加分不少。不过我们还是再来测试一下这种方式的速度对比:

<?php

$iter = 999999;

$f = '#EXT-X-BYTERANGE:%d@%d';
$p = '/^#EXT-X-BYTERANGE:(\d+)(@(\d+))?$/';
$s = '#EXT-X-BYTERANGE:1000';
// $s = '#EXT-X-BYTERANGE:1000@500';

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    list($length, $offset) = sscanf($s, $f);
}

echo microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    preg_match($p, $s, $matches);
    $length = (int) $matches[1];

    if (!empty($matches[3])) {
        $offset = (int) $matches[3];
    }
}

echo microtime(true) - $start, PHP_EOL;

结果依然是 sscanf 获胜。

对于解析 #EXT-X-VERSION:3 这种格式比较简单的,方法还有很多,比如利用 explode, substr, strstr 等函数,我打算再加赛一场,看看是否能跟 sscanf 一争高下。在看结果之前,不妨自己猜猜看谁是冠军:

<?php

$iter = 999999;

$f = '#EXT-X-BYTERANGE:%d';
$p = '/^#EXT-X-BYTERANGE:(\d+)$/';
$s = '#EXT-X-BYTERANGE:1000';

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    list($length) = sscanf($s, $f);
}

echo 'sscanf: ', microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    preg_match($p, $s, $matches);
    $length = (int) $matches[1];
}

echo 'preg_match: ', microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $length = (int) explode(':', $s)[1];
}

echo 'explode: ', microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $length = (int) substr(strstr($s, ':'), 1);
}

echo 'substr+strstr: ', microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $length = (int) substr(strrchr($s, ':'), 1);
}

echo 'substr+strrchr: ', microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $length = (int) substr(strpbrk($s, ':'), 1);
}

echo 'substr+strpbrk: ', microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $length = (int) substr($s, strpos($s, ':') + 1);
}

echo 'substr+strpos: ', microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $length = (int) substr($s, strlen($t) + 1);
}
 
echo 'substr+strlen: ', microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $length = (int) ltrim(strstr($s, ':'), ':');
}

echo 'ltrim+strstr: ', microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $length = (int) ltrim(strrchr($s, ':'), ':');
}

echo 'ltrim+strrch: ', microtime(true) - $start, PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $length = (int) ltrim(strpbrk($s, ':'), ':');
}

echo 'ltrim+strpbrk: ', microtime(true) - $start, PHP_EOL;

得益于上一届的冠军 strpos 出色的性能,strpos + substr 组合以大优势获得了冠军!substr + strrchr 组合获得亚军,substr + strstr 组合获得季军。不得不说 substr 还是很牛逼的,前三名它都有出镜。

2017-11-17 补充:今天又试验了 substr + strlen 的组合,以不大的优势超越了曾经的冠军获得了第一名!以上代码已经更新。

当然这些方法只能用在特殊情况,正常解析带格式的字符串还是得用回 preg_matchsscanf。事实上,sscanf 也是可以使用正则的,但要注意不是所有正则符号都能用,跟一般的正则还是有所区别:

$filename = sscanf ('a.jpg', '%[^.].jpg')[0];
// 注意这里如果格式参数写 '%s.jpg',结果是 "a.jpg" 而不是 "a"
// 因为 %s 必须得遇到空格类字符才会停止解析

sscanf 加正则的格式的方式与 preg_match 以及 sscanf 自身比一比:

$f2 = '#EXT-X-BYTERANGE:%[0-9]';

...

$start = microtime(true);
for ($i = 0; $i < $iter; ++$i) {
    $length = (int) sscanf($s, $f2)[0];
}

echo 'sscanf 2: ', microtime(true) - $start, PHP_EOL;

再猜猜看结果是什么??

公布结果:

带正则 sscanf 稍快于 preg_match,稍慢于不带正则的 sscanf

经过这次的比赛,再次告诉我们一个真理:正则能别用就别用啊。

PHP 从固定格式字符串里解析数据最快的方式 by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

微信赞赏码

如果觉得文章还不错,就请扫码鼓励一下作者吧
天使打赏人

发表评论

+ 50 = 58