Список пользователей

1
Админ
Постов: 101
2
Элита
Постов: 34
3
Элита
Постов: 28
4
VIP
Постов: 26
5
Дизайнер
Постов: 25
6
Пользователи
Постов: 25
7
Пользователи
Постов: 24
8
Проверенные
Постов: 21

  • Страница 1 из 1
  • 1
Парсер последних комментариев форума на главную UCOZ
Дата: Суббота, 25.10.2025, 21:02 | Сообщение # 1 | | Написал: Начинающий
Автор темы
Мурчанн не в сети
        Сообщений:101
         Регистрация:20.10.2016

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



Если копнуть глубже, то сам принцип работы — это асинхронное извлечение HTML-сущностей с целевого ресурса, их структурирование и рендеринг в локальный DOM браузера. При этом используется цепочка функций:

1. fetchHTML(url) наш HTTP-клиент уровня браузера, выполняющий кросс-доменные (или же same-origin) запросы с контролем кэша и учётом авторизационных credentials. Здесь нет никакой магии обычный fetch API, но в рамках строгого изолированного контекста.

2. DOMParser инструмент для трансформации полученного HTML в структурированный документ, где каждый элемент можно адресовать селекторами CSS и манипулировать им как объектом. То есть мы превращаем поток HTML в полноценный объектно-ориентированный «mini-DOM» для дальнейшей обработки.

3. cleanText и TreeWalker это уже интеллектуальный слой фильтрации. TreeWalker позволяет обходить текстовые узлы, игнорируя скрипты, стили, и вложенные элементы, которые не должны попасть в пользовательский интерфейс. В результате мы получаем максимально «чистый» контент для вывода.

4. Кэширование через localStorage с таймстампом здесь вступает микро-интеллект. Вместо того, чтобы перезагружать страницу при каждом посещении, мы храним состояние данных на стороне клиента, проверяем его актуальность (30 минут) и обновляем скрыто, без влияния на UI. Это не просто экономия ресурсов , это эмитация поведения полноценного API-клиента, только без лишних серверных вызовов.

5. Интерактивный рендеринг комментариев каждый блок становится элементом управления, при клике на который происходит динамическая подгрузка полного содержания темы в том же DOM-блоке. То есть мы имитируем SPA (Single Page Application), где переходы между страницами остаются внутри одного интерфейса. Пользователь видит мгновенный отклик без перезагрузки.

А теперь самое смешное: платформа UCOZ предлагает для такой простейшей задачи многоуровневый API с ключами и целыми модулями uAPI, где разработчики добавили поддержку PHP, авторизации, и прочую бюрократию, как будто мы строим космический шаттл, чтобы просто вывести последние пять комментариев.

Любители усложнять жизнь это, конечно, весело, но мы-то знаем: все эти «умные» системы работают ровно так же, как наш мини-парсер, только с десятком лишних обёрток и проверок.

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

А если кому-то кажется, что нужно подключать полноценный API, это просто смешно. 2

Ахахах ,это прям классика «от простого к эпичному» 😄.

Началось с «вывести последние комментарии», а кончилось почти что мини-планшетом с интерактивным интерфейсом, кэшированием и имитацией мессенджера!

Создаем глобальный блок и вставляем куда угодно .

Код
<div id="chat-tablet-wrapper" style="all: initial; display:block; width:100%; max-width:780px; margin:50px auto; font-family: 'Segoe UI', Tahoma, sans-serif;">

  <!-- Рама планшета -->
  <div style="border:3px solid #333; border-radius:24px; padding:20px; background:#000; position:relative;">
    
    <!-- Верхняя панель планшета -->
    <div style="height:20px; display:flex; justify-content:center; align-items:center; gap:6px; margin-bottom:8px;">
      <div style="width:80px; height:5px; background:#555; border-radius:3px;"></div>
    </div>

<!-- Панель мессенджера -->
<div id="chat-header" style="
  display:flex;
  align-items:center;
  gap:10px;
  padding:4px 8px;
  background:#075E54;
  color:#fff;
  border-radius:12px 12px 0 0;
  font-family: 'Segoe UI', Tahoma, sans-serif;
">
  <img id="chat-user-avatar" src="https://jordan.moy.su/ava/avapm.png" style="width:40px; height:40px; border-radius:50%; object-fit:cover;">
  <div style="flex-grow:1;">
    <strong id="chat-user-name">Аноним</strong><br>
    <span id="chat-user-status" style="font-size:11px; opacity:0.8;">offline</span>
  </div>
  <div style="display:flex; gap:6px;">
    <span style="cursor:pointer;">📞</span>
    <span style="cursor:pointer;">🎥</span>
    <span style="cursor:pointer;">⋮</span>
  </div>
</div>

    <!-- Экран планшета с фоном -->
    <div id="last-comments-container" style="
      border-radius:0px;
      height:400px;
      overflow-y:auto;
      padding:10px;
      display:flex;
      flex-direction:column;
      gap:10px;
      background-image: url('https://avatars.mds.yandex.net/i?id=f29c1e98869aa6740546c6da7110bee3_l-5313038-images-thumbs&n=13');
      background-size: cover;
      background-position: center;
      background-repeat: no-repeat;
    ">
      <!-- Комментарии будут здесь -->

    </div>

    <!-- Подпись Samsung -->
    <div style="text-align:center; color:#fff; font-size:14px; margin-top:8px; font-family:Arial, sans-serif;">
      SAMSUNG
    </div>

  </div>
</div>

<style>
/* Сообщения в стиле мессенджера */
.comment-bubble {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  max-width: 70%;
  cursor: pointer;
  transition: background 0.2s;
}

.comment-bubble:hover .comment-content {
  background: rgba(255,255,255,0.25);
}

.comment-bubble img {
  width:40px;
  height:40px;
  border-radius:50%;
  flex-shrink:0;
  object-fit:cover;
}

.comment-content {
  background: rgba(220,248,198,0.8); /* прозрачный зеленый */
  padding:8px 12px;
  border-radius:14px;
  box-shadow:0 1px 4px rgba(0,0,0,0.1);
  position:relative;
  word-break: break-word;
  font-size:13px;
  transition: background 0.2s;
}

.comment-content strong {
  display:block;
  font-weight:600;
  margin-bottom:2px;
  color:#075E54;
}

.comment-content span {
  font-size:13px;
  color:#333;
}

#last-comments-container {
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE 10+ */
}

#last-comments-container::-webkit-scrollbar {
  display: none; /* Chrome, Safari, Opera */
}

</style>

<script>
(async function() {
  const container = document.getElementById('last-comments-container');
  const BASE_FORUM_URL = '/forum/0-0-1-34';
  const MAX_TEXT = 260;
  const MAX_COMMENTS = 5;
  const CACHE_LIFETIME = 30 * 60 * 1000; // 30 минут

  function toAbs(url){ return url.startsWith('http') ? url : window.location.origin + url; }
  function escapeHtml(s){ return String(s||'').replace(/[&<>"']/g, m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); }

  function cleanText(postBodyTd, maxLength=MAX_TEXT){
    if(!postBodyTd) return '';
    const commentEl = postBodyTd.querySelector('div[itemprop="commentText"]');
    if(!commentEl) return '';
    let text='';
    const walker = document.createTreeWalker(commentEl, NodeFilter.SHOW_TEXT);
    let node;
    while(node = walker.nextNode()){
      if(node.parentNode.closest('script, style, iframe, img, a')) continue;
      let t = node.nodeValue.replace(/[\t\n\r\u00A0]+/g,' ').trim();
      if(t) text += t + ' ';
    }
    return text.trim().slice(0,maxLength) + (text.length>maxLength?'...':'');
  }

  async function fetchHTML(url){
    const r = await fetch(url,{credentials:'same-origin', cache:'no-cache'});
    return await r.text();
  }

  function findLastThreads(doc){
    const rows = Array.from(doc.querySelectorAll('tr'));
    const threads = [];
    for(const td of rows){
      const linkEl = td.querySelector('a.threadLink');
      if(!linkEl) continue;
      const href = toAbs(linkEl.getAttribute('href')||linkEl.href);
      threads.push({href});
      if(threads.length >= MAX_COMMENTS) break;
    }
    return threads;
  }

  async function enrichThread(t){
    try{
      const html = await fetchHTML(t.href);
      const doc = new DOMParser().parseFromString(html,'text/html');
      const postsInfo = Array.from(doc.querySelectorAll('td.postTdInfo'));
      const postsBody = Array.from(doc.querySelectorAll('td.post_body'));
      if(!postsInfo.length || !postsBody.length) return {...t,text:'',author:'Аноним',avatar:'/forumimages/default-avatar.png'};
      const lastIndex = postsInfo.length - 1;
      const info = postsInfo[lastIndex];
      const body = postsBody[lastIndex];
      const authorEl = info.querySelector('.postUser, .author, .post-author-name');
      const author = authorEl?.textContent?.trim() || 'Аноним';
      const avatarEl = info.querySelector('img.ipsUserPhoto, img.avatar');
      const avatar = avatarEl ? toAbs(avatarEl.src) : '/forumimages/default-avatar.png';
      const text = cleanText(body, MAX_TEXT);
      return {text, author, avatar, href: t.href};
    }catch(e){
      console.error(e);
      return {text:'',author:'Аноним',avatar:'/forumimages/default-avatar.png', href: t.href};
    }
  }

  function renderComments(comments){
    container.innerHTML = '';
    comments.forEach(c=>{
      const div = document.createElement('div');
      div.className='comment-bubble';
      div.innerHTML=`
        <img src="${c.avatar}" alt="avatar">
        <div class="comment-content">
          ${c.author ? `<strong>${escapeHtml(c.author)}</strong>` : ''}
          <span>${escapeHtml(c.text)}</span>
        </div>
      `;
      div.addEventListener('click', ()=> loadThreadInContainer(c.href));
      container.appendChild(div);
    });
  }

  async function loadThreadInContainer(threadUrl){
    try{
      const html = await fetchHTML(threadUrl);
      const doc = new DOMParser().parseFromString(html,'text/html');
      const postsBody = Array.from(doc.querySelectorAll('td.post_body'));
      container.innerHTML = '';
      postsBody.forEach(body => {
        const text = cleanText(body, 10000);
        const div = document.createElement('div');
        div.className='comment-bubble';
        div.innerHTML = `<div class="comment-content">${escapeHtml(text)}</div>`;
        container.appendChild(div);
      });
    }catch(e){ console.error(e);}
  }

  async function loadData(){
    const now = Date.now();
    const cache = JSON.parse(localStorage.getItem('forumCommentsCache') || '{}');
    if(cache.timestamp && (now - cache.timestamp < CACHE_LIFETIME) && cache.comments){
      renderComments(cache.comments);
      return;
    }

    try{
      const html = await fetchHTML(BASE_FORUM_URL);
      const doc = new DOMParser().parseFromString(html,'text/html');
      const threads = findLastThreads(doc);
      const comments = await Promise.all(threads.map(enrichThread));

      localStorage.setItem('forumCommentsCache', JSON.stringify({timestamp: now, comments}));
      renderComments(comments);
    }catch(e){ console.error(e);}
  }

  await loadData();

  // Фоновая проверка каждые 30 минут, не влияя на интерфейс
  setInterval(loadData, CACHE_LIFETIME);
})();
</script>


Я не уверен, что мне это нужно на сайте, но так просто получилось. 2

Порой сам не знаешь, куда идешь, а просто делаешь.

Мне нужен был небольшой парсер, который бы выводил несколько комментариев на главной странице. alik

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
  • Страница 1 из 1
  • 1
Поиск: