- 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-тем при многопоточном парсинге
143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
"""Парсинг HTML форума phpBB (subsilver2, нива-лада.рф).
|
||
|
||
Структуры:
|
||
|
||
Список тем в разделе (subsilver2):
|
||
<a href="./viewtopic.php?f=22&t=123" class="topictitle">Заголовок</a>
|
||
|
||
Пост в теме (табличная вёрстка subsilver2):
|
||
<tr class="row1"> ← строка: автор + дата
|
||
<td><b class="postauthor">Автор</b></td>
|
||
<td width="100%">
|
||
<div style="float: right;">
|
||
<b>Добавлено:</b> 12 авг 2025, 20:00
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<tr class="row1"> ← строка: аватар + текст
|
||
<td class="profile">...</td>
|
||
<td><div class="postbody">текст поста</div></td>
|
||
</tr>
|
||
"""
|
||
|
||
import re
|
||
import logging
|
||
from bs4 import BeautifulSoup
|
||
|
||
log = logging.getLogger(__name__)
|
||
|
||
|
||
def parse_topics(soup: BeautifulSoup) -> list[dict]:
|
||
"""Распарсить список тем со страницы раздела phpBB.
|
||
|
||
Ищем все a.topictitle → из href вытаскиваем t=ID.
|
||
|
||
Args:
|
||
soup: BeautifulSoup страницы раздела.
|
||
|
||
Returns:
|
||
list[dict]: [{id, title, url}].
|
||
url может быть None (будет сформирован адаптером).
|
||
"""
|
||
items = []
|
||
|
||
for link in soup.select("a.topictitle"):
|
||
href = link.get("href", "")
|
||
|
||
# Из href вытаскиваем t=ID
|
||
match = re.search(r"[?&]t=(\d+)", href)
|
||
if match is None:
|
||
continue
|
||
|
||
topic_id = int(match.group(1))
|
||
title = link.text.strip()
|
||
|
||
items.append({
|
||
"id": topic_id,
|
||
"title": title,
|
||
"url": None, # адаптер сформирует сам
|
||
})
|
||
|
||
return items
|
||
|
||
|
||
def parse_posts(soup: BeautifulSoup) -> list[dict]:
|
||
"""Распарсить посты со страницы темы phpBB (subsilver2).
|
||
|
||
Каждый пост — это 3 <tr> в таблице. Ищем b.postauthor
|
||
как маркер начала поста, затем:
|
||
- Тот же <tr>: дата из "Добавлено:"
|
||
- Следующий <tr>: текст из div.postbody
|
||
|
||
Args:
|
||
soup: BeautifulSoup страницы темы.
|
||
|
||
Returns:
|
||
list[dict]: [{author, date, num, text}, ...].
|
||
"""
|
||
posts = []
|
||
post_num = 0
|
||
|
||
for author_tag in soup.select("b.postauthor"):
|
||
post_num += 1
|
||
author = author_tag.text.strip()
|
||
|
||
# ── Дата: ищем "Добавлено:" в том же <tr> ───────────────────────
|
||
date = ""
|
||
header_row = author_tag.find_parent("tr")
|
||
if header_row is not None:
|
||
date_cell = header_row.select_one("td[width='100%']")
|
||
if date_cell is not None:
|
||
date_text = date_cell.get_text(" ", strip=True)
|
||
date_match = re.search(r"Добавлено:\s*(.+)", date_text)
|
||
if date_match:
|
||
date = date_match.group(1).strip()
|
||
|
||
# ── Текст: ищем div.postbody в следующем <tr> ───────────────────
|
||
text = ""
|
||
if header_row is not None:
|
||
body_row = header_row.find_next_sibling("tr")
|
||
if body_row is not None:
|
||
body_div = body_row.select_one("div.postbody")
|
||
if body_div is not None:
|
||
text = body_div.get_text("\n", strip=True)
|
||
|
||
posts.append({
|
||
"author": author,
|
||
"date": date,
|
||
"num": f"#{post_num}",
|
||
"text": text,
|
||
})
|
||
|
||
return posts
|
||
|
||
|
||
def parse_section_name(soup: BeautifulSoup) -> str | None:
|
||
"""Извлечь название раздела из заголовка phpBB.
|
||
|
||
phpBB обычно показывает название в h2 или в breadcrumbs.
|
||
|
||
Args:
|
||
soup: BeautifulSoup страницы раздела.
|
||
|
||
Returns:
|
||
str | None: название раздела.
|
||
"""
|
||
# Пробуем h2 (основной заголовок страницы)
|
||
h2 = soup.select_one("h2")
|
||
if h2 is not None:
|
||
text = h2.text.strip()
|
||
if text:
|
||
return text
|
||
|
||
# Пробуем title страницы
|
||
title_tag = soup.select_one("title")
|
||
if title_tag is not None:
|
||
text = title_tag.text.strip()
|
||
# Чистим от названия сайта
|
||
text = re.sub(r"\s*–\s*.*$", "", text).strip()
|
||
if text:
|
||
return text
|
||
|
||
return None
|