Дата: Четверг, 19.02.2026, 04:29 | Сообщение # 1 |
|
Написал: Узнаваемый
Автор темы
Мурчанн
не в сети
Сообщений: 211
Продолжение старой темы Парсер последних тем форума стильный вывод постов 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 |
|
Написал: Узнаваемый
Автор темы
Мурчанн
не в сети
Сообщений: 211
Обновлённая версия старого парсера сохранила прежние стили оформления, взятые за основу, но вся внутренняя часть полностью переработана. Теперь он работает по тому же принципу, что и новая версия, обеспечивая стабильное и плавное обновление контента без прежних сбоев и перегрузок.Исходник Код
<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>
Видео необязательно , парсер работает по тому же принципу, обеспечивая плавное обновление контента и стабильную работу без сбоев. Скорее всего, это последняя модификация парсера дальше уже останется только менять стили и придумывать новый дизайн. Скрипт достиг своего пика совершенства, хотя кто знает, возможно, в будущем всё же будут внесены небольшие изменения.
Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.