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

1
Админ
Постов: 288
2
VIP
Постов: 75
3
Элита
Постов: 62
4
Проверенные
Постов: 46
5
Проверенные
Постов: 45
6
Проверенные
Постов: 40
7
VIP
Постов: 36
8
VIP
Постов: 35

  • Страница 1 из 1
  • 1
Парсер последних тем форума стильный вывод постов uCoz V 3.2
Дата: Четверг, 19.02.2026, 04:29 | Сообщение # 1 | | Написал: Известный
Автор темы
Мурчанн не в сети
        Сообщений:288
         Регистрация:20.10.2016
За уважение окружающих За уважение окружающих За хорошее поведение За преданность форуму За любовь народа

Продолжение старой темы

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

Одной из главных трудностей был таймер обновления: обновления происходили крайне заметно и неудобно. При перезагрузке парсер «схлопывался» и временно переставал показывать контент, после чего лишь через некоторое время снова начинал выводить данные. Этот цикл повторялся каждый час, что делает использование парсера крайне неэффективным и неудобным для пользователей.

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

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

Я разработал новую версию парсера с обновлённой логикой работы. Теперь парсер загружает посты и новости всего один раз в память — в кэш локального хранилища и далее обновляет старые данные только при появлении новых сообщений на форуме.

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

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

В качестве бонуса будут добавлены новые стили, которые разбивают парсер на две колонки, делая интерфейс более удобным и наглядным.

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

Новый вариант парсера



Исходной код

Код
<section class="sect flex-grow-1">
<div class="sect__header d-flex">
<h2 class="sect__title flex-grow-1">
Forum Updates
<img src="https://jordan.moy.su/forumimages/3droom.png" alt="3D Room Icon" style="height:24px; margin-left:-2px; vertical-align:middle;">
</h2>
</div>

<div class="dark-border">
<div id="f28_cards"></div>
</div>
</section>

<style>
/* Контейнер всех карточек — теперь используем grid для более гибкого размещения */
#f28_cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  gap: 28px 24px;
  width: 100%;
  padding: 0 16px;
  margin: 0 auto;
  max-width: 1400px;
  box-sizing: border-box;
}

/* Основная карточка — glassmorphism + мягкая глубина */
.f28_card {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: #fff;
  border-radius: 20px;
  overflow: hidden;
  background: rgba(20, 20, 35, 0.38);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  box-shadow:
    0 12px 40px rgba(0,0,0,0.35),
    inset 0 1px 0 rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.06);
  transition:
    transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
    box-shadow 0.4s ease,
    opacity 0.4s ease;
  opacity: 0.98;
}

.f28_card:hover,
.f28_card:focus-visible {
  transform: translateY(-10px) scale(1.02);
  box-shadow:
    0 24px 60px rgba(0,0,0,0.45),
    inset 0 1px 0 rgba(255,255,255,0.12);
  opacity: 1;
}

/* Обёртка картинки */
.f28_img_wrapper {
  position: relative;
  width: 100%;
  height: 260px;          /* чуть выше для современного вида */
  overflow: hidden;
}

.f28_img_wrapper img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform;
}

.f28_card:hover .f28_img_wrapper img {
  transform: scale(1.08) translateZ(0);
}

/* Автор — теперь более стильный бейдж */
.f28_author {
  position: absolute;
  top: 16px;
  left: 16px;
  z-index: 3;
  font-size: 0.92rem;
  font-weight: 700;
  color: #fff;
  background: rgba(0,0,0,0.4);
  backdrop-filter: blur(8px);
  padding: 5px 12px;
  border-radius: 50px;
  border: 1px solid rgba(255,255,220,0.4);
  text-shadow: 0 1px 4px rgba(0,0,0,0.6);
  letter-spacing: 0.4px;
}

/* Заголовок поверх изображения */
.f28_title {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 24px 20px 20px;
  font-size: clamp(1.6rem, 4.2vw, 2.4rem);
  font-weight: 800;
  line-height: 1.18;
  background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, transparent 100%);
  text-shadow: 0 3px 12px rgba(0,0,0,0.8);
  letter-spacing: -0.5px;
}

/* Контент под картинкой */
.f28_desc_wrapper {
  padding: 20px;
  background: linear-gradient(to bottom, rgba(15,15,25,0.7) 0%, rgba(10,10,20,0.85) 100%);
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.f28_desc {
  font-size: 1.05rem;
  line-height: 1.62;
  color: #e0e0f0;
  opacity: 0.92;
  display: -webkit-box;
  -webkit-line-clamp: 5;           /* было 6 — уменьшил для читаемости */
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin: 0;
}

/* Нижняя панель — более современная */
.f28_footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: auto;
  padding-top: 16px;
  border-top: 1px solid rgba(255,255,255,0.07);
  font-size: 0.9rem;
  color: #b0b0d0;
}

.f28_date {
  font-weight: 500;
}

.f28_button {
  background: linear-gradient(135deg, #8a5acf 0%, #c06ab8 100%);
  color: white;
  padding: 9px 20px;
  border-radius: 50px;
  font-weight: 700;
  font-size: 0.94rem;
  text-decoration: none;
  transition: all 0.35s ease;
  box-shadow: 0 4px 20px rgba(133,91,151,0.3);
  white-space: nowrap;
}

.f28_button:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 32px rgba(133,91,151,0.45);
  background: linear-gradient(135deg, #9b6ee0 0%, #d47cc9 100%);
}

/* Адаптивность — теперь через grid, поэтому медиа-запросы минимальны */
@media (max-width: 900px) {
  #f28_cards {
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 24px 20px;
  }
}

@media (max-width: 600px) {
  .f28_img_wrapper {
    height: 220px;
  }
  .f28_title {
    padding: 20px 16px 16px;
    font-size: clamp(1.4rem, 5vw, 2rem);
  }
  .f28_desc {
    font-size: 1rem;
    -webkit-line-clamp: 4;
  }
}

/* Дополнительно: плавное появление карточек (если используешь JS для подгрузки) */
@keyframes cardFadeIn {
  from { opacity: 0; transform: translateY(30px); }
  to   { opacity: 1; transform: translateY(0);   }
}

.f28_card {
  animation: cardFadeIn 0.8s ease-out forwards;
  animation-delay: calc(var(--order, 0) * 120ms);
}
</style>

<script>
(async function forum28Parser(){

/* ===== SINGLE RUN LOCK ===== */
if(window.__F28_LOCK__) return;
window.__F28_LOCK__=true;

/* ================= CONFIG ================= */

const container=document.querySelector('#f28_cards');
if(!container) return;

const FORUM_URL='/forum/0-0-1-34';
const CACHE_KEY='f28_cards_cache_nf_logic_v2';
const FIRST_TOPIC_KEY='f28_first_topic_nf_logic_v2';
const MAX_CARDS=12;

/* ================= UTILS ================= */

const toAbsolute=url=>!url?''
:url.startsWith('http')?url
:url.startsWith('//')?location.protocol+url
:location.origin+(url.startsWith('/')?url:'/'+url);

const escapeHtml=text=>String(text||'').replace(/[&<>"']/g,m=>(
{'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]
));

function safeGet(k){try{return JSON.parse(localStorage.getItem(k)||'null')}catch{return null}}
function safeSet(k,v){try{localStorage.setItem(k,JSON.stringify(v))}catch{}}
function clearCache(){localStorage.removeItem(CACHE_KEY)}

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

/* ================= CREATE CARD (ALL CLASSES KEPT) ================= */

function createCard(data){
const card=document.createElement('a');
card.className='f28_card';
card.href=data.href;
card.target='_self';
card.dataset.href=data.href;

card.innerHTML=`
<div class="f28_img_wrapper">
   <img src="${data.img||'/forumimages/Newsletter-3.png'}">

   <div class="f28_author">
     ${escapeHtml(data.author||'Автор')}
   </div>

   <div class="f28_title">
     ${escapeHtml(data.title||'Без названия')}
   </div>
</div>

<div class="f28_desc_wrapper">
   <div class="f28_desc">
     ${escapeHtml(data.text||'')}
   </div>

   <div class="f28_footer" style="display:flex;justify-content:space-between;align-items:center;">

     <span class="f28_date" style="
       background: linear-gradient(135deg, #0fd700, #7e1280, #f714d9);
       color:#FFF;
       padding:4px 8px;
       border-radius:4px;
       font-weight:700;
       font-size:10px;
       text-shadow:1px 1px 2px rgba(0,0,0,0.2);
       box-shadow:0 2px 6px rgba(0,0,0,0.15);
       display:inline-block;
       transition:transform .2s, box-shadow .2s;
     ">
       ${escapeHtml(data.date||'Дата')}
     </span>

     <a href="${data.href}" class="f28_button">Read more</a>

   </div>
</div>`;
return card;
}

/* ================= RENDER ================= */

function renderCards(cards){
if(!cards?.length) return;
container.innerHTML='';
cards.forEach(c=>container.appendChild(createCard(c)));
}

/* ================= PARSE THREAD LIST ================= */

function parseThreadsList(doc){
const arr=[];
const items=doc.querySelectorAll('.threadNametd .threadLink');

for(let i=0;i<items.length&&arr.length<MAX_CARDS;i++){
  const a=items[i];
  const href=toAbsolute(a.getAttribute('href')||'');
  const title=a.textContent.trim();
  arr.push({href,title});
}
return arr;
}

/* ================= PARSE FIRST POST ================= */

function parseFirstPost(html){
const doc=new DOMParser().parseFromString(html,'text/html');

let text='',img='',author='Автор',date='Дата';

const post=doc.querySelector('.post_content,.post_body,.post,.ipsType_richText');
if(post){
  text=post.textContent.trim().slice(0,160);
  const im=post.querySelector('img');
  if(im) img=toAbsolute(im.getAttribute('src')||'');
}

const authorEl=doc.querySelector('.postUser,.author');
if(authorEl) author=authorEl.textContent.trim();

const td=[...doc.querySelectorAll('td.postTdTop')]
.find(x=>x.textContent.includes('Дата:'));

if(td){
  const m=td.innerHTML.match(/Дата:\s*([^|<]+)/);
  if(m) date=m[1].trim();
}

return {text,img,author,date};
}

/* ================= SHOW CACHE INSTANT ================= */

const cached=safeGet(CACHE_KEY);
if(cached?.cards) renderCards(cached.cards);

/* ================= NF LOGIC (CHECK ONLY ON PAGE LOAD) ================= */

try{

const html=await fetchHTML(FORUM_URL);
const doc=new DOMParser().parseFromString(html,'text/html');

const threads=parseThreadsList(doc);
if(!threads.length) return;

const firstTitle=threads[0].title.trim();
const savedTitle=localStorage.getItem(FIRST_TOPIC_KEY);

/* === SAME TOPIC → SLEEP (NO LOAD, NO FETCH POSTS) === */
if(savedTitle && savedTitle===firstTitle) return;

/* === NEW TOPIC → UPDATE CACHE === */

localStorage.setItem(FIRST_TOPIC_KEY,firstTitle);
clearCache();

const cards=(await Promise.all(
  threads.map(t=>
   fetchHTML(t.href)
   .then(h=>({...t,...parseFirstPost(h)}))
   .catch(()=>t)
  )
)).filter(Boolean);

safeSet(CACHE_KEY,{cards});
renderCards(cards);

}catch(e){
console.log('forum parser error',e);
}

})();
</script>


Видео

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
Дата: Четверг, 19.02.2026, 04:38 | Сообщение # 2 | | Написал: Известный
Автор темы
Мурчанн не в сети
        Сообщений:288
         Регистрация:20.10.2016
За уважение окружающих За уважение окружающих За хорошее поведение За преданность форуму За любовь народа

Обновлённая версия старого парсера сохранила прежние стили оформления, взятые за основу, но вся внутренняя часть полностью переработана. Теперь он работает по тому же принципу, что и новая версия, обеспечивая стабильное и плавное обновление контента без прежних сбоев и перегрузок.



Исходник

Код
<section class="sect flex-grow-1">
<div class="sect__header d-flex">
<h2 class="sect__title flex-grow-1">
Forum Updates
<img src="https://jordan.moy.su/forumimages/3droom.png" alt="3D Room Icon" style="height:24px; margin-left:-2px; vertical-align:middle;">
</h2>
</div>

<div class="dark-border">
<div id="f28_cards"></div>
</div>
</section>

<style>
/* Контейнер карточки */
#f28_cards {
display: block;
width: 100%;
margin-bottom: -50px; /* ← отступ снизу между блоком */
}

/* Одна большая карточка */
.f28_card {
display: flex;
flex-direction: column;
width: 100%;
text-decoration: none;
color: #fff;
transition: transform 0.3s ease, opacity 0.3s ease;
margin-bottom: 28px; /* ← отступы между карточками */
border-radius: 12px; /* скругление */
overflow: hidden;
box-shadow: 0 8px 25px rgba(0,0,0,0.3); /* тень */
background: rgba(15,15,20,0.8); /* полупрозрачный фон для объёма */
}

/* Обёртка картинки с заголовком */
.f28_img_wrapper {
position: relative;
width: 100%;
height: 240px;
overflow: hidden;
}

.f28_img_wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.4s ease;
}

.f28_card:hover .f28_img_wrapper img {
transform: scale(1.05);
}

/* Заголовок поверх картинки */
.f28_title {
position: absolute;
bottom: 12px;
left: 12px;
right: 12px;
font-size: 2.2rem;
font-weight: 700;
color: #fff;
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.7);
line-height: 1.25;
}

/* Оболочка описания */
.f28_desc_wrapper {
background: rgba(0,0,0,0.45);
padding: 12px 14px;
overflow: hidden;
border-top: 1px solid rgba(255,255,255,0.05);
display: flex;
flex-direction: column;
gap: 6px;
}

/* Описание текста */
.f28_desc {
font-size: 1rem;
color: #ccc;
line-height: 1.6em;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 6;
-webkit-box-orient: vertical;
}

/* Нижняя строка: автор и дата + кнопка */
.f28_meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85rem;
color: #aaa;
}

.f28_button {
background: #855b97;
color: #fff;
padding: 6px 12px;
border-radius: 6px;
text-decoration: none;
font-weight: 600;
transition: background 0.3s ease;
}

.f28_button:hover {
background: #a170b5;
}

/* Нижняя строка: дата слева, кнопка справа */
.f28_footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}

.f28_date {
font-size: 0.85rem;
color: #aaa;
}

.f28_button {
background: #855b97;
color: #fff;
padding: 6px 12px;
border-radius: 6px;
text-decoration: none;
font-weight: 600;
transition: background 0.3s ease;
white-space: nowrap;
}

.f28_button:hover {
background: #a170b5;
}

/* Адаптивность */
@media (max-width: 1024px) { .f28_card { flex: 0 0 calc(33.333% - 10px); } }
@media (max-width: 768px) { .f28_card { flex: 0 0 calc(50% - 10px); } }
@media (max-width: 500px) { .f28_card { flex: 0 0 100%; } }

/* Автор сверху с желтой обводкой */
.f28_author {
position: absolute;
top: 10px;
left: 12px;
color: #fff;
font-size: 1rem;
font-weight: 600;
text-shadow: 1px 1px 4px rgba(0,0,0,0.7);
padding: 2px 6px;
border: 2px solid yellow;
border-radius: 4px;
z-index: 2;
background: rgba(0,0,0,0.3);
}

/* Дата снизу слева */
.f28_footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}

.f28_date {
font-size: 0.85rem;
color: #ccc;
text-align: left;
}

</style>

<script>
(async function forum28Parser(){

/* ===== SINGLE RUN LOCK ===== */
if(window.__F28_LOCK__) return;
window.__F28_LOCK__=true;

/* ================= CONFIG ================= */

const container=document.querySelector('#f28_cards');
if(!container) return;

const FORUM_URL='/forum/0-0-1-34';
const CACHE_KEY='f28_cards_cache_nf_logic_v2';
const FIRST_TOPIC_KEY='f28_first_topic_nf_logic_v2';
const MAX_CARDS=12;

/* ================= UTILS ================= */

const toAbsolute=url=>!url?''
:url.startsWith('http')?url
:url.startsWith('//')?location.protocol+url
:location.origin+(url.startsWith('/')?url:'/'+url);

const escapeHtml=text=>String(text||'').replace(/[&<>"']/g,m=>(
{'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]
));

function safeGet(k){try{return JSON.parse(localStorage.getItem(k)||'null')}catch{return null}}
function safeSet(k,v){try{localStorage.setItem(k,JSON.stringify(v))}catch{}}
function clearCache(){localStorage.removeItem(CACHE_KEY)}

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

/* ================= CREATE CARD (ALL CLASSES KEPT) ================= */

function createCard(data){
const card=document.createElement('a');
card.className='f28_card';
card.href=data.href;
card.target='_self';
card.dataset.href=data.href;

card.innerHTML=`
<div class="f28_img_wrapper">
   <img src="${data.img||'/forumimages/Newsletter-3.png'}">

   <div class="f28_author">
     ${escapeHtml(data.author||'Автор')}
   </div>

   <div class="f28_title">
     ${escapeHtml(data.title||'Без названия')}
   </div>
</div>

<div class="f28_desc_wrapper">
   <div class="f28_desc">
     ${escapeHtml(data.text||'')}
   </div>

   <div class="f28_footer" style="display:flex;justify-content:space-between;align-items:center;">

     <span class="f28_date" style="
       background: linear-gradient(135deg, #0fd700, #7e1280, #f714d9);
       color:#FFF;
       padding:4px 8px;
       border-radius:4px;
       font-weight:700;
       font-size:10px;
       text-shadow:1px 1px 2px rgba(0,0,0,0.2);
       box-shadow:0 2px 6px rgba(0,0,0,0.15);
       display:inline-block;
       transition:transform .2s, box-shadow .2s;
     ">
       ${escapeHtml(data.date||'Дата')}
     </span>

     <a href="${data.href}" class="f28_button">Read more</a>

   </div>
</div>`;
return card;
}

/* ================= RENDER ================= */

function renderCards(cards){
if(!cards?.length) return;
container.innerHTML='';
cards.forEach(c=>container.appendChild(createCard(c)));
}

/* ================= PARSE THREAD LIST ================= */

function parseThreadsList(doc){
const arr=[];
const items=doc.querySelectorAll('.threadNametd .threadLink');

for(let i=0;i<items.length&&arr.length<MAX_CARDS;i++){
  const a=items[i];
  const href=toAbsolute(a.getAttribute('href')||'');
  const title=a.textContent.trim();
  arr.push({href,title});
}
return arr;
}

/* ================= PARSE FIRST POST ================= */

function parseFirstPost(html){
const doc=new DOMParser().parseFromString(html,'text/html');

let text='',img='',author='Автор',date='Дата';

const post=doc.querySelector('.post_content,.post_body,.post,.ipsType_richText');
if(post){
  text=post.textContent.trim().slice(0,160);
  const im=post.querySelector('img');
  if(im) img=toAbsolute(im.getAttribute('src')||'');
}

const authorEl=doc.querySelector('.postUser,.author');
if(authorEl) author=authorEl.textContent.trim();

const td=[...doc.querySelectorAll('td.postTdTop')]
.find(x=>x.textContent.includes('Дата:'));

if(td){
  const m=td.innerHTML.match(/Дата:\s*([^|<]+)/);
  if(m) date=m[1].trim();
}

return {text,img,author,date};
}

/* ================= SHOW CACHE INSTANT ================= */

const cached=safeGet(CACHE_KEY);
if(cached?.cards) renderCards(cached.cards);

/* ================= NF LOGIC (CHECK ONLY ON PAGE LOAD) ================= */

try{

const html=await fetchHTML(FORUM_URL);
const doc=new DOMParser().parseFromString(html,'text/html');

const threads=parseThreadsList(doc);
if(!threads.length) return;

const firstTitle=threads[0].title.trim();
const savedTitle=localStorage.getItem(FIRST_TOPIC_KEY);

/* === SAME TOPIC → SLEEP (NO LOAD, NO FETCH POSTS) === */
if(savedTitle && savedTitle===firstTitle) return;

/* === NEW TOPIC → UPDATE CACHE === */

localStorage.setItem(FIRST_TOPIC_KEY,firstTitle);
clearCache();

const cards=(await Promise.all(
  threads.map(t=>
   fetchHTML(t.href)
   .then(h=>({...t,...parseFirstPost(h)}))
   .catch(()=>t)
  )
)).filter(Boolean);

safeSet(CACHE_KEY,{cards});
renderCards(cards);

}catch(e){
console.log('forum parser error',e);
}

})();
</script>


Видео необязательно , парсер работает по тому же принципу, обеспечивая плавное обновление контента и стабильную работу без сбоев.

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

Мурчанн

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