Готово. Полный `tags.php` — с рандомной формой при каждом рендере (кеш хранит только частоты и формы, а выбор слова происходит при каждом выводе, так что F5 даёт новые слова). $endLen + 2) { if (mb_substr($word, -$endLen, null, 'UTF-8') === $end) { return mb_substr($word, 0, -$endLen, 'UTF-8'); } } } return $word; } } // ============================================================ // 2. Стоп-слова // ============================================================ if (!function_exists('ruStopWords')) { function ruStopWords() { static $words = null; if ($words !== null) return $words; $words = array_flip([ 'в','во','на','за','из','к','ко','с','со','у','о','об','обо','от','ото', 'по','под','подо','над','надо','при','про','для','без','безо','через', 'между','перед','передо','около','возле','вокруг','мимо','сквозь','среди', 'и','а','но','да','или','либо','то','же','бы','чтобы','что','как','когда', 'если','потому','поэтому','так','тоже','также','ни','не','нет', 'я','ты','он','она','оно','мы','вы','они','мне','тебе','ему','ей','нам', 'вам','им','меня','тебя','его','её','нас','вас','их','себя', 'мой','моя','моё','мои','твой','твоя','твоё','твои','наш','наша','наше', 'наши','ваш','ваша','ваше','ваши','свой','своя','своё','свои', 'это','этот','эта','эти','тот','та','те','такой','такая','такое', 'такие','весь','вся','всё','все','сам','сама','само','сами', 'тут','там','здесь','где','куда','откуда','тогда','сейчас','потом', 'очень','ещё','уже','только','даже','ведь','вот','ну','ли','будто', 'можно','нужно','надо','есть','был','была','было','были','будет','будут', 'elvir','ru','http','https','www','com','html','php', ]); return $words; } } // ============================================================ // 3. Построение частотного словаря (с сохранением форм) // ============================================================ if (!function_exists('buildTagCloud')) { function buildTagCloud($pdo, $limit = 30) { $texts = []; try { $stmt = $pdo->query("SELECT message FROM messages"); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $texts[] = $row['message']; } } catch (PDOException $e) { /* таблицы может не быть */ } try { $stmt = $pdo->query("SELECT message FROM projects"); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $texts[] = $row['message']; } } catch (PDOException $e) { /* ... */ } if (empty($texts)) return []; $stop = ruStopWords(); $freq = []; // stem => count $forms = []; // stem => [форма => сколько раз встретилась] foreach ($texts as $raw) { $text = preg_replace('/\[[^\]]+\]/u', ' ', $raw); $text = preg_replace('/https?:\/\/\S+/u', ' ', $text); $text = strip_tags($text); $text = preg_replace('/[^а-яёА-ЯЁ\s]+/u', ' ', $text); $text = mb_strtolower($text, 'UTF-8'); $words = preg_split('/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY); foreach ($words as $w) { if (mb_strlen($w, 'UTF-8') < 3) continue; if (isset($stop[$w])) continue; $stem = ruStem($w); if (mb_strlen($stem, 'UTF-8') < 3) continue; if (!isset($freq[$stem])) { $freq[$stem] = 0; $forms[$stem] = []; } $freq[$stem]++; if (!isset($forms[$stem][$w])) $forms[$stem][$w] = 0; $forms[$stem][$w]++; } } arsort($freq); $top = array_slice($freq, 0, $limit, true); $result = []; foreach ($top as $stem => $count) { $result[$stem] = [ 'count' => $count, 'forms' => $forms[$stem], ]; } return $result; } } // ============================================================ // 4. Кеширование (хранит частоты и формы, рендер — каждый раз разный) // ============================================================ if (!function_exists('buildTagCloudCached')) { function buildTagCloudCached($pdo, $limit = 30, $ttl = 3600) { $cacheDir = $_SERVER['DOCUMENT_ROOT'] . '/cache'; $cacheFile = $cacheDir . '/tags.json'; if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $ttl)) { $data = json_decode(file_get_contents($cacheFile), true); if (is_array($data)) return $data; } $tags = buildTagCloud($pdo, $limit); if (!is_dir($cacheDir)) @mkdir($cacheDir, 0777, true); @file_put_contents($cacheFile, json_encode($tags, JSON_UNESCAPED_UNICODE)); return $tags; } } // ============================================================ // 5. Отрисовка облака (случайная форма при каждом вызове) // ============================================================ if (!function_exists('renderTagCloud')) { function renderTagCloud($tags) { if (empty($tags)) { return '
Пока нет тегов.
'; } $counts = array_column($tags, 'count'); $min = min($counts); $max = max($counts); $range = $max - $min; if ($range == 0) $range = 1; $html = '
'; foreach ($tags as $stem => $info) { $count = $info['count']; $forms = array_keys($info['forms']); if (empty($forms)) continue; // Случайная реальная форма из записей $label = $forms[array_rand($forms)]; $size = 12 + (32 - 12) * ($count - $min) / $range; $size = round($size, 1); $opacity = 0.6 + 0.4 * ($count - $min) / $range; $opacity = round($opacity, 2); $html .= '' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ' '; } $html .= '
'; return $html; } } // ============================================================ // 6. Удобная обёртка // ============================================================ if (!function_exists('renderTagCloudFromDB')) { function renderTagCloudFromDB($pdo, $limit = 30, $ttl = 3600) { $tags = buildTagCloudCached($pdo, $limit, $ttl); return renderTagCloud($tags); } } ``` ## Подключение на главной ```php

Добро пожаловать!

Облако тегов

$tagCloudHtml
HTML; ``` ## CSS ```css .tag-cloud { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center; gap: 8px 14px; padding: 15px 10px; line-height: 1.2; text-align: center; } .tag-cloud-item { color: #78ff87; cursor: default; transition: opacity 0.2s, transform 0.2s; font-weight: 500; } .tag-cloud-item:hover { opacity: 1 !important; transform: scale(1.1); color: #a8ffb3; } .tag-cloud-empty { text-align: center; color: #888; padding: 15px; font-style: italic; } ``` ## Как это работает 1. **Первый заход на страницу.** `buildTagCloudCached` не находит свежего кеша → `buildTagCloud` проходит по всем записям, считает частоты, собирает формы, сохраняет всё в `/cache/tags.json`. 2. **Каждый рендер.** `renderTagCloud` берёт кеш и для каждой основы выбирает **случайную форму** из тех, что реально встречались. Свежий F5 → другой набор слов. 3. **Кеш живёт 1 час.** По истечении — пересчёт частот и форм. Сами слова при рендере меняются всегда. 4. **Формы — настоящие.** «Проект», «проектами», «проекте», «заметку», «заметками» — всё, что встречалось в ваших записях. Никаких сгенерированных «фейковых» слов. ## Мелочи - **Стоп-слова** фильтруются до стемминга, поэтому в облаке их не будет. - **BB-коды, URL, HTML, цифры, латиница** отсекаются — в облако попадают только русские слова. - **Длина от 3 символов** — «дом», «кот» попадут, «и», «в» — нет. - **Счётчик в `title`** — при наведении видно, сколько раз основа встретилась. - **Стили** — если захотите другой цвет или размеры, меняйте `.tag-cloud-item` и диапазон `12 + (32 - 12) * ...`. - **Хотите форму посвежее чаще** — уменьшите `$ttl`: `renderTagCloudFromDB($pdo, 30, 300)` — кеш на 5 минут. - **Хотите сбросить кеш вручную** — удалите `/cache/tags.json`.