Texte tronqué contenant du HTML, ignorant les balises

je veux tronquer du texte (chargé à partir d'une base de données ou d'un fichier texte), mais il contient du HTML de sorte que les balises sont incluses et moins de texte sera retourné. Ceci peut causer des balises n'étant pas fermé ou partiellement fermé (si propre peut ne pas fonctionner correctement et il y a encore moins de contenu). Comment puis-je tronquer basé sur le texte (et s'arrêter probablement quand 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)."..."

se traduirait par:

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

ce que je voudrais c'est:

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

Comment faire?

Alors que ma question est comment le faire en PHP, il serait bon de savoir comment le faire en C#... les deux devraient être OK car je pense que je serais en mesure de porter la méthode plus (à moins que ce soit une méthode intégrée).

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

strip_tags est un repli, mais je perdrais le formatage et les liens et il aurait toujours le problème avec les entités HTML.

32
demandé sur trejder 2009-07-28 15:30:28

13 réponses

en supposant que vous utilisez XHTML valide, il est simple de parser le HTML et de s'assurer que les balises sont manipulées correctement. Vous avez simplement besoin de suivre quelles étiquettes ont été ouvertes jusqu'à présent, et assurez-vous de les fermer à nouveau "sur votre chemin de 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 D'encodage : le code ci-dessus suppose que le XHTML est UTF-8 encodé. Les codages à un octet compatibles ASCII (tels que Latin-1 ) sont également soutenu, il suffit de passer false comme le troisième argument. D'autres encodages multibytes ne sont pas supportés, bien que vous puissiez hacker le support en utilisant mb_convert_encoding pour convertir en UTF-8 avant d'appeler la fonction, puis convertir à nouveau dans chaque déclaration print .

( toujours être en UTF-8, mais.)

Modifier : mise à Jour pour gérer les entités de caractères et encodage UTF-8. Correction de bug où le fonction imprimerait un caractère de trop, si ce caractère était une entité de caractère.

42
répondu Søren Løvborg 2012-07-07 19:20:59

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

5
répondu periklis 2012-01-12 19:43:47

100% précis, mais approche assez difficile:

  1. Iterate characterites using DOM
  2. utiliser des méthodes DOM pour supprimer les éléments restants
  3. Sérialiser les DOM

Simple force brute approche:

  1. fendre la chaîne en tags (pas les éléments) et fragments de texte en utilisant preg_split('/(<tag>)/') avec PREG_DELIM_CAPTURE.
  2. mesure la longueur de texte que vous voulez (ce sera chaque second élément de split, vous pourriez utiliser html_entity_decode() pour aider à mesurer avec précision)
  3. couper la corde (couper &[^\s;]+$ à l'extrémité pour se débarrasser de l'entité éventuellement coupée)
  4. Fixer avec HTML Tidy
4
répondu Kornel 2009-07-28 12:04:36

j'ai écrit une fonction qui tronque le HTML comme vous le suggérez, mais au lieu de l'imprimer il le met juste le garde tout dans une variable de chaîne. gère aussi 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; 
        }
4
répondu alockwood05 2015-12-28 15:55:05

pourrait utiliser DomDocument dans ce cas avec un hack regex méchant, le pire qui se produirait est un avertissement, s'il y a une étiquette 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;

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

2
répondu alex 2011-08-30 00:44:17

Bounce ajouté un support de caractères multi-octets à la solution de Søren Løvborg - j'ai ajouté:

  • prise en charge des balises HTML non appariées (p.ex. <hr> , <br> <col> etc. n'obtenez pas fermé HTML '/' n'est pas nécessaire à la fin de celles-ci (dans l'est de XHTML)),
  • indicateur de troncature personnalisable (par défaut à &hellips; i.e....),
  • retourner en chaîne sans utiliser de tampon de sortie, et
  • tests unitaires avec une couverture de 100%.

tout cela à Pastie .

2
répondu hawkip 2011-12-28 11:21:36

vous pouvez utiliser rangé ainsi:

function truncate_html($html, $max_length) {   
  return tidy_repair_string(substr($html, 0, $max_length),
     array('wrap' => 0, 'show-body-only' => TRUE), 'utf8'); 
}
2
répondu gpilotino 2012-09-10 08:23:59

ce qui suit est un analyseur de machine d'état simple qui vous manipule cas de test avec succès. Je échoue sur les étiquettes imbriquées cependant comme il ne suit pas les étiquettes eux-mêmes. Je m'étouffe également sur les entités à l'intérieur des balises HTML (par exemple dans un attribut href d'une balise <a> ). Donc, il ne peut pas être considéré comme une solution à 100% à ce problème, mais parce qu'il est facile à comprendre, il pourrait être la base d'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
répondu Stefan Gehrig 2009-07-28 12:01:35

j'ai fait des changements légers à Søren Løvborg printTruncated fonction le 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;   

    }
1
répondu Bounce 2011-11-22 14:53:10

une autre lumière modifie la fonction d'impression de Søren Løvborg en la rendant compatible UTF-8 (a besoin de mbstring) et en la rendant chaîne de retour non imprimable. Je pense que c'est plus utile. Et mon code n'utilise pas de tampon comme une variante de rebond, juste une variable de plus.

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

grand merci à Søren Løvborg pour cette fonction, c'est très bon.

/* 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;
}
1
répondu andrnag 2012-01-15 12:13:13

le CakePHP framework a une fonction truncate() HTML-aware dans le TextHelper qui fonctionne pour moi. Voir Core-Helpers / Text . Licence MIT.

1
répondu DavidJ 2013-03-20 18:18:16

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

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

Comment comptez-vous tronquer cela et finir avec du HTML valide?

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

0
répondu Antony Carthy 2009-07-28 11:44:50

utiliser la fonction truncateHTML() de: https://github.com/jlgrall/truncateHTML

exemple: tronquer après 9 caractères incluant l'ellipse:

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

Dispose: UTF-8, configurable points de suspension, d'inclure ou d'exclure de la longueur de points de suspension, fermeture automatique des balises, l'effondrement des espaces, des éléments invisibles ( <head> , <script> , <noscript> , <style> , <!-- comments --> ), HTML $entities; , tronquant au dernier mot entier (avec option de toujours tronquer les mots très longs), PHP 5.6 et 7.0+, 240+ unit tests, renvoie une chaîne (n'utilise pas le tampon de sortie), et code bien commenté.

j'ai écrit cette fonction, parce que j'ai vraiment aimé Søren Løvborg fonction ci-dessus (en particulier comment il a géré les encodages), mais j'avais besoin d'un peu plus de fonctionnalité et de flexibilité.

0
répondu jlgrall 2018-02-07 19:41:23