- core/ — общий слой: fetch, throttle, output, state, runner - forums/base.py — абстрактный BaseAdapter - forums/xenforo/ — адаптер для XenForo (vwts.ru) - forums/phpbb/ — адаптер для phpBB (нива-лада.рф) - __main__.py — точка входа CLI - history/001-initial-structure.md — журнал изменений Также на ВМ (не в этом коммите): - lada_scraper/parse.py — пропуск sticky-тем при многопоточном парсинге
164 lines
5.2 KiB
Python
164 lines
5.2 KiB
Python
"""Парсинг HTML форума XenForo (vwts.ru).
|
||
|
||
Структуры:
|
||
|
||
Список тем в разделе:
|
||
<div class="structItem"> ← контейнер темы
|
||
<div class="structItem-title">
|
||
<a href="/forum/topic/12345/">Заголовок темы</a>
|
||
</div>
|
||
</div>
|
||
|
||
Пост в теме:
|
||
<article class="message"> ← контейнер поста
|
||
<h4 class="message-name">Автор</h4>
|
||
<time class="u-dt" datetime="2024-01-15T10:00:00Z">...</time>
|
||
<ul class="message-attribution-opposite">
|
||
<li><a>#1</a></li> ← номер поста
|
||
</ul>
|
||
<div class="message-content"> ← тело поста
|
||
...
|
||
</div>
|
||
</article>
|
||
|
||
Теги темы:
|
||
<li class="tagItem">
|
||
<a>Название тега</a>
|
||
</li>
|
||
"""
|
||
|
||
import re
|
||
import logging
|
||
from bs4 import BeautifulSoup
|
||
|
||
log = logging.getLogger(__name__)
|
||
|
||
|
||
def parse_topics(soup: BeautifulSoup) -> list[dict]:
|
||
"""Распарсить список тем со страницы раздела XenForo.
|
||
|
||
Ищем:
|
||
div.structItem → a с href, содержащим /topic/ID/
|
||
|
||
Args:
|
||
soup: BeautifulSoup страницы раздела.
|
||
|
||
Returns:
|
||
list[dict]: [{id, title, url}, ...].
|
||
"""
|
||
items = []
|
||
|
||
for container in soup.select("div.structItem"):
|
||
# Ссылка на тему
|
||
link = container.select_one("div.structItem-title a")
|
||
if link is None:
|
||
continue
|
||
|
||
href = link.get("href", "")
|
||
|
||
# Из href вытаскиваем ID темы: /topic/12345/
|
||
match = re.search(r"/topic/(\d+)/", href)
|
||
if match is None:
|
||
continue
|
||
|
||
topic_id = int(match.group(1))
|
||
title = link.text.strip()
|
||
|
||
# Формируем полный URL (если относительный)
|
||
if href.startswith("/"):
|
||
full_url = f"https://vwts.ru{href}"
|
||
elif href.startswith("http"):
|
||
full_url = href
|
||
else:
|
||
full_url = f"https://vwts.ru/forum/topic/{topic_id}/"
|
||
|
||
items.append({
|
||
"id": topic_id,
|
||
"title": title,
|
||
"url": full_url,
|
||
})
|
||
|
||
return items
|
||
|
||
|
||
def parse_posts(soup: BeautifulSoup) -> list[dict]:
|
||
"""Распарсить посты со страницы темы XenForo.
|
||
|
||
Ищем:
|
||
article.message → автор, дата, номер, текст
|
||
|
||
Args:
|
||
soup: BeautifulSoup страницы темы.
|
||
|
||
Returns:
|
||
list[dict]: [{author, date, num, text}, ...].
|
||
"""
|
||
posts = []
|
||
|
||
for article in soup.select("article.message"):
|
||
# ── Автор ───────────────────────────────────────────────────────
|
||
author_elem = article.select_one("h4.message-name")
|
||
author = author_elem.get_text(strip=True) if author_elem else "?"
|
||
|
||
# ── Дата ────────────────────────────────────────────────────────
|
||
time_elem = article.select_one("time.u-dt")
|
||
date = time_elem.get("datetime", "") if time_elem else ""
|
||
|
||
# ── Номер поста (#1, #2...) ─────────────────────────────────────
|
||
num_elem = article.select_one("ul.message-attribution-opposite a")
|
||
num = num_elem.text.strip() if num_elem else ""
|
||
|
||
# ── Текст поста ─────────────────────────────────────────────────
|
||
content = article.select_one("div.message-content")
|
||
text = ""
|
||
if content is not None:
|
||
# Удаляем скрытые элементы (спойлеры, скрытые блоки)
|
||
for hidden in content.select(
|
||
"div[style*='none'], span[style*='none'], details",
|
||
):
|
||
hidden.decompose()
|
||
# Извлекаем текст
|
||
text = content.get_text("\n", strip=True)
|
||
|
||
posts.append({
|
||
"author": author,
|
||
"date": date,
|
||
"num": num,
|
||
"text": text,
|
||
})
|
||
|
||
return posts
|
||
|
||
|
||
def parse_tags(soup: BeautifulSoup) -> list[str]:
|
||
"""Распарсить теги темы XenForo.
|
||
|
||
Ищем:
|
||
li.tagItem → a → текст тега
|
||
|
||
Args:
|
||
soup: BeautifulSoup страницы темы.
|
||
|
||
Returns:
|
||
list[str]: список тегов (может быть пустым).
|
||
"""
|
||
return [
|
||
tag.text.strip()
|
||
for tag in soup.select("li.tagItem a")
|
||
]
|
||
|
||
|
||
def parse_section_name(soup: BeautifulSoup) -> str | None:
|
||
"""Извлечь название раздела из h1.
|
||
|
||
Args:
|
||
soup: BeautifulSoup страницы раздела.
|
||
|
||
Returns:
|
||
str: название раздела или None.
|
||
"""
|
||
h1 = soup.select_one("h1")
|
||
if h1 is not None:
|
||
return h1.text.strip()
|
||
return None
|