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´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´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 ´
- 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.
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><Hello></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>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.
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
100% précis, mais approche assez difficile:
- Iterate characterites using DOM
- utiliser des méthodes DOM pour supprimer les éléments restants
- Sérialiser les DOM
Simple force brute approche:
- fendre la chaîne en tags (pas les éléments) et fragments de texte en utilisant
preg_split('/(<tag>)/')
avec PREG_DELIM_CAPTURE. - 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) - couper la corde (couper
&[^\s;]+$
à l'extrémité pour se débarrasser de l'entité éventuellement coupée) - Fixer avec HTML Tidy
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 = '…'){
//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;
}
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´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>
.
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 .
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;
}
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;
}
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;
}
le CakePHP framework a une fonction truncate() HTML-aware dans le TextHelper qui fonctionne pour moi. Voir Core-Helpers / Text . Licence MIT.
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.
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é.