Эта тема на forum.dklab.ru


SETKINS: Обрезка текста с тегами
есть какой-то текст с тегами html

<img>
<a href >
<object >


может кто сталкивался с его обрезкой до определенной длины.
т.е. вообще это новости. Если они очень большие, то их надо резать допустим выводить первые 250 символов

как сделать так чтобы выдался текст, может чуть больше, но если на 245 месте начался тег, чтобы его не раздробило,
а вывело целиком

спасибо!!!
Юрий Насретдинов:
SETKINS
Задачка довольно сложная. Я думаю, что самое лучшее решение -- это сначала удалять все теги из текста новости, а потом её обрезать (причём, ИМХО, лучше не оставлять не то, чтобы кусков тегов, но и даже кусков предложений). При необходимости, можно попробовать с помощью «нечёткой логики :)» возвращать теги в те предложения, которые остались целы после обрезки текста.
kernel32:
Думаю, тут надо strip_tags_smart(). А если хочется, чтоб в возвращаемом превью была и картинка, то можно вставлять туда первую картинку из новости ( <img...> ), вытаскиваю её через preg_match()
У меня где-то была похожая функция, которая возвращала превью новости, но там bb-коды... Написал сам, но глючноватая она. На досуге, может, допишу.
SETKINS:
у меня html коды получаются после парсинга bbcode

так если бы поделился примером функции было бы круто
kernel32:
SETKINS, примером функции делиться не буду, т.к. она потенциально недописана.
Как допишу - с удовольствием поделюсь. :) Время только бы найти.
SETKINS:
function breakLongWords($str, $maxLength, $char){
$wordEndChars = array(" ", "\n", "\r", "\f", "\v", "\0");
$count = 0;
$newStr = "";
$openTag = false;




for($i=0; $i<strlen($str); $i++){
$newStr .= $str{$i};

if($str{$i} == "<"){
$openTag = true;
continue;
}
if(($openTag) && ($str{$i} == ">")){
$openTag = false;
continue;
}

if(!$openTag){
if(in_array($str{$i}, $wordEndChars)){//If not word ending char

if($i>$maxLength){//if current word max length is reached
return $newStr;

}
}else{//Else char is word ending, reset word char count

}
}

}//End for
return $newStr;
}




вот эта штука в принципе режет текст, правда у нее проверка отстойная на открыт тег или нет
подходит допустим для тегов

т.е. тег http://site.dom она спокойно порежет, без пробелов правда не порежет.
Валенок:
SETKINS перед этим (и после этого, если нет защиты от отрезания конечных тегов) я бы посоветовал провести HTML через Tidy, да и вообще лучше не по байту добавлять к строке, а вести переменную и ее инкрементировать, а потом строку обрезать... и обрезать лучше перед тегом, т.е. сделать так:



если попали в тег то
сохраняем значение каунтера

инкрементируем каунтер

когда доходим до макимума
если были внутри тега
возвращаем не каунтер, а сохраненное значение


kernel32:
Вот. Написал с нуля.
Только прошу: больно не бейте, сделал на скорую руку и особо не тестировал... :)

function breakLongWords($str, $maxLength, $endChar){
// Если длина строки меньше $maxLength
if (strlen($str) <= $maxLength) return $str;
// Массив информации о тэгах.
// Для каждого элемента:
// array(имя_тэга, шаблон_начала, закрывающий_тэг);
$tags = array(
array('b', '\[b\]', '[/b]'),
array('i', '\[i\]', '[/i]'),
array('color', '\[color=.*?\]', '[/color]'),
);
// Отрезаем строку по вхождению $endChar...
$str = substr($str, 0, strpos($str, $endChar, $maxLength));
// Активируем тэги, чтобы потом узнать, какие остались незакрыты
$newStr = activateBbTags($str);
// Генерируем паттерн. Получается что-то вроде {\[b\]|\[i\]|\[color=.*?\]}
$pattern = '';
foreach ($tags as $c=>$tInfo) {
$pattern = $pattern.$tInfo[1];
if ($c<count($tags)-1) $pattern = $pattern . "|";
}
$pattern = "{".$pattern."}";
// Теперь ищем открытые тэги, которые надо закрыть.
preg_match_all($pattern, $newStr, $openedTags, PREG_PATTERN_ORDER);
// Ставим элементы массива совпадений в обратном порядке,
// чтобы в правильном порядке закрывать открытые тэги
$openedTags = array_reverse($openedTags[0]);
// теперь $newStr - отрезанный текст без активированных тэгов.
$newStr = $str;
// Ищем, какой тэг открыт, и в конец строки добавляем его закрывающий тэг
foreach ($openedTags as $oTag) {
// Ищем в каждом элементе массива $tags
foreach ($tags as $tInfo) {
// Если нашли, что надо, закрываем тэг и выходим из этого (внутреннего) цикла.
// Внешний цикл продолжается...
if (preg_match('{^'.$tInfo[1].'$}', $oTag)) {
$newStr = $newStr.$tInfo[2];
break;
}
}
}
// Теперь возвращаем результат :) ^_^
return $newStr;
}

function activateBbTags($str) {
$str = preg_replace('{\[b\](.+?)\[/b\]}', '<b>$1</b>', $str);
$str = preg_replace('{\[i\](.+?)\[/i\]}', '<i>$1</i>', $str);
$str = preg_replace('{\[color=(.*?)\](.+?)\[/color\]}', '<font color="$1">$2</font>', $str);
return $str;
}

$text2= 'asdf; [i]lkhj [color=red]skl adfj[/color] sdlfk sdafasd [i] sdfasdf asdklhfjkashd';
echo activateBbTags(breakLongWords($text2, 20, " "));

правда, если вызвать мою функцию так:
echo activateBbTags(breakLongWords($text2, 50, " "));
то в результате получим
asdf; <i>lkhj <font color="red">skl adfj sdlfk sdafasd [i]</i></font>[/i]
Но тут уже дело в "жадности" и "ленивости" квантификаторов...
Надо бы с этим разобраться.
kernel32:
Нашёл еще кое-что: http://ru.php.net/preg_match_all
неизвестный аффтар написал функцию, которая закрывает все html-тэги. Не знаю, на сколько она адекватная.
Можно попробовать использовать её.
/**
* close all open xhtml tags at the end of the string
*
* @author Milian Wolff <[url]http://milianw.de[/url]>
* @param string $html
* @return string
*/
function closetags($html){
#put all opened tags into an array
preg_match_all("#<([a-z]+)( .*)?(?!/)>#iU",$html,$result);
$openedtags=$result[1];

#put all closed tags into an array
preg_match_all("#</([a-z]+)>#iU",$html,$result);
$closedtags=$result[1];
$len_opened = count($openedtags);
# all tags are closed
if(count($closedtags) == $len_opened){
return $html;
}
$openedtags = array_reverse($openedtags);
# close tags
for($i=0;$i<$len_opened;$i++) {
if (!in_array($openedtags[$i],$closedtags)){
$html .= '</'.$openedtags[$i].'>';
} else {
unset($closedtags[array_search($openedtags[$i],$closedtags)]);
}
}
return $html;
}
kernel32:
Посмотрел я, подумал, и понял, что лучше использовать следующий вариант:
function conciseStr($str, $maxLength, $endChar){
if (strlen($str) <= $maxLength) return closeTags(activateBbTags($str));
$str = activateBbTags($str); // Ваша функция, которая активирует нужные бб-тэги
$str = substr($str, 0, strpos($str, $endChar, $maxLength));
$str = closeTags($str); // Эта функция выше
return $str;
}

$text2= 'asdf; [i_tag]lkhj [color_tag=red]skl adfj[/color_tag] sdlfk sdafasd [/i_tag] sdfa sdf asdklhfjkashd';
echo conciseStr($text2, 50, " ");

Хотя в некоторых случаях и так не пойдёт... Но по крайней мере, если супер-пупер навороченных тэгов нет, то этот вариант сойдёт.
WingedFox:
Жизнь куда как проще:

/**
* Function cuts string with the HTML tags by the specified number of chars and strips empty HTML tags from the output.
*
* @param string $txt text to cut
* @param int $len number of chars to keep in the resulting string
* @param string $delim optional string of the stop-chars, used to split the text when limit reached in the middle of the current word
* @return string
* @author Ilya Lebedev
*/
function breakword ($txt,$len,$delim='\s;,.!?:#') {
$txt = preg_replace_callback ("#(</?[a-z]+(?:>|\s[^>]*>)|[^<]+)#mi"
,create_function('$a'
,'static $len = '.$len.';'
.'$len1 = $len-1;'
.'$delim = \''.str_replace("#","\\#",$delim).'\';'
.'if ("<" == $a[0]{0}) return $a[0];'
.'if ($len<=0) return "";'
.'$res = preg_split("#(.{0,$len1}+(?=[$delim]))|(.{0,$len}[^$delim]*)#ms",$a[0],2,PREG_SPLIT_DELIM_CAPTURE);'
.'if ($res[1]) { $len -= strlen($res[1])+1; $res = $res[1];}'
.'else { $len -= strlen($res[2]); $res = $res[2];}'
.'$res = rtrim($res);/*preg_replace("#[$delim]+$#m","",$res);*/'
.'return $res;')
,$txt);
while (preg_match("#<([a-z]+)[^>]*>\s*</\\1>#mi",$txt)) {
$txt = preg_replace("#<([a-z]+)[^>]*>\s*</\\1>#mi","",$txt);
}
return $txt;
}

$text = "<div>some:text;<a href=\"#\">some.url<em > emphased</em> some more url</a> <span > more text</span> ending.</div>";

echo "<pre>";
echo "text: ".htmlspecialchars($text);
echo "<hr />";
echo "len = 1 : ".htmlspecialchars(breakword($text,1));
echo "<hr />";
echo "len = 4 : ".htmlspecialchars(breakword($text,4));
echo "<hr />";
echo "len = 5 : ".htmlspecialchars(breakword($text,5));
echo "<hr />";
echo "len = 6 : ".htmlspecialchars(breakword($text,6));
echo "<hr />";
echo "len = 9 : ".htmlspecialchars(breakword($text,9));
echo "<hr />";
echo "len = 10 : ".htmlspecialchars(breakword($text,10));
echo "<hr />";
echo "len = 11 : ".htmlspecialchars(breakword($text,11));
echo "<hr />";
echo "len = 14 : ".htmlspecialchars(breakword($text,14));
echo "<hr />";
echo "len = 15 : ".htmlspecialchars(breakword($text,15));
echo "<hr />";
echo "len = 16 : ".htmlspecialchars(breakword($text,16));
echo "</pre>";


Результат:

len = 1 : <div>some</div>
len = 4 : <div>some</div>
len = 5 : <div>some</div>
len = 6 : <div>some:text</div>
len = 9 : <div>some:text</div>
len = 10 : <div>some:text</div>
len = 11 : <div>some:text;<a href="#">some</a></div>
len = 14 : <div>some:text;<a href="#">some</a></div>
len = 15 : <div>some:text;<a href="#">some</a></div>
len = 16 : <div>some:text;<a href="#">some.url</a></div>

Фуртуна Максим:
WingedFox Спасибо, решение гениальное. Только не могу понять зачем здесь $len1, если потом происходит rtrim над строкой. Думаю можно $len1 заменить на $len.
WingedFox:
Фуртуна Максим
На здоровье! =)

Посмотрите там preg_split
PahaW:
WingedFox
смотриТЕ, а не проще ли вынести в отдельную функцию ин е лепить в до кучи, не понятно же.
$delim='\s;,.!?:#';

function create2($a){
global $delim;
static $len = '.$len.';
$len1 = $len-1;
$delim = str_replace("#","\\#",$delim);
if ("<" == $a[0]{0}) return $a[0];
if ($len<=0) return "";
$res = preg_split("#(.{0,$len1}+(?=[$delim]))|(.{0,$len}[^$delim]*)#ms",$a[0],2,PREG_SPLIT_DELIM_CAPTURE);
if ($res[1]) {
$len -= strlen($res[1])+1; $res = $res[1];
} else {
$len -= strlen($res[2]); $res = $res[2];
}
$res = rtrim($res);
/*preg_replace("#[$delim]+$#m","",$res);*/
return $res;

}

function breakword ($txt,$len,$delim='\s;,.!?:#') {
$txt = preg_replace_callback ("#(</?[a-z]+(?:>|\s[^>]*>)|[^<]+)#mi", "create2", $txt);
while (preg_match("#<([a-z]+)[^>]*>\s*</\\1>#mi",$txt)) {
$txt = preg_replace("#<([a-z]+)[^>]*>\s*</\\1>#mi","",$txt);
}
return $txt;
}
и не совсем понятна строчка:
$res = preg_split("#(.{0,$len1}+(?=[$delim]))|(.{0,$len}[^$delim]*)#ms",$a[0],2,PREG_SPLIT_DELIM_CAPTURE);
можно ее расшифровать?
PREG_SPLIT_DELIM_CAPTURE
Если этот флаг будет установлен, то введенное выражение в образце разделителя будет захвачено и возвращено также.
я имею ввиду pattern и limit почему 2?
bæv:
PahaW, http://forum.dklab.ru/about/todo/PravilaEtogoForuma-ProchitayteObyazatelno.html — на форуме принято общение «на Вы».
WingedFox:
PahaW
Да можно много чего, но глобальная переменная с разделителем - это бессмысленное изменение, несущее больше вреда, нежели пользы.
Суть рега такова что он захватывает все слова и "остальное", которое должно оставаться на месте.
PahaW:
WingedFox
тоесть массив символов что находят между тегами, как же тогда идет подсчет с длиной, тоже не совсем понятно, ведь между каждыми новыми тегами мы должны отсчитывать, новое кол-во символов.
WingedFox:
PahaW
Посмотрите на рег - он либо режет строчку по разделителю, если требуемая длина приходится на середину текста, либо захватывает весь текст, если длина отрезаемого больше. После этого, корректируется длина для работы со следующим совпадением.
bestsite4u:
мне кажется лучший вариант - это использование регулярных выражение, как написал PahaW

Эта тема на forum.dklab.ru