Подтвердить что ты не робот

Преобразование отступов с preg_replace (без обратного вызова)

У меня есть часть XML, возвращаемая DOMDocument::saveXML(). Он уже довольно отступом, с двумя пробелами на уровне, например:

<?xml version="1.0"?>
<root>
  <error>
    <a>eee</a>
    <b>sd</b>
  </error>
</root>

Как невозможно настроить DOMDocument (AFAIK) на символ (-ы) отступа, я считал возможным запустить регулярное выражение и изменить отступ заменяя все пары с двумя пробелами на вкладку. Это можно сделать с помощью функции обратного вызова (Demo):

$xml_string = $doc->saveXML();
function callback($m)
{
    $spaces = strlen($m[0]);
    $tabs = $spaces / 2;
    return str_repeat("\t", $tabs);
}
$xml_string = preg_replace_callback('/^(?:[ ]{2})+/um', 'callback', $xml_string);

Теперь мне интересно, возможно ли это сделать без функции обратного вызова (и без e -modifier (EVAL)). Любые мастера регулярных выражений с идеей?

4b9b3361

Ответ 1

Вы можете использовать \G:

preg_replace('/^  |\G  /m', "\t", $string);

Были ли некоторые тесты и получили следующие результаты в Win32 с PHP 5.2 и 5.4:

>php -v
PHP 5.2.17 (cli) (built: Jan  6 2011 17:28:41)
Copyright (c) 1997-2010 The PHP Group
Zend Engine v2.2.0, Copyright (c) 1998-2010 Zend Technologies

>php -n test.php
XML length: 21100
Iterations: 1000
callback: 2.3627231121063
\G:       1.4221360683441
while:    3.0971200466156
/e:       7.8781840801239


>php -v
PHP 5.4.0 (cli) (built: Feb 29 2012 19:06:50)
Copyright (c) 1997-2012 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2012 Zend Technologies

>php -n test.php
XML length: 21100
Iterations: 1000
callback: 1.3771259784698
\G:       1.4414191246033
while:    2.7389969825745
/e:       5.5516891479492

Удивительно, что обратный вызов быстрее, чем \G в PHP 5.4 (хотя это зависит от данных, \G быстрее в некоторых других случаях).

Для \G /^ |\G /m используется и немного быстрее, чем /(?:^|\G) /m. /(?>^|\G) /m еще медленнее, чем /(?:^|\G) /m. /u, /S, /X не влияют на производительность \G.

Замена while выполняется быстрее, если глубина мала (до 4 отступов, 8 пробелов, в моем тесте), но затем становится медленнее по мере увеличения глубины.

Использовался следующий код:

<?php

$base_iter = 1000;

$xml_string = str_repeat(<<<_STR_
<?xml version="1.0"?>
<root>
  <error>
    <a>  eee  </a>
    <b>  sd    </b>         
    <c>
            deep
                deeper  still
                    deepest  !
    </c>
  </error>
</root>
_STR_
, 100);


//*** while ***

$re = '%# Match leading spaces following leading tabs.
    ^                     # Anchor to start of line.
    (\t*)                 # $1: Preserve any/all leading tabs.
    [ ]{2}                # Match "n" spaces.
    %mx';

function conv_indent_while($xml_string) {
    global $re;

    while(preg_match($re, $xml_string))
        $xml_string = preg_replace($re, "$1\t", $xml_string);

    return $xml_string;
}


//*** \G ****

function conv_indent_g($string){
    return preg_replace('/^  |\G  /m', "\t", $string);
}


//*** callback ***

function callback($m)
{
    $spaces = strlen($m[0]);
    $tabs = $spaces / 2;
    return str_repeat("\t", $tabs);
}
function conv_indent_callback($str){
    return preg_replace_callback('/^(?:[ ]{2})+/m', 'callback', $str);
}


//*** callback /e *** 

function conv_indent_e($str){
    return preg_replace('/^(?:  )+/me', 'str_repeat("\t", strlen("$0")/2)', $str);
}



//*** tests

function test2() {
    global $base_iter;
    global $xml_string;
    $t = microtime(true);

    for($i = 0; $i < $base_iter; ++$i){
        $s = conv_indent_while($xml_string);
        if(strlen($s) >= strlen($xml_string))
            exit("strlen invalid 2");
    }

    return (microtime(true) - $t);
}

function test1() {
    global $base_iter;
    global $xml_string;
    $t = microtime(true);

    for($i = 0; $i < $base_iter; ++$i){
        $s = conv_indent_g($xml_string);
        if(strlen($s) >= strlen($xml_string))
            exit("strlen invalid 1");
    }

    return (microtime(true) - $t);
}

function test0(){
    global $base_iter;
    global $xml_string;
    $t = microtime(true);

    for($i = 0; $i < $base_iter; ++$i){     
        $s = conv_indent_callback($xml_string);
        if(strlen($s) >= strlen($xml_string))
            exit("strlen invalid 0");
    }

    return (microtime(true) - $t);
}


function test3(){
    global $base_iter;
    global $xml_string;
    $t = microtime(true);

    for($i = 0; $i < $base_iter; ++$i){     
        $s = conv_indent_e($xml_string);
        if(strlen($s) >= strlen($xml_string))
            exit("strlen invalid 02");
    }

    return (microtime(true) - $t);
}



echo 'XML length: ' . strlen($xml_string) . "\n";
echo 'Iterations: ' . $base_iter . "\n";

echo 'callback: ' . test0() . "\n";
echo '\G:       ' . test1() . "\n";
echo 'while:    ' . test2() . "\n";
echo '/e:       ' . test3() . "\n";


?>

Ответ 2

На первый взгляд приходит следующее упрощенное решение:

$xml_string = str_replace('  ', "\t", $xml_string);

Но я предполагаю, что вы хотели бы ограничить замену только ведущим пробелом. В этом случае ваше текущее решение выглядит довольно чистым для меня. Тем не менее, вы можете сделать это без обратного вызова или модификатора e, но вам нужно запустить его рекурсивно, чтобы выполнить задание следующим образом:

$re = '%# Match leading spaces following leading tabs.
    ^                     # Anchor to start of line.
    (\t*)                 # $1: Preserve any/all leading tabs.
    [ ]{2}                # Match "n" spaces.
    %umx';
while(preg_match($re, $xml_string))
    $xml_string = preg_replace($re, "$1\t", $xml_string);

Удивительно, но мое тестирование показывает, что это почти в два раза быстрее, чем метод обратного вызова. (Я бы догадался об обратном.)

Обратите внимание, что Qtax имеет элегантное решение, которое отлично работает (я дал ему свой +1). Тем не менее, мои тесты показывают, что он медленнее исходного метода обратного вызова. Я думаю, это связано с тем, что выражение /(?:^|\G) /um не позволяет механизму регулярных выражений воспользоваться преимуществами внутренней оптимизации "привязка в начале шаблона". Двигатель RE вынужден проверять шаблон на каждую позицию в целевой строке. С образцовыми выражениями, начинающимися с якоря ^, движок RE должен только проверять в начале каждой строки, что позволяет ему быстрее соответствовать.

Отличный вопрос! +1

Добавление/Исправление:

Я должен извиниться, потому что заявления о производительности, которые я сделал выше, ошибочны. Я запускал регулярные выражения только для одного (не репрезентативного) тестового файла, который имел в основном вкладки в ведущем пробеле. При тестировании на более реалистичный файл с большим количеством ведущих пробелов мой рекурсивный метод выше выполняется значительно медленнее, чем два других метода.

Если кому-то интересно, вот эталон script Я использовал для измерения производительности каждого регулярного выражения:

test.php

<?php // test.php 20120308_1200
require_once('inc/benchmark.inc.php');

// -------------------------------------------------------
// Test 1: Recursive method. (ridgerunner)
function tabify_leading_spaces_1($xml_string) {
    $re = '%# Match leading spaces following leading tabs.
        ^                     # Anchor to start of line.
        (\t*)                 # $1: Any/all leading tabs.
        [ ]{2}                # Match "n" spaces.
        %umx';
    while(preg_match($re, $xml_string))
        $xml_string = preg_replace($re, "$1\t", $xml_string);
    return $xml_string;
}

// -------------------------------------------------------
// Test 2: Original callback method. (hakre)
function tabify_leading_spaces_2($xml_string) {
    return preg_replace_callback('/^(?:[ ]{2})+/um', '_callback', $xml_string);
}
function _callback($m) {
    $spaces = strlen($m[0]);
    $tabs = $spaces / 2;
    return str_repeat("\t", $tabs);
}

// -------------------------------------------------------
// Test 3: Qtax elegantly simple \G method. (Qtax)
function tabify_leading_spaces_3($xml_string) {
    return preg_replace('/(?:^|\G)  /um', "\t", $xml_string);
}

// -------------------------------------------------------
// Verify we get the same results from all methods.
$data = file_get_contents('testdata.txt');
$data1 = tabify_leading_spaces_1($data);
$data2 = tabify_leading_spaces_2($data);
$data3 = tabify_leading_spaces_3($data);
if ($data1 == $data2 && $data2 == $data3) {
    echo ("GOOD: Same results.\n");
} else {
    exit("BAD: Different results.\n");
}
// Measure and print the function execution times.
$time1 = benchmark_12('tabify_leading_spaces_1', $data, 2, true);
$time2 = benchmark_12('tabify_leading_spaces_2', $data, 2, true);
$time3 = benchmark_12('tabify_leading_spaces_3', $data, 2, true);
?>

Вышеупомянутый script использует следующую удобную небольшую функцию бенчмаркинга, которую я написал некоторое время назад:

benchmark.inc.php

<?php // benchmark.inc.php
/*----------------------------------------------------------------------------
 function benchmark_12($funcname, $p1, $reptime = 1.0, $verbose = true, $p2 = NULL) {}
    By: Jeff Roberson
    Created:        2010-03-17
    Last edited:    2012-03-08

Discussion:
    This function measures the time required to execute a given function by
    calling it as many times as possible within an allowed period == $reptime.
    A first pass determines a rough measurement of function execution time
    by increasing the $nreps count by a factor of 10 - (i.e. 1, 10, 100, ...),
    until an $nreps value is found which takes more than 0.01 secs to finish.
    A second pass uses the value determined in the first pass to compute the
    number of reps that can be performed within the allotted $reptime seconds.
    The second pass then measures the time required to call the function the
    computed number of times (which should take about $reptime seconds). The
    average function execution time is then computed by dividing the total
    measured elapsed time by the number of reps performed in that time, and
    then all the pertinent values are returned to the caller in an array.

    Note that this function is limited to measuring only those functions
    having either one or two arguments that are passed by value and
    not by reference. This is why the name of this function ends with "12".
    Variations of this function can be easily cloned which can have more
    than two parameters.

Parameters:
    $funcname:  String containing name of function to be measured. The
                function to be measured must take one or two parameters.
    $p1:        First argument to be passed to $funcname function.
    $reptime    Target number of seconds allowed for benchmark test.
                (float) (Default=1.0)
    $verbose    Boolean value determines if results are printed.
                (bool) (Default=true)
    $p2:        Second (optional) argument to be passed to $funcname function.
Return value:
    $result[]   Array containing measured and computed values:
    $result['funcname']     : $funcname - Name of function measured.
    $result['msg']          : $msg - String with formatted results.
    $result['nreps']        : $nreps - Number of function calls made.
    $result['time_total']   : $time - Seconds to call function $nreps times.
    $result['time_func']    : $t_func - Seconds to call function once.
    $result['result']       : $result - Last value returned by function.

Variables:
    $time:      Float epoch time (secs since 1/1/1970) or benchmark elapsed secs.
    $i:         Integer loop counter.
    $nreps      Number of times function called in benchmark measurement loops.

----------------------------------------------------------------------------*/
function benchmark_12($funcname, $p1, $reptime = 1.0, $verbose = false, $p2 = NULL) {
    if (!function_exists($funcname)) {
        exit("\n[benchmark1] Error: function \"{$funcname}()\" does not exist.\n");
    }
    if (!isset($p2)) { // Case 1: function takes one parameter ($p1).
    // Pass 1: Measure order of magnitude number of calls needed to exceed 10 milliseconds.
        for ($time = 0.0, $n = 1; $time < 0.01; $n *= 10) { // Exponentially increase $nreps.
            $time = microtime(true);            // Mark start time. (sec since 1970).
            for ($i = 0; $i < $n; ++$i) {       // Loop $n times. ($n = 1, 10, 100...)
                $result = ($funcname($p1));     // Call the function over and over...
            }
            $time = microtime(true) - $time;    // Mark stop time. Compute elapsed secs.
            $nreps = $n;                        // Number of reps just measured.
        }
        $t_func = $time / $nreps;               // Function execution time in sec (rough).
    // Pass 2: Measure time required to perform $nreps function calls (in about $reptime sec).
        if ($t_func < $reptime) {               // If pass 1 time was not pathetically slow...
            $nreps = (int)($reptime / $t_func); // Figure $nreps calls to add up to $reptime.
            $time = microtime(true);            // Mark start time. (sec since 1970).
            for ($i = 0; $i < $nreps; ++$i) {   // Loop $nreps times (should take $reptime).
                $result = ($funcname($p1));     // Call the function over and over...
            }
            $time = microtime(true) - $time;    // Mark stop time. Compute elapsed secs.
            $t_func = $time / $nreps;           // Average function execution time in sec.
        }
    } else { // Case 2: function takes two parameters ($p1 and $p2).
    // Pass 1: Measure order of magnitude number of calls needed to exceed 10 milliseconds.
        for ($time = 0.0, $n = 1; $time < 0.01; $n *= 10) { // Exponentially increase $nreps.
            $time = microtime(true);            // Mark start time. (sec since 1970).
            for ($i = 0; $i < $n; ++$i) {       // Loop $n times. ($n = 1, 10, 100...)
                $result = ($funcname($p1, $p2));     // Call the function over and over...
            }
            $time = microtime(true) - $time;    // Mark stop time. Compute elapsed secs.
            $nreps = $n;                        // Number of reps just measured.
        }
        $t_func = $time / $nreps;               // Function execution time in sec (rough).
    // Pass 2: Measure time required to perform $nreps function calls (in about $reptime sec).
        if ($t_func < $reptime) {               // If pass 1 time was not pathetically slow...
            $nreps = (int)($reptime / $t_func); // Figure $nreps calls to add up to $reptime.
            $time = microtime(true);            // Mark start time. (sec since 1970).
            for ($i = 0; $i < $nreps; ++$i) {   // Loop $nreps times (should take $reptime).
                $result = ($funcname($p1, $p2));     // Call the function over and over...
            }
            $time = microtime(true) - $time;    // Mark stop time. Compute elapsed secs.
            $t_func = $time / $nreps;           // Average function execution time in sec.
        }
    }
    $msg = sprintf("%s() Nreps:%7d  Time:%7.3f s  Function time: %.6f sec\n",
            $funcname, $nreps, $time, $t_func);
    if ($verbose) echo($msg);
    return array('funcname' => $funcname, 'msg' => $msg, 'nreps' => $nreps,
        'time_total' => $time, 'time_func' => $t_func, 'result' => $result);
}
?>

Когда я запускаю test.php, используя содержимое benchmark.inc.php, вот результаты, которые я получаю:

GOOD: Same results.
tabify_leading_spaces_1() Nreps: 1756 Time: 2.041 s Function time: 0.001162 sec
tabify_leading_spaces_2() Nreps: 1738 Time: 1.886 s Function time: 0.001085 sec
tabify_leading_spaces_3() Nreps: 2161 Time: 2.044 s Function time: 0.000946 sec

Нижняя строка: я бы рекомендовал использовать метод Qtax.

Спасибо Qtax!