43 votes

Tronquer le texte contenant du HTML, en ignorant les balises

Je veux tronquer un texte (chargé à partir d'une base de données ou d'un fichier texte), mais il contient du HTML. Par conséquent, les balises sont incluses et moins de texte sera renvoyé. Il se peut que les balises ne soient pas fermées, ou qu'elles soient partiellement fermées (ce qui fait que Tidy ne fonctionne pas correctement et qu'il y a toujours moins de contenu). Comment puis-je tronquer en fonction du texte (et probablement arrêter lorsque vous arrivez à un tableau 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)."..."

Aurait pour conséquence :

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 le faire ?

Bien que ma question porte sur la façon de le faire en PHP, il serait bon de savoir comment le faire en C#... les deux devraient être OK car je pense être capable de porter la méthode (à moins que ce ne soit une méthode intégrée).

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

strip_tags est une solution de rechange, mais je perdrais le formatage et les liens et le problème des entités HTML persisterait.

51voto

Søren Løvborg Points 3425

En supposant que vous utilisez du XHTML valide, il est simple d'analyser le HTML et de s'assurer que les balises sont traitées correctement. Il suffit de suivre les balises qui ont été ouvertes jusqu'à présent et de s'assurer qu'elles sont refermées "à la 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 sur l'encodage : Le code ci-dessus suppose que le XHTML est UTF-8 encodé. Les codages à un seul octet compatibles ASCII (tels que Latin-1 ) sont également supportés, il suffit de passer false comme troisième argument. D'autres encodages multi-octets ne sont pas supportés, bien que vous puissiez les hacker en utilisant la commande mb_convert_encoding de convertir en UTF-8 avant d'appeler la fonction, puis de reconvertir à nouveau dans chaque print déclaration.

(Vous devriez toujours utiliser UTF-8, cependant).

Modifier : Mise à jour pour gérer les entités de caractères et UTF-8. Correction du bogue où la fonction imprimait un caractère de trop, si ce caractère était une entité de caractère.

1 votes

Cela semble pouvoir fonctionner... mais qu'en est-il des entités HTML ?

0 votes

Cela ne fonctionne pas avec les caractères internationaux car PHP preg_match compte par octet au lieu de caractère, pour le décalage. Pour voir l'essentiel de la solution à ce problème : stackoverflow.com/questions/9950842/

0 votes

@DaveStein Merci de le souligner. Considérant que j'utilise moi-même toujours UTF-8, ce bug est un peu embarrassant. Il est corrigé dans le code maintenant (ainsi qu'un autre bug de comptage que je viens de repérer).

5voto

alockwood05 Points 90

J'ai écrit une fonction qui tronque le HTML comme vous le suggérez, mais au lieu de l'imprimer, elle le garde dans une variable de type chaîne de caractères.

 /**
     *  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; 
        }

1 votes

C'est une très bonne idée, mais je reçois des erreurs ainsi que des avertissements. PHP version 5.5.

0 votes

Merci @Matt ! Je vais devoir y jeter un coup d'oeil. Cela fait un moment que je n'ai pas écrit ce bout de code.

0 votes

Quelque peu limité. "<div>data that is too big to fit into the truncated size</div>" returns </div> instead of the text up to the truncated size. Is this a bug or a feature?

4voto

porneL Points 42805

100% exact, mais approche assez difficile :

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

Approche facile par force brute :

  1. Divisez la chaîne de caractères en balises (et non en éléments) et en fragments de texte à l'aide de la fonction preg_split('/(<tag>)/') avec PREG_DELIM_CAPTURE.
  2. Mesurez la longueur du texte que vous voulez (ce sera un élément sur deux à partir de la division, vous pourriez utiliser html_entity_decode() pour aider à mesurer avec précision)
  3. Coupez la ficelle (coupez &[^\s;]+$ à la fin pour se débarrasser d'une éventuelle entité coupée)
  4. Réparez-le avec HTML Tidy

0 votes

J'ai voté en faveur de la précision, mais j'ai voté en défaveur de la méthode de force brute.

1 votes

La méthode de la force brute est-elle si mauvaise ? La première partie peut être rendue assez précise (si vous êtes bon avec les regexps), et avec Tidy vous supporterez correctement les balises de début HTML optionnelles (<table><tr><td></tbody></table> est du HTML4 valide :), ce que la solution naïve basée sur la pile ne ferait pas.

0 votes

Si seulement quelqu'un pouvait donner un exemple de l'approche correcte :(

4voto

periklis Points 4978

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

3voto

Stefan Gehrig Points 47227

Ce qui suit est un analyseur d'état simple qui traite votre cas de test avec succès. Il échoue cependant sur les balises imbriquées car il ne suit pas les balises elles-mêmes. Il échoue également sur les entités à l'intérieur des balises HTML (par exemple, dans une balise href -attribut d'un <a> -tag). On ne peut donc pas considérer qu'il s'agit d'une solution à 100 % à ce problème, mais comme il est facile à comprendre, il 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;
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X