Convertendo indentação com preg_replace (sem retorno de chamada)

Eu tenho um pouco de XML retornado pelo DOMDocument::saveXML() . Já está bastante recuado, com dois espaços por nível, assim:

    eee sd   

Como não é possível configurar o DOMDocument (AFAIK) sobre o (s) personagem (s) de recuo, pensei que era possível executar uma expressão regular e alterar a indentação substituindo todos os pares de dois espaços em uma guia. Isso pode ser feito com uma function de retorno de chamada ( 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); 

Agora estou me perguntando se é possível fazer isso com uma function de retorno de chamada (e sem o e modificador (EVAL)). Algum feiticeiro regex com uma idéia?

Você pode usar \G :

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

Fez alguns benchmarks e obteve os seguintes resultados no Win32 com o PHP 5.2 e 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 

Surpreendendo que o retorno de chamada é mais rápido que o \G no PHP 5.4 (embora isso dependa dos dados, \G é mais rápido em alguns outros casos).

Para \G /^ |\G /m é usado, e é um pouco mais rápido que /(?:^|\G) /m . /(?>^|\G) /m é ainda mais lento do que /(?:^|\G) /m . /u , /S , /X não afetaram o desempenho \G significativamente.

O while substituição é mais rápido se a profundidade for baixa (até cerca de 4 indentações, 8 espaços, no meu teste), mas depois fica mais lento à medida que a profundidade aumenta.

O código a seguir foi usado:

     eee   sd   deep deeper still deepest !    _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"; ?> 

A primeira solução simplista primeiro vem à mente:

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

Mas eu suponho que você gostaria de limitar a substituição apenas para os principais espaços em branco . Para esse caso, sua solução atual parece muito limpa para mim. Dito isto, você pode fazê-lo sem um retorno de chamada ou o modificador e , mas você precisa executá-lo recursivamente para fazer o trabalho assim:

 $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); 

Surpreendentemente, meu teste mostra que isso é quase duas vezes mais rápido do que o método de retorno de chamada. (Eu teria adivinhado o contrário.)

Note que Qtax possui uma solução elegante que funciona bem (eu dei meu +1). No entanto, meus benchmarks mostram que ele é mais lento do que o método de retorno de chamada original. Eu acho que isso é porque a expressão /(?:^|\G) /um não permite que o motor regex aproveite a otimização interna: “âncora no início do padrão” . O motor RE é forçado a testar o padrão em todas e cada uma das posições na cadeia de destino. Com as expressões de padrão que começam com a âncora ^ , o motor RE só precisa verificar no início de cada linha que lhe permite combinar muito mais rápido.

Excelente pergunta! +1

Adendo / Correção:

Devo me desculpar porque as declarações de desempenho que fiz acima estão erradas . Eu corri as regexes contra apenas um arquivo de teste (não representativo) que tinha principalmente abas no espaço em branco principal. Quando testado contra um arquivo mais realista com muitos espaços principais, meu método recursivo acima executa significativamente mais lento que os outros dois methods.

Se alguém estiver interessado, aqui está o script de referência que eu usei para medir o desempenho de cada regex:

test.php

  

O script acima usa a seguinte pequena function de benchmarking útil que escrevi há algum tempo:

benchmark.inc.php

  $funcname, 'msg' => $msg, 'nreps' => $nreps, 'time_total' => $time, 'time_func' => $t_func, 'result' => $result); } ?> 

Quando eu executar test.php usando o conteúdo de benchmark.inc.php , aqui estão os resultados que recebo:

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

Bottom line: Eu recomendaria usar o método Qtax.

Obrigado Qtax!