2010-03-08 14 views
35

Tengo varias cadenas de HTML para cortar a 100 caracteres (del contenido eliminado, no el original) sin pelar etiquetas y sin romper HTML.Utilizando PHP substr() y strip_tags() manteniendo el formato y sin romper HTML

cadena HTML original (288 caracteres):

$content = "<div>With a <span class='spanClass'>span over here</span> and a 
<div class='divClass'>nested div over <div class='nestedDivClass'>there</div> 
</div> and a lot of other nested <strong><em>texts</em> and tags in the air 
<span>everywhere</span>, it's a HTML taggy kind of day.</strong></div>"; 

ajuste estándar: de recorte hasta 100 caracteres y saltos de HTML, el contenido despojado trata de ~ 40 caracteres:

$content = substr($content, 0, 100)."..."; /* output: 
<div>With a <span class='spanClass'>span over here</span> and a 
<div class='divClass'>nested div ove... */ 

Stripped HTML: Emite el conteo correcto de caracteres pero obviamente pierde formato:

$content = substr(strip_tags($content)), 0, 100)."..."; /* output: 
With a span over here and a nested div over there and a lot of other nested 
texts and tags in the ai... */ 

solución parcial: utilizando HTML Tidy o purificador para cerrar las salidas etiquetas HTML, pero limpia de 100 caracteres HTML no muestran contenido.

$content = substr($content, 0, 100)."..."; 
$tidy = new tidy; $tidy->parseString($content); $tidy->cleanRepair(); /* output: 
<div>With a <span class='spanClass'>span over here</span> and a 
<div class='divClass'>nested div ove</div></div>... */ 

reto: Para la salida HTML limpio y n caracteres (sin incluir recuento de caracteres de elementos HTML):

$content = cutHTML($content, 100); /* output: 
<div>With a <span class='spanClass'>span over here</span> and a 
<div class='divClass'>nested div over <div class='nestedDivClass'>there</div> 
</div> and a lot of other nested <strong><em>texts</em> and tags in the 
ai</strong></div>..."; 

Preguntas similares

Respuesta

33

No es espectacular, pero funciona.

function html_cut($text, $max_length) 
{ 
    $tags = array(); 
    $result = ""; 

    $is_open = false; 
    $grab_open = false; 
    $is_close = false; 
    $in_double_quotes = false; 
    $in_single_quotes = false; 
    $tag = ""; 

    $i = 0; 
    $stripped = 0; 

    $stripped_text = strip_tags($text); 

    while ($i < strlen($text) && $stripped < strlen($stripped_text) && $stripped < $max_length) 
    { 
     $symbol = $text{$i}; 
     $result .= $symbol; 

     switch ($symbol) 
     { 
      case '<': 
       $is_open = true; 
       $grab_open = true; 
       break; 

      case '"': 
       if ($in_double_quotes) 
        $in_double_quotes = false; 
       else 
        $in_double_quotes = true; 

      break; 

      case "'": 
       if ($in_single_quotes) 
        $in_single_quotes = false; 
       else 
        $in_single_quotes = true; 

      break; 

      case '/': 
       if ($is_open && !$in_double_quotes && !$in_single_quotes) 
       { 
        $is_close = true; 
        $is_open = false; 
        $grab_open = false; 
       } 

       break; 

      case ' ': 
       if ($is_open) 
        $grab_open = false; 
       else 
        $stripped++; 

       break; 

      case '>': 
       if ($is_open) 
       { 
        $is_open = false; 
        $grab_open = false; 
        array_push($tags, $tag); 
        $tag = ""; 
       } 
       else if ($is_close) 
       { 
        $is_close = false; 
        array_pop($tags); 
        $tag = ""; 
       } 

       break; 

      default: 
       if ($grab_open || $is_close) 
        $tag .= $symbol; 

       if (!$is_open && !$is_close) 
        $stripped++; 
     } 

     $i++; 
    } 

    while ($tags) 
     $result .= "</".array_pop($tags).">"; 

    return $result; 
} 

Ejemplo de uso:

$content = html_cut($content, 100); 
+3

Diría increíble, funciona, exactamente como se describe en el desafío ... –

+0

Aún recibo cosas como 'envíenos un correo electrónico al Read More' todo el tiempo. Supongo que algo está mal con mi contenido proveniente de la base de datos pero ¿alguien tiene una idea? ¡Gracias! – TomShreds

+1

¿Qué tal soporte para utf-8? –

3

Utilice HTML parser y pare después de 100 caracteres de texto.

+1

+1 para el analizador de HTML, e inclinándose hacia una solución si el texto de salida excluye HTML. Un ejemplo rápido podría haber sido bueno. –

16

No estoy afirmando que han inventado esto, pero hay una muy completa Text::truncate() method in CakePHP el que hace lo que quiere:

function truncate($text, $length = 100, $ending = '...', $exact = true, $considerHtml = false) { 
    if (is_array($ending)) { 
     extract($ending); 
    } 
    if ($considerHtml) { 
     if (mb_strlen(preg_replace('/<.*?>/', '', $text)) <= $length) { 
      return $text; 
     } 
     $totalLength = mb_strlen($ending); 
     $openTags = array(); 
     $truncate = ''; 
     preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER); 
     foreach ($tags as $tag) { 
      if (!preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/s', $tag[2])) { 
       if (preg_match('/<[\w]+[^>]*>/s', $tag[0])) { 
        array_unshift($openTags, $tag[2]); 
       } else if (preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $closeTag)) { 
        $pos = array_search($closeTag[1], $openTags); 
        if ($pos !== false) { 
         array_splice($openTags, $pos, 1); 
        } 
       } 
      } 
      $truncate .= $tag[1]; 

      $contentLength = mb_strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $tag[3])); 
      if ($contentLength + $totalLength > $length) { 
       $left = $length - $totalLength; 
       $entitiesLength = 0; 
       if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $tag[3], $entities, PREG_OFFSET_CAPTURE)) { 
        foreach ($entities[0] as $entity) { 
         if ($entity[1] + 1 - $entitiesLength <= $left) { 
          $left--; 
          $entitiesLength += mb_strlen($entity[0]); 
         } else { 
          break; 
         } 
        } 
       } 

       $truncate .= mb_substr($tag[3], 0 , $left + $entitiesLength); 
       break; 
      } else { 
       $truncate .= $tag[3]; 
       $totalLength += $contentLength; 
      } 
      if ($totalLength >= $length) { 
       break; 
      } 
     } 

    } else { 
     if (mb_strlen($text) <= $length) { 
      return $text; 
     } else { 
      $truncate = mb_substr($text, 0, $length - strlen($ending)); 
     } 
    } 
    if (!$exact) { 
     $spacepos = mb_strrpos($truncate, ' '); 
     if (isset($spacepos)) { 
      if ($considerHtml) { 
       $bits = mb_substr($truncate, $spacepos); 
       preg_match_all('/<\/([a-z]+)>/', $bits, $droppedTags, PREG_SET_ORDER); 
       if (!empty($droppedTags)) { 
        foreach ($droppedTags as $closingTag) { 
         if (!in_array($closingTag[1], $openTags)) { 
          array_unshift($openTags, $closingTag[1]); 
         } 
        } 
       } 
      } 
      $truncate = mb_substr($truncate, 0, $spacepos); 
     } 
    } 

    $truncate .= $ending; 

    if ($considerHtml) { 
     foreach ($openTags as $tag) { 
      $truncate .= '</'.$tag.'>'; 
     } 
    } 

    return $truncate; 
} 
+0

¿Qué pasa si tienes algún 'Offset'? digamos obtener html hasta 100 caracteres y la próxima vez tomar de 101 a 200? ¿posible? –

2

Debe utilizar Tidy HTML. Cortas el hilo y luego ejecutas Tidy para cerrar las etiquetas.

(Credits where credits are due) la clase de

+0

+1 para Tidy HTML (y agregado como una solución parcial) pero no representa 100 caracteres de texto entregado. –

5

Uso de PHP DOMDocument para normalizar un fragmento de HTML:

$dom= new DOMDocument(); 
$dom->loadHTML('<div><p>Hello World');  
$xpath = new DOMXPath($dom); 
$body = $xpath->query('/html/body'); 
echo($dom->saveXml($body->item(0))); 

Esta pregunta es similar a un earlier question y he copiado y pegado una solución a este problema. Si el HTML es enviado por los usuarios, también deberá filtrar posibles vectores de ataque de Javascript, como onmouseover="do_something_evil()" o <a href="javascript:more_evil();">...</a>. Herramientas como HTML Purifier fueron diseñadas para atrapar y resolver estos problemas y son mucho más completas que cualquier código que pueda publicar.

+1

+1 para HTML Purifier –

+0

Esta es una excelente solución. –

1

Independientemente de los 100 números de conteo que Estado al inicio, se indican en el desafío lo siguiente:

  • de salida del contador de caracteres de strip_tags (el número de caracteres en el texto real que se muestra de la HTML)
  • conservan el formato HTML etiqueta de cierre
  • todo el código HTML sin terminar

Aquí está mi propuesta: Básicamente, analizo cada carácter contando a medida que avanzo. Me aseguro de no contar ningún carácter en ninguna etiqueta HTML. También compruebo al final para asegurarme de que no estoy en medio de una palabra cuando paro. Una vez que me detengo, retrocedo al primer ESPACIO disponible o> como punto de parada.

$position = 0; 
$length = strlen($content)-1; 

// process the content putting each 100 character section into an array 
while($position < $length) 
{ 
    $next_position = get_position($content, $position, 100); 
    $data[] = substr($content, $position, $next_position); 
    $position = $next_position; 
} 

// show the array 
print_r($data); 

function get_position($content, $position, $chars = 100) 
{ 
    $count = 0; 
    // count to 100 characters skipping over all of the HTML 
    while($count <> $chars){ 
     $char = substr($content, $position, 1); 
     if($char == '<'){ 
      do{ 
       $position++; 
       $char = substr($content, $position, 1); 
      } while($char !== '>'); 
      $position++; 
      $char = substr($content, $position, 1); 
     } 
     $count++; 
     $position++; 
    } 
echo $count."\n"; 
    // find out where there is a logical break before 100 characters 
    $data = substr($content, 0, $position); 

    $space = strrpos($data, " "); 
    $tag = strrpos($data, ">"); 

    // return the position of the logical break 
    if($space > $tag) 
    { 
     return $space; 
    } else { 
     return $tag; 
    } 
} 

Esto también contará los códigos de retorno, etc. Teniendo en cuenta que ocuparán espacio, no los he eliminado.

+0

+1 por el esfuerzo y la consideración para terminar la última palabra en lugar de cortarla.Supongo que un valor que podría pasar se cortaría en el salto lógico ANTERIOR o en el siguiente salto lógico. El próximo es probable que se deslice sobre el límite de caracteres si es importante. ¡Gracias por el esfuerzo! –

0

Aquí está mi intento en el cortador. Quizás ustedes puedan detectar algunos errores. El problema, me encontré con los otros programas de análisis, es que no cierre correctamente y etiquetas que cortar en el medio de una palabra (bla)

function cutHTML($string, $length, $patternsReplace = false) { 
    $i = 0; 
    $count = 0; 
    $isParagraphCut = false; 
    $htmlOpen = false; 
    $openTag = false; 
    $tagsStack = array(); 

    while ($i < strlen($string)) { 
     $char = substr($string, $i, 1); 
     if ($count >= $length) { 
      $isParagraphCut = true; 
      break; 
     } 

     if ($htmlOpen) { 
      if ($char === ">") { 
       $htmlOpen = false; 
      } 
     } else { 
      if ($char === "<") { 
       $j = $i; 
       $char = substr($string, $j, 1); 

       while ($j < strlen($string)) { 
        if($char === '/'){ 
         $i++; 
         break; 
        } 
        elseif ($char === ' ') { 
         $tagsStack[] = substr($string, $i, $j); 
        } 
        $j++; 
       } 
       $htmlOpen = true; 
      } 
     } 

     if (!$htmlOpen && $char != ">") { 
      $count++; 
     } 

     $i++; 
    } 

    if ($isParagraphCut) { 
     $j = $i; 
     while ($j > 0) { 
      $char = substr($string, $j, 1); 
      if ($char === " " || $char === ";" || $char === "." || $char === "," || $char === "<" || $char === "(" || $char === "[") { 
       break; 
      } else if ($char === ">") { 
       $j++; 
       break; 
      } 
      $j--; 
     } 
     $string = substr($string, 0, $j); 
     foreach($tagsStack as $tag){ 
      $tag = strtolower($tag); 
      if($tag !== "img" && $tag !== "br"){ 
       $string .= "</$tag>"; 
      } 
     } 
     $string .= "..."; 
    } 

    if ($patternsReplace) { 
     foreach ($patternsReplace as $value) { 
      if (isset($value['pattern']) && isset($value["replace"])) { 
       $string = preg_replace($value["pattern"], $value["replace"], $string); 
      } 
     } 
    } 
    return $string; 
} 
+0

¡Casi! Pero sigo recibiendo: 'envíenos un correo electrónico al Read More' cuando estoy usando ese código (lo mismo que para una buena respuesta). ¿Alguna idea? ¡Gracias! – TomShreds

+0

no estoy seguro, pero parece que sacó el texto de alguna base de datos y se cambió la cita de "a "e; intente usar htmlspecialchars-decode (http://php.net/manual/en/function.htmlspecialchars-decode.php) – Kubee

1

Aquí es una función que estoy usando en una de mi proyectos. Está basado en DOMDocument, funciona con HTML5 y es aproximadamente 2 veces más rápido que otras soluciones que he probado (al menos en mi máquina, 0.22 ms vs 0.43 ms usando html_cut($text, $max_length) desde la respuesta superior en una cadena de 500 caracteres de texto-nodo con un límite de 400).

function cut_html ($html, $limit) { 
    $dom = new DOMDocument(); 
    $dom->loadHTML(mb_convert_encoding("<div>{$html}</div>", "HTML-ENTITIES", "UTF-8"), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); 
    cut_html_recursive($dom->documentElement, $limit); 
    return substr($dom->saveHTML($dom->documentElement), 5, -6); 
} 

function cut_html_recursive ($element, $limit) { 
    if($limit > 0) { 
     if($element->nodeType == 3) { 
      $limit -= strlen($element->nodeValue); 
      if($limit < 0) { 
       $element->nodeValue = substr($element->nodeValue, 0, strlen($element->nodeValue) + $limit); 
      } 
     } 
     else { 
      for($i = 0; $i < $element->childNodes->length; $i++) { 
       if($limit > 0) { 
        $limit = cut_html_recursive($element->childNodes->item($i), $limit); 
       } 
       else { 
        $element->removeChild($element->childNodes->item($i)); 
        $i--; 
       } 
      } 
     } 
    } 
    return $limit; 
} 
0

tratar esta función

// trim the string function 
function trim_word($text, $length, $startPoint=0, $allowedTags=""){ 
    $text = html_entity_decode(htmlspecialchars_decode($text)); 
    $text = strip_tags($text, $allowedTags); 
    return $text = substr($text, $startPoint, $length); 
} 

y

echo trim_word("<h2 class='zzzz'>abcasdsdasasdas</h2>","6"); 
2

hice otra función para hacerlo, soporta UTF-8:

/** 
* Limit string without break html tags. 
* Supports UTF8 
* 
* @param string $value 
* @param int $limit Default 100 
*/ 
function str_limit_html($value, $limit = 100) 
{ 

    if (mb_strwidth($value, 'UTF-8') <= $limit) { 
     return $value; 
    } 

    // Strip text with HTML tags, sum html len tags too. 
    // Is there another way to do it? 
    do { 
     $len   = mb_strwidth($value, 'UTF-8'); 
     $len_stripped = mb_strwidth(strip_tags($value), 'UTF-8'); 
     $len_tags  = $len - $len_stripped; 

     $value = mb_strimwidth($value, 0, $limit + $len_tags, '', 'UTF-8'); 
    } while ($len_stripped > $limit); 

    // Load as HTML ignoring errors 
    $dom = new DOMDocument(); 
    @$dom->loadHTML('<?xml encoding="utf-8" ?>'.$value, LIBXML_HTML_NODEFDTD); 

    // Fix the html errors 
    $value = $dom->saveHtml($dom->getElementsByTagName('body')->item(0)); 

    // Remove body tag 
    $value = mb_strimwidth($value, 6, mb_strwidth($value, 'UTF-8') - 13, '', 'UTF-8'); // <body> and </body> 
    // Remove empty tags 
    return preg_replace('/<(\w+)\b(?:\s+[\w\-.:]+(?:\s*=\s*(?:"[^"]*"|"[^"]*"|[\w\-.:]+))?)*\s*\/?>\s*<\/\1\s*>/', '', $value); 
} 

SEE DEMO.

que recomiendan su uso en html_entity_decode inicio de la función, por lo que conservan la UTF-8 caracteres:

$value = html_entity_decode($value); 
Cuestiones relacionadas