web-dev-qa-db-fra.com

Tronquer le texte contenant HTML, en ignorant les balises

Je souhaite tronquer du texte (chargé à partir d'une base de données ou d'un fichier texte), mais il contient du code HTML. Par conséquent, les balises sont incluses et moins de texte est renvoyé. Cela peut alors avoir pour conséquence que les balises ne soient pas fermées ou soient partiellement fermées (de sorte que Tidy risque de ne pas fonctionner correctement et qu'il y ait encore moins de contenu). Comment puis-je tronquer en fonction du texte (et probablement m'arrêter lorsque vous arrivez à une table car cela pourrait causer des problèmes plus complexes).

substr("Hello, my <strong>name</strong> is <em>Sam</em>. I&acute;m a web developer.",0,26)."..."

Aboutirait à:

Hello, my <strong>name</st...

Ce que je voudrais, c'est:

Hello, my <strong>name</strong> is <em>Sam</em>. I&acute;m...

Comment puis-je faire ceci?

Bien que ma question concerne la façon de le faire en PHP, il serait bon de savoir comment le faire en C # ... soit ça devrait aller, car je pense que je pourrais transférer la méthode (à moins que ce méthode).

Notez également que j'ai inclus une entité HTML &acute; - qui devrait être considérée comme un seul caractère (plutôt que 7 caractères comme dans cet exemple).

strip_tags est un repli, mais je perdrais la mise en forme et les liens et le problème persisterait avec les entités HTML.

32
SamWM

En supposant que vous utilisez du XHTML valide, il est simple d'analyser le code HTML et de vous assurer que les balises sont gérées correctement. Vous devez simplement suivre les balises qui ont été ouvertes jusqu'à présent et assurez-vous de les refermer "à votre sortie".

<?php
header('Content-type: text/plain; charset=utf-8');

function printTruncated($maxLength, $html, $isUtf8=true)
{
    $printedLength = 0;
    $position = 0;
    $tags = array();

    // For UTF-8, we need to count multibyte sequences as one character.
    $re = $isUtf8
        ? '{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;|[\x80-\xFF][\x80-\xBF]*}'
        : '{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;}';

    while ($printedLength < $maxLength && preg_match($re, $html, $match, PREG_OFFSET_CAPTURE, $position))
    {
        list($tag, $tagPosition) = $match[0];

        // Print text leading up to the tag.
        $str = substr($html, $position, $tagPosition - $position);
        if ($printedLength + strlen($str) > $maxLength)
        {
            print(substr($str, 0, $maxLength - $printedLength));
            $printedLength = $maxLength;
            break;
        }

        print($str);
        $printedLength += strlen($str);
        if ($printedLength >= $maxLength) break;

        if ($tag[0] == '&' || ord($tag) >= 0x80)
        {
            // Pass the entity or UTF-8 multibyte sequence through unchanged.
            print($tag);
            $printedLength++;
        }
        else
        {
            // Handle the tag.
            $tagName = $match[1][0];
            if ($tag[1] == '/')
            {
                // This is a closing tag.

                $openingTag = array_pop($tags);
                assert($openingTag == $tagName); // check that tags are properly nested.

                print($tag);
            }
            else if ($tag[strlen($tag) - 2] == '/')
            {
                // Self-closing tag.
                print($tag);
            }
            else
            {
                // Opening tag.
                print($tag);
                $tags[] = $tagName;
            }
        }

        // Continue after the tag.
        $position = $tagPosition + strlen($tag);
    }

    // Print any remaining text.
    if ($printedLength < $maxLength && $position < strlen($html))
        print(substr($html, $position, $maxLength - $printedLength));

    // Close any open tags.
    while (!empty($tags))
        printf('</%s>', array_pop($tags));
}


printTruncated(10, '<b>&lt;Hello&gt;</b> <img src="world.png" alt="" /> world!'); print("\n");

printTruncated(10, '<table><tr><td>Heck, </td><td>throw</td></tr><tr><td>in a</td><td>table</td></tr></table>'); print("\n");

printTruncated(10, "<em><b>Hello</b>&#20;w\xC3\xB8rld!</em>"); print("\n");

Note de codage : Le code ci-dessus suppose que le XHTML est UTF-8 codé. Les codages à un octet compatibles ASCII (tels que Latin-1 ) sont également pris en charge, il suffit de passer false en tant que troisième argument. Les autres codages multi-octets ne sont pas pris en charge, bien que vous puissiez modifier le support en utilisant mb_convert_encoding pour convertir en UTF-8 avant d'appeler la fonction, puis en reconvertissant à chaque instruction print.

(Vous devriez cependant toujours utiliser UTF-8.)

Edit : mis à jour pour gérer les entités de caractères et UTF-8. Correction d'un bug où la fonction imprimait un caractère de trop, si ce caractère était une entité de personnage.

43
Søren Løvborg

J'ai écrit une fonction qui tronque le code HTML comme vous le suggérez, mais au lieu de l’imprimer, elle conserve simplement le tout dans une variable chaîne. gère également les entités HTML. 

 /**
     *  function to truncate and then clean up end of the HTML,
     *  truncates by counting characters outside of HTML tags
     *  
     *  @author alex lockwood, alex dot lockwood at websightdesign
     *  
     *  @param string $str the string to truncate
     *  @param int $len the number of characters
     *  @param string $end the end string for truncation
     *  @return string $truncated_html
     *  
     *  **/
        public static function truncateHTML($str, $len, $end = '&hellip;'){
            //find all tags
            $tagPattern = '/(<\/?)([\w]*)(\s*[^>]*)>?|&[\w#]+;/i';  //match html tags and entities
            preg_match_all($tagPattern, $str, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER );
            //WSDDebug::dump($matches); exit; 
            $i =0;
            //loop through each found tag that is within the $len, add those characters to the len,
            //also track open and closed tags
            // $matches[$i][0] = the whole tag string  --the only applicable field for html enitities  
            // IF its not matching an &htmlentity; the following apply
            // $matches[$i][1] = the start of the tag either '<' or '</'  
            // $matches[$i][2] = the tag name
            // $matches[$i][3] = the end of the tag
            //$matces[$i][$j][0] = the string
            //$matces[$i][$j][1] = the str offest

            while($matches[$i][0][1] < $len && !empty($matches[$i])){

                $len = $len + strlen($matches[$i][0][0]);
                if(substr($matches[$i][0][0],0,1) == '&' )
                    $len = $len-1;


                //if $matches[$i][2] is undefined then its an html entity, want to ignore those for tag counting
                //ignore empty/singleton tags for tag counting
                if(!empty($matches[$i][2][0]) && !in_array($matches[$i][2][0],array('br','img','hr', 'input', 'param', 'link'))){
                    //double check 
                    if(substr($matches[$i][3][0],-1) !='/' && substr($matches[$i][1][0],-1) !='/')
                        $openTags[] = $matches[$i][2][0];
                    elseif(end($openTags) == $matches[$i][2][0]){
                        array_pop($openTags);
                    }else{
                        $warnings[] = "html has some tags mismatched in it:  $str";
                    }
                }


                $i++;

            }

            $closeTags = '';

            if (!empty($openTags)){
                $openTags = array_reverse($openTags);
                foreach ($openTags as $t){
                    $closeTagString .="</".$t . ">"; 
                }
            }

            if(strlen($str)>$len){
                // Finds the last space from the string new length
                $lastWord = strpos($str, ' ', $len);
                if ($lastWord) {
                    //truncate with new len last Word
                    $str = substr($str, 0, $lastWord);
                    //finds last character
                    $last_character = (substr($str, -1, 1));
                    //add the end text
                    $truncated_html = ($last_character == '.' ? $str : ($last_character == ',' ? substr($str, 0, -1) : $str) . $end);
                }
                //restore any open tags
                $truncated_html .= $closeTagString;


            }else
            $truncated_html = $str;


            return $truncated_html; 
        }
5
alockwood05

J'ai utilisé une fonction Nice trouvée sur http://alanwhipple.com/2011/05/25/php-truncate-string-preserving-html-tags-words , apparemment tirée de CakePHP.

4
periklis

Approche précise à 100%, mais assez difficile:

  1. Itérer des caractères en utilisant DOM
  2. Utiliser des méthodes DOM pour supprimer les éléments restants
  3. Sérialiser le DOM

Approche facile par force brute:

  1. Fractionnez la chaîne en balises (pas des éléments) et fragments de texte en utilisant preg_split('/(<tag>)/') avec PREG_DELIM_CAPTURE.
  2. Mesurez la longueur de texte que vous souhaitez (ce sera chaque seconde élément de la division, vous pouvez utiliser html_entity_decode() pour vous aider à mesurer avec précision)
  3. Coupez la chaîne (coupez &[^\s;]+$ à la fin pour vous débarrasser de l'entité éventuellement hachée)
  4. Fixez-le avec HTML Tidy
4
Kornel

Dans ce cas, vous pouvez utiliser DomDocument avec un bidouillage de regex, le pire qui puisse arriver est un avertissement, s'il y a une balise cassée:

$dom = new DOMDocument();
$dom->loadHTML(substr("Hello, my <strong>name</strong> is <em>Sam</em>. I&acute;m a web developer.",0,26));
$html = preg_replace("/\<\/?(body|html|p)>/", "", $dom->saveHTML());
echo $html;

Devrait donner la sortie: Hello, my <strong>**name**</strong>.

Bounce a ajouté la prise en charge des caractères multi-octets à la solution de Søren Løvborg - j'ai ajouté:

  • prise en charge des balises HTML non appariées (par exemple, <hr>, <br>, <col>, etc. ne sont pas fermés - en HTML, un '/' n'est pas requis à la fin de celles-ci (dans le cas où XHTML est utilisé),
  • indicateur de troncature personnalisable (par défaut &hellips; i.e.…),
  • retourner comme une chaîne sans utiliser le tampon de sortie, et
  • tests unitaires avec une couverture de 100%.

Tout ça chez Pastie .

2
hawkip

Une autre lumière passe à la fonction printTruncated de Søren Løvborg, la rendant compatible UTF-8 (Needs mbstring) et rendant la chaîne renvoyée non imprimée. Je pense que c'est plus utile. Et mon code n'utilise pas la mise en mémoire tampon comme la variante Bounce, juste une variable supplémentaire.

UPD: pour que cela fonctionne correctement avec les caractères utf-8 dans les attributs de balises, vous avez besoin de la fonction mb_preg_match, listée ci-dessous.

Merci beaucoup à Søren Løvborg pour cette fonction, c'est très bien.

/* Truncate HTML, close opened tags
*
* @param int, maxlength of the string
* @param string, html       
* @return $html
*/

function htmlTruncate($maxLength, $html)
{
    mb_internal_encoding("UTF-8");
    $printedLength = 0;
    $position = 0;
    $tags = array();
    $out = "";

    while ($printedLength < $maxLength && mb_preg_match('{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;}', $html, $match, PREG_OFFSET_CAPTURE, $position))
    {
        list($tag, $tagPosition) = $match[0];

        // Print text leading up to the tag.
        $str = mb_substr($html, $position, $tagPosition - $position);
        if ($printedLength + mb_strlen($str) > $maxLength)
        {
            $out .= mb_substr($str, 0, $maxLength - $printedLength);
            $printedLength = $maxLength;
            break;
        }

        $out .= $str;
        $printedLength += mb_strlen($str);

        if ($tag[0] == '&')
        {
            // Handle the entity.
            $out .= $tag;
            $printedLength++;
        }
        else
        {
            // Handle the tag.
            $tagName = $match[1][0];
            if ($tag[1] == '/')
            {
                // This is a closing tag.

                $openingTag = array_pop($tags);
                assert($openingTag == $tagName); // check that tags are properly nested.

                $out .= $tag;
            }
            else if ($tag[mb_strlen($tag) - 2] == '/')
            {
                // Self-closing tag.
                $out .= $tag;
            }
            else
            {
                // Opening tag.
                $out .= $tag;
                $tags[] = $tagName;
            }
        }

        // Continue after the tag.
        $position = $tagPosition + mb_strlen($tag);
    }

    // Print any remaining text.
    if ($printedLength < $maxLength && $position < mb_strlen($html))
        $out .= mb_substr($html, $position, $maxLength - $printedLength);

    // Close any open tags.
    while (!empty($tags))
        $out .= sprintf('</%s>', array_pop($tags));

    return $out;
}

function mb_preg_match(
    $ps_pattern,
    $ps_subject,
    &$pa_matches,
    $pn_flags = 0,
    $pn_offset = 0,
    $ps_encoding = NULL
) {
    // WARNING! - All this function does is to correct offsets, nothing else:
    //(code is independent of PREG_PATTER_ORDER / PREG_SET_ORDER)

    if (is_null($ps_encoding)) $ps_encoding = mb_internal_encoding();

    $pn_offset = strlen(mb_substr($ps_subject, 0, $pn_offset, $ps_encoding));
    $ret = preg_match($ps_pattern, $ps_subject, $pa_matches, $pn_flags, $pn_offset);

    if ($ret && ($pn_flags & PREG_OFFSET_CAPTURE))
        foreach($pa_matches as &$ha_match) {
                $ha_match[1] = mb_strlen(substr($ps_subject, 0, $ha_match[1]), $ps_encoding);
        }

    return $ret;
}
2
Andrey Nagikh

Le CakePHP framework a une fonction truncate () compatible HTML dans TextHelper qui fonctionne pour moi. Voir Core-Helpers/Text . MIT licence.

2
DavidJ

vous pouvez utiliser ranger aussi:

function truncate_html($html, $max_length) {   
  return tidy_repair_string(substr($html, 0, $max_length),
     array('wrap' => 0, 'show-body-only' => TRUE), 'utf8'); 
}
2
gpilotino

J'ai apporté de légères modifications à la fonction Søren Løvborg printTruncated en la rendant compatible UTF-8:

   /* Truncate HTML, close opened tags
    *
    * @param int, maxlength of the string
    * @param string, html       
    * @return $html
    */  
    function html_truncate($maxLength, $html){

        mb_internal_encoding("UTF-8");

        $printedLength = 0;
        $position = 0;
        $tags = array();

        ob_start();

        while ($printedLength < $maxLength && preg_match('{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;}', $html, $match, PREG_OFFSET_CAPTURE, $position)){

            list($tag, $tagPosition) = $match[0];

            // Print text leading up to the tag.
            $str = mb_strcut($html, $position, $tagPosition - $position);

            if ($printedLength + mb_strlen($str) > $maxLength){
                print(mb_strcut($str, 0, $maxLength - $printedLength));
                $printedLength = $maxLength;
                break;
            }

            print($str);
            $printedLength += mb_strlen($str);

            if ($tag[0] == '&'){
                // Handle the entity.
                print($tag);
                $printedLength++;
            }
            else{
                // Handle the tag.
                $tagName = $match[1][0];
                if ($tag[1] == '/'){
                    // This is a closing tag.

                    $openingTag = array_pop($tags);
                    assert($openingTag == $tagName); // check that tags are properly nested.

                    print($tag);
                }
                else if ($tag[mb_strlen($tag) - 2] == '/'){
                    // Self-closing tag.
                    print($tag);
                }
                else{
                    // Opening tag.
                    print($tag);
                    $tags[] = $tagName;
                }
            }

            // Continue after the tag.
            $position = $tagPosition + mb_strlen($tag);
        }

        // Print any remaining text.
        if ($printedLength < $maxLength && $position < mb_strlen($html))
            print(mb_strcut($html, $position, $maxLength - $printedLength));

        // Close any open tags.
        while (!empty($tags))
             printf('</%s>', array_pop($tags));


        $bufferOuput = ob_get_contents();

        ob_end_clean();         

        $html = $bufferOuput;   

        return $html;   

    }
2
Bounce

Ce qui suit est un analyseur de machine d'état simple qui gère votre cas de test avec succès. Je ne parviens pas sur les balises imbriquées car il ne suit pas les balises elles-mêmes. J'étouffe aussi les entités dans les balises HTML (par exemple, dans un attribut href- d'une balise <a>-). Cela ne peut donc pas être considéré comme une solution à 100% à ce problème mais, comme il est facile à comprendre, cela pourrait servir de base à une fonction plus avancée.

function substr_html($string, $length)
{
    $count = 0;
    /*
     * $state = 0 - normal text
     * $state = 1 - in HTML tag
     * $state = 2 - in HTML entity
     */
    $state = 0;    
    for ($i = 0; $i < strlen($string); $i++) {
        $char = $string[$i];
        if ($char == '<') {
            $state = 1;
        } else if ($char == '&') {
            $state = 2;
            $count++;
        } else if ($char == ';') {
            $state = 0;
        } else if ($char == '>') {
            $state = 0;
        } else if ($state === 0) {
            $count++;
        }

        if ($count === $length) {
            return substr($string, 0, $i + 1);
        }
    }
    return $string;
}
1
Stefan Gehrig

Ceci est très difficile à faire sans utiliser un validateur et un analyseur, la raison étant que vous imaginez si vous avez

<div id='x'>
    <div id='y'>
        <h1>Heading</h1>
        500 
        lines 
        of 
        html
        ...
        etc
        ...
    </div>
</div>

Comment envisagez-vous de tronquer cela et de vous retrouver avec un code HTML valide?

Après une brève recherche, j'ai trouvé ce lien qui pourrait aider.

0
Antony Carthy

Utilisez la fonction truncateHTML() de: Https://github.com/jlgrall/truncateHTML

Exemple: tronque après 9 caractères, y compris les points de suspension:

truncateHTML(9, "<p><b>A</b> red ball.</p>", ['wholeWord' => false]);
// =>           "<p><b>A</b> red ba…</p>"

Caractéristiques: UTF-8, Ellipsis configurables, inclure/exclure la longueur des Ellipsis, balises à fermeture automatique, espaces réductibles, éléments invisibles (<head>, <script>, <noscript>, <style>, <!-- comments -->), HTML $entities;, tronqué enfin Mot entier (avec la possibilité de toujours tronquer des mots très longs), PHP 5.6 et 7.0+, plus de 240 tests unitaires, renvoie une chaîne (n'utilise pas le tampon de sortie) et un code bien commenté.

J'ai écrit cette fonction, parce que j'aimais beaucoup la fonction de Søren Løvborg ci-dessus (en particulier la façon dont il gérait les encodages), mais il me fallait un peu plus de fonctionnalités et de flexibilité.

0
jlgrall