Встречались, наверное, с этой задачей — когда из статьи надо вырезать небольшой (а чаще всего, известной длины) кусочек, чтобы сформировать «тизер» или аннотацию. Наиболее правильный подход — предусмотреть для каждого вида аннотации (если статья может быть представлена в разных видах аннотаций, отличающихся, к примеру, длинной) — аннотацию, составленную вручную. Работы для редактора, конечно, прибавится, но и текстовых повторов будет меньше и пользователям будет приятнее.
Но отвлечемся от идеального случая, т.к. на практике аннотацией обычно служит первый параграф статьи. Итак, постановка задачи.
Необходимо выделить из HTML текста фразу (из начала этого текста) «примерно» заданной длины, сохраняя (частично) форматирование статьи.
Я хочу получить функцию следующего вида:
1 |
function teaser_str(text, len, tags); |
где text — исходный html текст, len — примерная длина аннотации в симв. и tags — набор разрешенных html тегов.
Хотелось бы, чтобы фраза не обрывалась на полуслове, а была частью предложения или целым предложением. Рассмотрим пару случаев — один простой и второй посложнее, чтобы вам было из чего выбирать.
Аннотация не содержит частичного форматирования, заданного в статье
То есть в аннотацию не надо переносить HTML теги, следить за тем закрыты ли они и гораздо проще определить длину строки. Параметр tags в этом случае не нужен. Параметр wordBound будет переключать между вариантами концовки аннотации — false — для включения в аннотацию целых предложений, а — true — для получения более точной длины аннотации — вырезаем строку по границе слова.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function teaser_str_easy($text, $len = 50, $wordBound = false) { $text = strip_tags($text); //для начала избавимся от HTML тегов, остаётся простой текст if (mb_strlen($text) < $len) return $text; //текст короче требуемой длины ? - возвратим все, что есть //с помощью регулярного выражения выбираем строку не короче заданной длины, //заканчивающуюся одним из символов - [!?.] //в набор можно добавить запятую, но потом не забыть её исключить из полученной аннотации и показать //разрыв предложения (например, многоточием) if (!$wordBound) { if (preg_match('#^(.{' . $len . '}.*[!?.])#ismuU', $text, $m)) { return $m[1]; } } else { //Если требуется получить более точную длину для аннотации, то можно искать ближайшую границу слова. //Здесь подойдет вот такой вариант с регулярным выражением: if (preg_match('#^(.{' . $len . '}.*)[,\s]#ismuU', $text, $m)) { return $m[1]; } } //если не удалось получить требуемую аннотацию (тогда plan B), //скорее всего не встречается нужных символов из набора [!?.] return $text; } |
Аннотация с частичным форматированием из статьи
Ссылки, эмфазис, жирный шрифт и т.п. — все это можно перенести из статьи в аннотацию. При этом нужно решить две задачи — аккуратно посчитать длину строки, не включая туда участки html тегов и правильно учитывая случаи встречающихся html-кодов символов вроде — «"». А вторая задача — правильно закрыть открытые теги, т.к. строка нужной длины может быть уже найдена, а теги включенные в строку оказались не сбалансированы.
Первую задачу я решаю «вручную» — составляю «автомат с памятью состояния». Вторая давно задача решена и её в готовом виде я возьму из исходного кода движка WordPress ;). Там есть функция балансировки тегов, которая как раз нам подходит — force_balance_tags.
Вот что получается:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
function teaser_str($text, $len, $tags = '<p><a><i><br>') { //функция strip_tags может избирательно вырезать теги - воспользуемся этой особенностью :) $str = strip_tags($text, $tags); //текст короче требуемой длины ? - вернем его if (mb_strlen(strip_tags($str)) < $len) return $str; //специальный "автомат с памятью" по-символьно читает текст, //мы выделяем законченное предложение или фразу $i = 0; $inTag = false; //мы внутри тега $inStr = false; //мы внутри строки $inEnti = false; //мы внутри html-кода символа $canStop = 0; //найден знак препинания - можно остановиться $result = ''; //аккумулятор результата $resultLength = 0; //длина строки без учета кода тегов //подробно пояснять не стану работу этого автомата //логика основана на текущих состояниях while($resultLength < $len || !$canStop || $inTag) { $c = mb_substr($str, $i++, 1); if ($c === '') break; switch($c) { case '<': if (!$inStr) $inTag = true; break; case '>': if (!$inStr) $inTag = false; break; case '"': if ($inTag) $inStr = !$inStr; break; case '&': if (!$inStr) $inEnti = true; break; //найден знак конца предложения, но мы зависим от контекста case '.': case '!': case '?': $canStop = true; break; case ';': if ($inEnti) { $inEnti = false; break; } case ',': $canStop = true; break; default: $canStop = false; } if (!$inTag) $resultLength ++; $result .= $c; } //случай окончания предложения на запятую или точку с запятой if ($c !== '' && strpos(",;", $c) !== false) { $result = substr($result, 0, -1) . ' ...'; } return force_balance_tags($result); } /* а эта функция взята из WORDPRESS сохранены оригинальные комментарии разработчика на английском языке она занимается балансировкой тегов */ function force_balance_tags( $text ) { $tagstack = array(); $stacksize = 0; $tagqueue = ''; $newtext = ''; $single_tags = array( 'br', 'hr', 'img', 'input' ); // Known single-entity/self-closing tags $nestable_tags = array( 'blockquote', 'div', 'span', 'q' ); // Tags that can be immediately nested within themselves // WP bug fix for comments - in case you REALLY meant to type '< !--' $text = str_replace('< !--', '< !--', $text); // WP bug fix for LOVE <3 (and other situations with '<' before a number) $text = preg_replace('#<([0-9]{1})#', '<$1', $text); while ( preg_match("/<(\/?[\w:]*)\s*([^>]*)>/", $text, $regex) ) { $newtext .= $tagqueue; $i = strpos($text, $regex[0]); $l = strlen($regex[0]); // clear the shifter $tagqueue = ''; // Pop or Push if ( isset($regex[1][0]) && '/' == $regex[1][0] ) { // End Tag $tag = strtolower(substr($regex[1],1)); // if too many closing tags if( $stacksize <= 0 ) { $tag = ''; // or close to be safe $tag = '/' . $tag; } // if stacktop value = tag close value then pop else if ( $tagstack[$stacksize - 1] == $tag ) { // found closing tag $tag = '</' . $tag . '>'; // Close Tag // Pop array_pop( $tagstack ); $stacksize--; } else { // closing tag not at top, search for it for ( $j = $stacksize-1; $j >= 0; $j-- ) { if ( $tagstack[$j] == $tag ) { // add tag to tagqueue for ( $k = $stacksize-1; $k >= $j; $k--) { $tagqueue .= '</' . array_pop( $tagstack ) . '>'; $stacksize--; } break; } } $tag = ''; } } else { // Begin Tag $tag = strtolower($regex[1]); // Tag Cleaning // If self-closing or '', don't do anything. if ( substr($regex[2],-1) == '/' || $tag == '' ) { // do nothing } // ElseIf it's a known single-entity tag but it doesn't close itself, do so elseif ( in_array($tag, $single_tags) ) { $regex[2] .= '/'; } else { // Push the tag onto the stack // If the top of the stack is the same as the tag we want to push, close previous tag if ( $stacksize > 0 && !in_array($tag, $nestable_tags) && $tagstack[$stacksize - 1] == $tag ) { $tagqueue = '</' . array_pop ($tagstack) . '>'; $stacksize--; } $stacksize = array_push ($tagstack, $tag); } // Attributes $attributes = $regex[2]; if( !empty($attributes) ) $attributes = ' '.$attributes; $tag = '<' . $tag . $attributes . '>'; //If already queuing a close tag, then put this tag on, too if ( !empty($tagqueue) ) { $tagqueue .= $tag; $tag = ''; } } $newtext .= substr($text, 0, $i) . $tag; $text = substr($text, $i + $l); } // Clear Tag Queue $newtext .= $tagqueue; // Add Remaining text $newtext .= $text; // Empty Stack while( $x = array_pop($tagstack) ) $newtext .= '</' . $x . '>'; // Add remaining tags to close // WP fix for the bug with HTML comments $newtext = str_replace("< !--","<!--",$newtext); $newtext = str_replace("< !--","< !--",$newtext); return $newtext; } |