Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > PHP: Тексты > Как обрезать строку с форматированием


Автор: Gold Dragon 17.4.2010, 12:14
Собственно вопрос...

Допустим существует стока с форматирование типа
Код

<h3>Уважаемые посетители, на наших страницах в апреле ... </h3>
<p align="justify">
<span style="color: #000000">&laquo;Профессиональные ОПРОСНИКИ&raquo;</span>, кого-то они направят в выборе профессии, кому-то <b>они</b> помогут оценить возможность перемен для себя!
</p>

Мне нужно обрезать строку и оставить допустим 150 символов. Как это можно сделать сохранив форматирование?

Добавлено через 2 минуты и 49 секунд
как-то странно создалась тема :( попробую продублировать


Собственно вопрос...

Допустим существует стока с форматирование типа
Код

<h3>Уважаемые посетители, на наших страницах в апреле ... </h3>
<p align="justify">
<span style="color: #000000">&laquo;Профессиональные ОПРОСНИКИ&raquo;</span>, кого-то они направят в выборе профессии, кому-то <b>они</b> помогут оценить возможность перемен для себя!
</p>


Мне нужно обрезать строку и оставить допустим 150 символов. Как это можно сделать сохранив форматирование? 

Автор: Photon 17.4.2010, 19:18
Ну в голову приходит только совершенно дебильный метод: проход по строке, при обнаружении открывающего тега сохранить в массиве, что он был, если нашли такой же закрывающий - удалить из массива. Как только дошли до нужной позиции - добавляем закрывающие теги на всё, что осталось в массиве тегов

Автор: smartov 17.4.2010, 20:12
Рекурсивная функция, которая вырезает 150 символов, смотрит нету ли в них тегов, отрезает от изначальной строки  и прибавляет к конечной еще кусок длиной в "длина тегов". И так пока текст кроме тегов не станет 150 символов.
После чего надо пробежаться и закрыть все теги.

Автор: Gold Dragon 18.4.2010, 09:42
а примерчик можно, а то что-то не въеду smile не очень я со всякими регулярками

Автор: ArchNix 18.4.2010, 10:21
Примерчик  smile 
Код

$TEST =<<<HTML
<h3>Уважаемые посетители, на наших страницах в апреле ... </h3>
<p align="justify">
<span style="color: #000000">&laquo;Профессиональные ОПРОСНИКИ&raquo;</span>, кого-то они направят в выборе профессии, кому-то <b>они</b> помогут оценить возможность перемен для себя!
</p>
HTML;

$charset = "UTF-8";

mb_regex_encoding($charset);
mb_ereg_search_init($TEST, "([А-я]*)");

$r = mb_ereg_search_getregs();
do {
  $r = mb_ereg_search_regs();
  $arr[] = $r[1];
} while($r);

print_r($arr);

Автор: brother79 19.4.2010, 12:26
1) Тупо отсчитываешь 150 символов, пропуская встретивиеся теги.
2) Если последний тег не закрытый, то 2 вар-та есть, либо закрыть самому, либо обрезать весь

Автор: Gold Dragon 19.4.2010, 13:13
ArchNix, попробую что за код... smile


brother79, это не выход. Может быть куча вложенных тегов которые не закроются. И тогда весь сайт поплывёт

Автор: brother79 20.4.2010, 05:08
Цитата(Gold Dragon @  19.4.2010,  17:13 Найти цитируемый пост)
brother79, это не выход. Может быть куча вложенных тегов которые не закроются. И тогда весь сайт поплывёт 


Да, кстати, они же могут быть вложенные. Ну тогда считай открывающиеся и закрывающиеся

1) пиши полноценный парсер
2) сделай что-то типа 

Код

if (preg_match_all('/([^\<]*)(\<[^\>]+\>)/', $s, $m)) {
  print_r($m)
}


Далее в $m уже будут выделены отдельно все теги и текст между ними, останется только пройтись по этому массиву, считая символы и запоминая какие теги открываются и закрываются, скажем добавляя их в другой массив и удаляя из него при закрытии, а в конце, те, что не удалённые  - закрыть и удалить из массива.

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

Автор: Gold Dragon 20.4.2010, 08:00
в общем начал колдовать.. Но столкнулся с ещё одной бедой.. А если есть таблица? Тут не только нужно считать теги но и структуру считать.. В общем ужас smile

Автор: SickFxck 20.4.2010, 10:10
Gold Dragon, какая разница — таблица у тебя или что-то другое? Какой ты алгоритм сам для себя выбрал?

Автор: Gold Dragon 20.4.2010, 10:12
разница есть, например, если используются colspan или rowspan. Или что-то типа <col> у которого нет </col>

Автор: smartov 20.4.2010, 10:45
Цитата(Gold Dragon @  20.4.2010,  10:12 Найти цитируемый пост)
Или что-то типа <col> у которого нет </col> 

Все теги имеют закрывающий тег. 
То, что для некоторых его можно не писать - это послабляющий фактор, но никак не мешает тебе его закрыть.

Добавлено через 2 минуты и 6 секунд
Черт, подумал и понял проблему...
То есть выкусил типа
Код

<col>BlahBlah
<tr><td>Blah2


И по логике, получается, что col надо закрыть smile
Сложно... Чуть ли не парсер писать надо. 

Автор: Gold Dragon 20.4.2010, 11:24
вот именно, писать парсинг... :( ну или сделать ограничения не теги

Автор: brother79 20.4.2010, 11:24
Цитата(Gold Dragon @  20.4.2010,  12:00 Найти цитируемый пост)
в общем начал колдовать.. Но столкнулся с ещё одной бедой.. А если есть таблица? Тут не только нужно считать теги но и структуру считать.. В общем ужас smile 


Если у тебя документ корректный - то ты знаешь, какие теги должны иметь закрывающийся, а какие нет, например <br>, если же какой-то м.... пишет не закрытый <td> или ещё чего, ты на это универсальную прогу не придумаешь. Я бы предложил - нормальный код парсить или делать препроход и закрывать те же <td> перед <tr>

Автор: Gold Dragon 20.4.2010, 11:55
я пишу модуль вывода краткого содержимого старей/новостей  для CMS. И поэтому модуль будут использовать как угодно и где угодно...

скорее всего нужно писать с простого и по мере новых версий вводить новые теги.. другого выхода пока не вижу. Пока будут получаться неоправданные затраты

Автор: ksnk 20.4.2010, 12:18
Gold Dragon, Есть мысль - заставить юзера(юзеровый броузер) самого впилить все нужные теги...
Новости - это кусочки откопипащеных html'ей? Тогда нужно просто вставить этот текст в какой-то фрейм в html документе, на клиентской стороне у самого "редактора", а потом брать с собой на сервер то, что броузер уже успел "открыть-закрыть". Просто у броузера уже есть механизм исправления корявого кода и им вполне можно пользоваться  smile Заодно это будет последним китайским предупреждением редактору новости - действительно ли вот это и есть то, что хочется показать народу?...

C кодом на сервере нужно, конечно, проделать много дополнительных телодвижений - stripTags для непонятных тегов, очистка понятных тегов от странного и лишнего форматирования и протчий клининг...

Автор: SickFxck 21.4.2010, 21:08
Цитата(smartov @  20.4.2010,  10:45 Найти цитируемый пост)
Черт, подумал и понял проблему...
То есть выкусил типа
<col>BlahBlah
<tr><td>Blah2

Я до сих пор не вижу проблемы. В данном случае вы просто неправильно "откусили" вот и всё. "Кусать" надо до тех пор, пока не закроются все теги, попавшие в текст, длиной 150 символов. Да, строка может получиться длиннее 150-ти символов, но зато тут виден какой-то алгоритм.

Ещё раз спрашиваю: какой алгоритм выбрал тредстартер?

Добавлено через 3 минуты и 45 секунд
Сложнее всего ответить на вопрос: «а что вообще такое «подстрока гипертекста»? Что вообще вы хотите увидеть, попытавшись получить «150 символов таблицы»? Я смысла в этом вообще вижу мало.

Автор: smartov 21.4.2010, 21:39
Цитата(SickFxck @  21.4.2010,  21:08 Найти цитируемый пост)
надо до тех пор, пока не закроются все теги, попавшие в текст, длиной 150 символов. Да, строка может получиться длиннее 150-ти символов, но зато тут виден какой-то алгоритм.

Очень класно. Обрежем 150 символов, но не 150 символов. 
Соответствие решения техзаданию даже близко не валяется. 


Цитата(SickFxck @  21.4.2010,  21:08 Найти цитируемый пост)
Я до сих пор не вижу проблемы. 

А я вижу. Раз просят 150, надо сделать все, чтобы было максимально близко, а не делать как получится, потому что так удобнее

Автор: SickFxck 21.4.2010, 21:50
smartov, ответь на вопросы:
Цитата(SickFxck @  21.4.2010,  21:08 Найти цитируемый пост)
Сложнее всего ответить на вопрос: «а что вообще такое «подстрока гипертекста»? Что вообще вы хотите увидеть, попытавшись получить «150 символов таблицы»? Я смысла в этом вообще вижу мало. 

А уже только потом можно будет поговорить о реализации.

Автор: smartov 22.4.2010, 07:17
SickFxck, мне задание очевидно - его бизнес логика сродни тому, почему у нас на винграде при нажатии кнопки "быстрая цитата" скрипт вставляет цитируемый текст без форматирования, а должен бы с ним. Если у тебя еще есть вопросы - задавай их ТС, а не мне.

Автор: SickFxck 22.4.2010, 07:51
Цитата(smartov @  22.4.2010,  07:17 Найти цитируемый пост)
мне задание очевидно - его бизнес логика сродни тому, почему у нас на винграде при нажатии кнопки "быстрая цитата" скрипт вставляет цитируемый текст без форматирования, а должен бы с ним

То, что ты не можешь точно описать, не существует.

Цитата(smartov @  22.4.2010,  07:17 Найти цитируемый пост)
Если у тебя еще есть вопросы - задавай их ТС, а не мне. 

Тогда просьба не оценивать чужие предложения на соответствие "техзаданию" автора:
Цитата(smartov @  21.4.2010,  21:39 Найти цитируемый пост)
Соответствие решения техзаданию даже близко не валяется. 

Автор: smartov 22.4.2010, 14:23
Цитата(SickFxck @  22.4.2010,  07:51 Найти цитируемый пост)
Тогда просьба не оценивать чужие предложения на соответствие "техзаданию" автора:

Согласен. Прошу прощения. Может автору действительно подходит твой алгоритм.

Автор: Gold Dragon 27.4.2010, 07:57
Извините. был занят... Может я не совсем правильно всем объяснил... вот на примере

есть примеры следующего контекста

Код

<table>
<tr><td>Строка 1</td></tr>
<tr><td>Строка 2</td></tr>
<tr><td>Строка 3</td></tr>
<tr><td>Строка 4</td></tr>
<tr><td>Строка 5</td></tr>
<tr><td>Строка 6</td></tr>
</table>


Код

<ul>
<li>Строка 1</li>
<li>Строка 2</li>
<li>Строка 3</li>
<li>Строка 4</li>
<li>Строка 5</li>
<li>Строка 6</li>
</ul>


Код

<p>Строка 1</p>
<p>Строка 2</p>
<p>Строка 3</p>
<p>Строка 4</p>
<p>Строка 5</p>
<p>Строка 6</p>


Сам текст может иметь любые стили форматирования. И как видно, нужно не только закрывать теги, но и "научить" парсер в каком порядке их закрывать. И строка само собой получится более 150 символов.. Нужно сохранять 150 текста smile

Добавлено через 2 минуты и 48 секунд
интересный вариант предложил brother79, осталось понять логику как это реализовать

Автор: SelenIT 27.4.2010, 09:59
Если в лоб, можно попробовать тупо вырезать без учета тегов (напр., регуляркой типа '/([^<>](<[^<>]*>)*){0,150}/s'), а на результат натравить какой-нибудь tidy_repair_string (пусть сам чинит покоцаную разметку;) и из результата вырезать всё между <body>...</body>. Возможно, как вариант, вместо Tidy - DOMDocument::loadHTML (не проверял, поэтому не уверен).

Но вообще подход рискованный. Если это для новых статей/новостей, которые вводятся через WYSIWYG-редактор, лично я бы сделал как-нибудь так: для краткого содержания завел бы отдельное поле (тоже WYSIWYG'овое) и по достижении нужного лимита значением innerText.length/textContent.length основного поля копировал бы innerHTML основного поля туда яваскриптом, оставив редактору новости возможность вручную поменять его на что-то более осмысленное (напр., заменить обрезок таблицы на краткую аннотацию к ней).

Автор: SickFxck 27.4.2010, 15:10
Через стек просто делается:
Код

function htmlSubstr($html, $length)
{
    $out = '';
    $arr = preg_split('/<(.+?)>/s', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
    $tagStack = array();

    for($i = 0, $l = 0; $i < count($arr); $i++) {
        if( $i & 1 ) {

            if( $arr[$i][0] == '/' ) {
                array_pop($tagStack);
            } elseif( substr($arr[$i], -1) != '/' ) {
                array_push($tagStack, $arr[$i]);
            }

            $out .= '<' . $arr[$i] . '>';

        } else {
            
            if( ($l += strlen($arr[$i])) >= $length ) {
                $out .= substr($arr[$i], 0, $l - $length);
                break;
            } else {
                $out .= $arr[$i];
            }
     
        }
    }

    while( ($tag = array_pop($tagStack)) !== NULL )
    {
        $out .= '</' . $tag . '>';
    }

    return $out;
}

Автор: SelenIT 27.4.2010, 16:49
Кстати,
Цитата(smartov @  20.4.2010,  10:45 Найти цитируемый пост)
Все теги имеют закрывающий тег. 
То, что для некоторых его можно не писать - это послабляющий фактор, но никак не мешает тебе его закрыть.

справедливо для XML (включая XHTML как application/xhtml+xml), но не для HTML (включая XHTML как text/html). В HTML для пустых (принципиально не имеющих содержимого) тегов закрывающий тег именно что http://www.w3.org/TR/html4/index/elements.html. И <col> - как раз один из таких тегов;)

По сабжу, в первом приближении работает и такое:
Код

    $limit = 150;
    preg_match('/((<[^<>]*>)*(\s+|(&#?\w+;)|[^<>\s])){0,'.$limit.'}/su', $text, $m);
    $html = tidy_repair_string($m[0].'&hellip;',array(),'utf8');
    preg_match('/<body>(.*)<\/body>/isu', $html, $m);
    echo $m[1];

Закрывает всё вроде как надо (проверил на примерах из топика). Правда, нужна поддержка Tidy на сервере и кодировка utf-8 (с cp1251 у меня не получилось). И сбитые rowspan/colspan таблиц не лечит (в принципе, пустяк, таблица-то закрыта, вёрстка дальше не поплывёт)...

Автор: Gold Dragon 28.4.2010, 07:29
что такое Tidy?

Автор: Gold Dragon 28.4.2010, 08:15
SickFxck, твой код практически отлично работает.. не пробовал на таблицах, но с другими элементами вроде пащет.. Правда <IMG> не проверил... А так всё открывается и закрывается.. 

НО(!)

- задаваемый размер не соответствует реальному тексту
- не учтено всякие специальные символы типа &laquo;

SickFxck, можешь пояснить что и как делается в коде? что-то я запутался smile

вот для примера код
Код

<?php

$s =<<<HTML
<h3>Уважаемые посетители, на <u>наших</u> страницах в апреле ... </h3>
<p align="justify">
<span style="color: #ff0000">&laquo;Професс<i>иона</i>льные ОПРОСНИКИ&raquo;</span>, кого-то они <i>направят</i> в выборе <b>профессии</b>, кому-то <b>они</b> помогут оценить возможность перемен для себя!
</p>
HTML;
$aa = htmlSubstr($s, 10);

echo $aa;
echo '<br>----------------<br>'.strlen(strip_tags($aa));

function htmlSubstr($html, $length)
{
    $out = '';
    $arr = preg_split('/<(.+?)>/s', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
    $tagStack = array();
    for($i = 0, $l = 0; $i < count($arr); $i++) {
        if( $i & 1 ) {
            if( $arr[$i][0] == '/' ) {
                array_pop($tagStack);
            } elseif( substr($arr[$i], -1) != '/' ) {
                array_push($tagStack, $arr[$i]);
            }
            $out .= '<' . $arr[$i] . '>';
        } else {
            
            if( ($l += strlen($arr[$i])) >= $length ) {
                $out .= substr($arr[$i], 0, $l - $length);
                break;
            } else {
                $out .= $arr[$i];
            }
     
        }
    }
    while( ($tag = array_pop($tagStack)) !== NULL )
    {
        $out .= '</' . $tag . '>';
    }
    return $out;
}
?>

Автор: SickFxck 28.4.2010, 08:55
Цитата(Gold Dragon @  28.4.2010,  08:15 Найти цитируемый пост)
- задаваемый размер не соответствует реальному тексту
- не учтено всякие специальные символы типа &laquo;

Скорее всего это одна и та же проблема.

Вечером выложу исправленный вариант, если ничего другого не найдёшь.

Добавлено через 53 секунды
А если ты работаешь с UTF-8, то там нужно mb-функции использовать.

Автор: Gold Dragon 28.4.2010, 08:58
SickFxck, прокомментируй код.. Хочу разобраться smile 


Автор: SelenIT 28.4.2010, 11:39
Цитата(Gold Dragon @  28.4.2010,  07:29 Найти цитируемый пост)
что такое Tidy? 

Фактически http://php.net/tidy.

Цитата(Gold Dragon @  28.4.2010,  08:15 Найти цитируемый пост)
не учтено всякие специальные символы типа &laquo;

У меня учтено;)
Цитата

'/((<[^<>]*>)*(\s+|(&#?\w+;)|[^<>\s])){0,'.$limit.'}/su'

Автор: SickFxck 28.4.2010, 13:06
Код

function htmlSubstr($html, $length)
{
    $out = '';
    $arr = preg_split('/(<.+?>|&#?\\w+;)/s', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
    $tagStack = array();

    for($i = 0, $l = 0; $i < count($arr); $i++) {
        if( $i & 1 ) {
            if( substr($arr[$i], 0, 2) == '</' ) {
                array_pop($tagStack);
            } elseif( $arr[$i][0] != '&' && substr($arr[$i], -2) != '/>' ) {
                array_push($tagStack, $arr[$i]);
            }

            if( $arr[$i][0] == '&' )
                $l++;

            $out .= $arr[$i];
        } else {
            if( ($l += strlen($arr[$i])) >= $length ) {
                $out .= substr($arr[$i], 0, $l - $length);
                break;
            } else {
                $out .= $arr[$i];
            }
        }
    }

    while( ($tag = array_pop($tagStack)) !== NULL ) {
        $out .= '</' . substr($tag, 1);
    }

    return $out;
}

Автор: SickFxck 28.4.2010, 14:40
Цитата(Gold Dragon @  28.4.2010,  08:58 Найти цитируемый пост)
SickFxck, прокомментируй код.. Хочу разобраться 

Если кратко, то, встречая открывающий тег (парный), мы его пихаем в стек, если встретился закрывающий — выталкиваем. Если длина строки превысила допустимое количество — прерываем цикл. После выталкиваем все незакрытые теги.

Я пользуюсь тем, что preg_split($pattern, $subject, -1, PREG_SPLIT_DELIM_CAPTURE) возвращает массив в виде чередующихся строк и подстрок, подходящих под шаблон. То есть, тег всегда будет в нечётных индексах массива, а простой текст — в чётных.

Да и ещё один фикс:
Код

    while( ($tag = array_pop($tagStack)) !== NULL ) {
        $out .= '</' . strtok(substr($tag, 1), " \t>") . '>';
    }


P.S. И заметь, что, например, "&amp;" я считаю одним символом ("&"), а не 5-ю.

Автор: Gold Dragon 29.4.2010, 07:39
боюсь что Tidy не могу использовать. не на всех серверах он установлен. Проверил у своих хостеров, только у одного стоит :(

Автор: Gold Dragon 29.4.2010, 10:37
никак не пойму почему количество заданных символов не соответствует реальной обрезке :(
и разница колеблется в случайном порядке, как в большую сторону так и в меньшую

Автор: SickFxck 29.4.2010, 11:18
Цитата(Gold Dragon @  29.4.2010,  10:37 Найти цитируемый пост)
никак не пойму почему количество заданных символов не соответствует реальной обрезке

Ты про чей вариант?

Свой я уже пояснил:
Цитата(SickFxck @  28.4.2010,  14:40 Найти цитируемый пост)
P.S. И заметь, что, например, "&amp;" я считаю одним символом ("&"), а не 5-ю.

Поэтому нельзя делать проверку с помощью strlen().

Ах да. Действительно есть кое-что. Окончательный вариант:
Код

function htmlSubstr($html, $length)
{
    $out = '';
    $arr = preg_split('/(<.+?>|&#?\\w+;)/s', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
    $tagStack = array();

    for($i = 0, $l = 0; $i < count($arr); $i++) {
        if( $i & 1 ) {
            if( substr($arr[$i], 0, 2) == '</' ) {
                array_pop($tagStack);
            } elseif( $arr[$i][0] == '&' ) {
                $l++;
            } elseif( substr($arr[$i], -2) != '/>' ) {
                array_push($tagStack, $arr[$i]);
            }

            $out .= $arr[$i];
        } else {
            if( ($l += strlen($arr[$i])) >= $length ) {
                $out .= substr($arr[$i], 0, $length - $l + strlen($arr[$i]));
                break;
            } else {
                $out .= $arr[$i];
            }
        }
    }

    while( ($tag = array_pop($tagStack)) !== NULL ) {
        $out .= '</' . strtok(substr($tag, 1), " \t>") . '>';
    }

    return $out;
}


Код

function htmlStrlen($html)
{
    return strlen(preg_replace('/&#?\\w+;/', '?', strip_tags($html)));
}

assert(htmlStrlen(htmlSubstr('HTML-<strong>string</strong>', 6)) == 6);
assert(htmlStrlen(htmlSubstr('&amp;&#xD7;&amp;', 2)) == 2);
assert(htmlStrlen(htmlSubstr('<p class="p1">test &amp; test </p>', 5)) == 5);

Автор: Gold Dragon 29.4.2010, 11:52
теперь код отлично работает на этом этапе smile Но это при условии что код валидный

если есть <img...> а не <img ... /> то появится </img> smile

а можно как то включить в условия чтобы если встретится например тег <myqqq> то просто его игнорировать и оставить в коде?

Автор: SickFxck 29.4.2010, 11:58
Делай массив таких тегов
Код

$single = array('img', ...);


Добавь условие здесь:
Код
} elseif( substr($arr[$i], -2) != '/>' ) {

substr(), strtok(), strtolower(), in_array()...

Автор: SelenIT 29.4.2010, 13:55
Цитата(Gold Dragon @  29.4.2010,  07:39 Найти цитируемый пост)
Проверил у своих хостеров, только у одного стоит :( 

Я думаю, адекватного хостера можно попросить его добавить. Всё-таки стандартные решения надежнее, имхо (хотя бы тем, что не надо самому заводить массивы пустых тегов и разрешенных тегов)...

Автор: Gold Dragon 29.4.2010, 15:38
SelenIT, согласен с тобой.. Но не всё так просто ... ни каждый хостер будет доставлять библиотеки по желанию пользователей...

хотя я думаю нужно и этот вариант сделать, а в настройках давать пользователю вариант выбирать... 

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)