PHP remove_empty_tags

klamm

Chef
Teammitglied
ID: 20876
L
20 April 2006
13.647
1.367
Bin ich zu doof oder finde ich im ganzen Netz keine Lösung zum entfernen von leeren HTML-Tags? (Leerzeichen ist auch "leer"). Da das ja nicht bei allen Tags sinnvoll ist, würde ich der Function gerne die outer-Tags übergeben.

PHP:
$tags = Array('p','div');
remove_empty_tags($string,$tags);

// folgende Vorkommen sollten hier also weg-gestipped werden
// <div>
//
// </div>
// <p> </p>
// <p>  </p>
// <p><strong> </strong></p>
 
PHP:
function remove_empty_tags($string, $tags)
{
  // NOTE: $tags must not contain RegExp special chars!!
  $tags = implode('|', $tags);
  $string = preg_replace("~(.*)<($tags)>(\\s*)</\\2>(.*)~i", '\1\4', $string);
  return preg_replace("~(.*)<(?:$tags)\\s?\\/>(.*)~i", '\1\2', $string);
}
Ohne Gewehr (*peng*). Musst vermutlich noch dran rumbasteln.

edit:
was verbessert, trotzdem ungetestet
 
Thx schonmal ... aber Dein Code hat das Problem, was ich auch habe. Er geht nicht rekursiv nach innen. <p><b> </b></p> ist ja auch ein "leerer" <p>-Tag. Hm man könnte innerhalb der gefundenen Tags einfach strip_tags() anwenden vllt.?

Edit: Oder eher mit preg_replace_callback() arbeiten.
Und dann rekurvis wieder aufrufen. *bastel*
 
Thx schonmal ... aber Dein Code hat das Problem, was ich auch habe. Er geht nicht rekursiv nach innen.
Rekursiv = Baum aufbauen... und das dauert.
Hm man könnte innerhalb der gefundenen Tags einfach strip_tags() anwenden vllt.?
Das löscht ja auch volle Tags weg. Aber das hat mich auf ne Idee gebracht...

Wenn du mit preg_replace_callback() arbeitest, testest du innerhalb eines Tags, ob noch was übrig bleibt, wenn du alle Tags (nur <...>, nicht den Inhalt! "<p><b>FOO</b></p>" ist ja nichtleer) entfernst. Denk aber dran, Bilder und sowas vom Löschen auszunehmen.

edit:
Jo, preg_replace_callback() :yes: :ugly:
 
strip_tags() entfernt nur die Tags an sich, nicht den Inhalt.
Sollte also als Billigvariante funzen.

Edit: Darf ich fragen, was Du mit dieser Zeile vorhattest?
PHP:
return preg_replace("~(.*)<(?:$tags)\\s?\\/>(.*)~i", '\1\2', $string);
 
Also die GANZ billig Variante ist Deine ...
PHP:
preg_replace("~(.*)<($tag)>(\\s*)</\\2>(.*)~i", '\1\4', $string);
Problem ist nur, dass man im Prinzip mit dem ersten (innersten) Tag anfangen, und danach noch X-Mal aufrufen müsste, damit alles beseitigt ist.

<p><b><strong></strong></b></p>

Muss also Array('strong','b','p') sein ... dann genügt 3x aufrufen.

Die oberbillig-Variante wäre, es pauschal (3*max_verschachtelungen) aufzurufen. Ansonsten hab ich grad an ner Regexp gebastelt, die mir den innermost-genesteten Tag beschafft, bis ich dann gemerkt habe, dass ich ja den outermost-Tag brauche. :ugly:

Für den strip_tags callback brauch im Prinzip sowas
<p><p><b><strong></strong></b></p></p>
grün = *
 
Lukas Spaghetti-Variante
NICHT NACHMACHEN!!!

PHP:
// leere tags (auch verschachtelte) entfernen
// ALLE in frage kommenden Tags müssen angegeben werden
function remove_empty_tags($string,$tags){
   $anz = count($tags); // anzahl tags
   $nest = 3; // max verschachtelung leerer tags
   $tags = implode('|', $tags);
   for($i=1;$i<=$anz*$nest;$i++)
       $string = preg_replace("~(.*)<($tags)>( |\\x00|\\xa0|\\s)*</\\2>(.*)~i", '\1\4', $string);
   return $string;
}

// aufruf
$string = "<p><p><b><strong>    <p></p>  </strong></b></p></p>";
$tags = Array('p','strong','b');
echo remove_empty_tags($string,$tags);
 
Ich habe mal einen Vorschlag erstellt, bei dem die Verschachtelungstiefe unbegrenzt ist. Es werden immer alle von innen nach außen entfernt.

PHP:
// Leere Tags entfernen
function remove_empty_tags ($string, $tags) {
    $p_o_tag = '<('.implode('|', $tags).')(\s.*[^\/])?>';
    $p_empty = '( |\x00|\xa0|\s)*';
    $p_cl_tag = '<\/\\1>';
    while (
        preg_match('/'.$p_o_tag.$p_empty.$p_cl_tag.'/', $string)
    ) {
        $string =
            preg_replace(
                '/'.$p_o_tag.$p_empty.$p_cl_tag.'/', '', $string
            );
    }
    return $string;
}

// Aufruf
$string = "<p><p><b><strong>    <p></p>  </strong></b></p></p>";
$string = "<div>"{$string}"</div>";
$tags = Array('p', 'strong', 'b');
echo remove_empty_tags($string, $tags); // ==> '<div>""</div>'
 
Ah super. Macht zwar jedesmal einen preg_match() mehr, aber dafür ggf. weniger oft unnötige Durchgänge. Edit: Mach noch /i rein ... damit <tag> und <TAG> funzt.

Edit2:
Ah nice, der kann auch <p class="bla"></p> handeln.
 
Verbesserungsvorschlag:
PHP:
$p_o_tag = '<('.implode('|', $tags).')([^\/])?>';
Ob vor der spitzen Klammer-zu noch genau ein Leerzeichen und danach beliebig viele andere Zeichen kommen, ob du gleich auf besagte Klammer wartest, is egal.

Liest sich leichter und is vielleicht auch ne Femtosekunde schneller :mrgreen:
 
Verbesserungsvorschlag:
PHP:
$p_o_tag = '<('.implode('|', $tags).')([^\/])?>';
Öhm aber das .* darf nich raus, oder? Nur das \s ...

Edit: Könnte man das if(preg_match) -> preg_replace nicht vereinen?
Billige Idee für "do_solange_wie_preg_replace_was_macht"

PHP:
do{
$string_old = $string;
$string =  preg_replace(              '/'.$p_o_tag.$p_empty.$p_cl_tag.'/', '', $string
            );
    }
while($string_old!=$string);
 
Ahh... jetzt gesehen, ich hab das [^\/] als [^\/]* gelesen.
PHP:
$p_o_tag = '<('.implode('|', $tags).')([^\/])*>';
So müsste es einfach sein und dasselbe tun, wenn ich das richtig sehe :think:
Erst spitze Klammer öffnen, danach den Tag, danach, alles außer Slash, bis zur spitzen Klammer-zu.
 
Jupp, sollte er nicht sein :-? Is aber bei B2T's Variante auch schon der Fall.
 
Ist der Regex dann nicht greedy?:-?
Dehsalb will ich zumindest vermeiden, dass im Prinzip 2x der gleiche Ausdruck ausgewertet werden muss bei jedem Durchgang ... (match und replace). Oder is PHP (oder sonstwer) so schlau und cached das, sofern der String sich nicht geändert hat?
 
Dem RegExp kann auf jeden Fall der Modifier S nicht schaden.

Und als weitere Optimierung würde ich beim preg_match() die Treffer in einer Variable speichern, um sie im weiteren Schritt mittels str_replace() zu ersetzen.

[edit]

Achja, $p_empty muss ja nicht weiter verarbeitet werden, also kann man da auch das Capturen weglassen:
PHP:
    $p_empty = '(?: |\x00|\xa0|\s)*';
Analog die zweite Capturegroup bei $p_o_tag:
PHP:
    $p_o_tag = '<('.implode('|', $tags).')(?:\s.*[^\/])?>';
 
Zuletzt bearbeitet:
Vielen Dank für Eure Anmerkungen. Habe soweit wie möglich alles an Feedback berücksichtigt. Hier das Ergebnis:

PHP:
// Leere Tags entfernen
function remove_empty_tags ($string, $tags) {
    $p_o_tag = '<('.implode('|', $tags).')(?:\s[^>]*[^\/])?';
    $p_o_tag_short_tag = '\/>';
    $p_o_tag_long_tag = '>';
    $p_empty = '(?: |\x00|\xa0|\s)*';
    $p_cl_tag = '<\/\\1>';
    $pattern =
        $p_o_tag
        .'(?:'.$p_o_tag_short_tag
        .'|'.$p_o_tag_long_tag.$p_empty.$p_cl_tag.')';
    while (
        $string !=
            ($val = preg_replace('/'.$pattern.'/iS', '', $string))
    ) {
        $string = $val;
    }
    return $string;
}

// Aufruf
$string = "<p><p><b><strong>    <p></p><p />  </strong></b></p></p>";
$string = "<div>"{$string}"</div>";
$tags = array('p', 'strong', 'b');
echo remove_empty_tags($string, $tags)."\n";

Im einzelnen:
Ah, ok, Denkfehler.Leere XML-Tags entfernen: <p /> und <div/> sind auch leere Tags.
Habe Pattern entsprechend erweitert.
Edit: Mach noch /i rein ... damit <tag> und <TAG> funzt.
erledigt.
Edit: Könnte man das if(preg_match) -> preg_replace nicht vereinen?
Billige Idee für "do_solange_wie_preg_replace_was_macht"

PHP:
do{
$string_old = $string;
$string =  preg_replace(              '/'.$p_o_tag.$p_empty.$p_cl_tag.'/', '', $string
            );
    }
while($string_old!=$string);
Ja, das geht mit der Ausnutzung des Returnwerts des Zuweisungsoperators. Eingebaut bzw. umgebaut.
PHP:
$p_o_tag = '<('.implode('|', $tags).')([^\/])*>';
So müsste es einfach sein und dasselbe tun, wenn ich das richtig sehe :think:
Erst spitze Klammer öffnen, danach den Tag, danach, alles außer Slash, bis zur spitzen Klammer-zu.
Habe ich vereinfacht. Klammern braucht man dann aber nicht mehr um das [^\/]*. Nein, das tut es nicht. Das \s ist sehr wohl essentiell. :ugly:
Ist der Regex dann nicht greedy?:-?
Im Prinzip stolpert er über seine Gier, aber ich habe aus dem [^\/]* ein [^\/>]* gemacht, um sicher zu gehen.
Dem RegExp kann auf jeden Fall der Modifier S nicht schaden.
Modifier hinzugefügt.
Und als weitere Optimierung würde ich beim preg_match() die Treffer in einer Variable speichern, um sie im weiteren Schritt mittels str_replace() zu ersetzen.
Entfällt. Durch das Wegfallen von preg_match() erübrigt sich das.
[edit]

Achja, $p_empty muss ja nicht weiter verarbeitet werden, also kann man da auch das Capturen weglassen:
PHP:
    $p_empty = '(?: |\x00|\xa0|\s)*';
Analog die zweite Capturegroup bei $p_o_tag:
PHP:
    $p_o_tag = '<('.implode('|', $tags).')(?:\s.*[^\/])?>';
Alle nicht weiterverarbeitete Subpatterns sind mit ?: versehen worden.
 
Zuletzt bearbeitet:
Im Prinzip stolpert er über seine Gier, aber ich habe aus dem [^\/]* ein [^\/>]* gemacht, um sicher zu gehen.

Aja, das hab ich mich überhaupt gefragt. Angenommen ich hab innerhalb eines Attributs jetzt ein / oder >, dann bricht er ja auch ab :think:
Da müsstest du prüfen, ob das nur das allerletzte Zeichen vor > ist.:-?
 
Angenommen ich hab innerhalb eines Attributs jetzt ein / oder >, dann bricht er ja auch ab :think:
Da müsstest du prüfen, ob das nur das allerletzte Zeichen vor > ist.:-?

Das > ist innerhalb eines Attributs nach XML-Standard nicht erlaubt, es muss > verwendet werden. Das mit dem / ist allerdings richtig bemerkt. Also kehre ich doch zu etwas mehr in Richtung meiner Version zurück. Ich editiere die neue Version oben hinein. <-- Erledigt.
 
Zuletzt bearbeitet: