灵猴市集Linghou Market

虎扑热门帖子

虎扑hupu-hot-threads

虎扑热门帖子提取 用途 读取虎扑社区页面中展示的热门或首页帖子,返回帖子标题、亮数、回复数、分区、热门标记和帖子地址。 执行前提 先打开 https://bbs.hupu.com/ 或虎扑社区中包含帖子列表的页面,再执行脚本。该页面公开可访问,通常无需登录;但虎扑可能按访问来源或频率显示阻断页、验证页或空页面,脚本不会尝试绕过。 参数 limit:可选,返…

热门帖子社区
版本1.0.0
扫描passed
更新2026-06-28
超时30000ms

匹配范围

*://bbs.hupu.com/*

排除范围

未声明

能力声明

dom.read

代码

源码已展开
1const rawParams = params && typeof params === "object" ? params : {};2const HUPU_HOST = "https://bbs.hupu.com";3 4function integerParam(name, fallback, min, max) {5  const raw = rawParams[name];6  const value = raw === undefined || raw === null || raw === "" ? fallback : Number(raw);7  if (!Number.isInteger(value) || value < min || value > max) {8    throw new Error(`${name} must be an integer between ${min} and ${max}`);9  }10  return value;11}12 13function sleep(ms) {14  return new Promise(resolve => setTimeout(resolve, ms));15}16 17function normalizeText(value) {18  return (value || "").replace(/\s+/g, " ").trim();19}20 21function parseCount(raw) {22  const text = normalizeText(raw).replace(/,/g, "");23  if (!text) return null;24  const match = text.match(/^(\d+(?:\.\d+)?)\s*(万)?/);25  if (!match) return null;26  const number = Number(match[1]);27  if (!Number.isFinite(number)) return null;28  return Math.round(match[2] === "万" ? number * 10000 : number);29}30 31function assertHupuPage() {32  const current = new URL(location.href);33  if (current.hostname !== "bbs.hupu.com") {34    throw new Error("Open https://bbs.hupu.com/ first, then run this script");35  }36}37 38function detectBlockedPage(doc) {39  const text = normalizeText(`${doc.title || ""} ${doc.body ? doc.body.innerText : ""}`).slice(0, 5000);40  if (/405|request has been blocked|访问被阻断|安全验证|验证码|人机验证|访问异常|请输入验证码/.test(text)) {41    return "The page is blocked by verification or risk control";42  }43  return "";44}45 46function threadIdFromHref(raw) {47  if (!raw) return "";48  try {49    const url = new URL(raw, HUPU_HOST);50    if (url.hostname !== "bbs.hupu.com") return "";51    const match = url.pathname.match(/^\/(\d{5,})\.html$/);52    return match ? match[1] : "";53  } catch {54    return "";55  }56}57 58function extractFromInfoRows(doc, limit) {59  const rows = [];60  const infos = doc.querySelectorAll(".t-info");61 62  for (const info of infos) {63    if (rows.length >= limit) break;64    const anchor = info.querySelector("a[href]");65    const tid = threadIdFromHref(anchor ? anchor.getAttribute("href") || anchor.href : "");66    if (!tid) continue;67    const titleEl = anchor.querySelector(".t-title") || anchor;68    const title = normalizeText(titleEl ? titleEl.textContent : "");69    if (!title) continue;70    const listItem = info.closest(".list-item") || info.parentElement;71    const labelEl = listItem ? listItem.querySelector(".t-label a, .t-label") : null;72 73    rows.push({74      rank: rows.length + 1,75      tid,76      title,77      lights: parseCount(info.querySelector(".t-lights")?.textContent || ""),78      replies: parseCount(info.querySelector(".t-replies")?.textContent || ""),79      forum: normalizeText(labelEl ? labelEl.textContent : ""),80      is_hot: (anchor.getAttribute("class") || "").split(/\s+/).includes("hot"),81      url: `${HUPU_HOST}/${tid}.html`,82    });83  }84 85  return rows;86}87 88function extractFromThreadLinks(doc, limit) {89  const rows = [];90  const seen = new Set();91  const links = doc.querySelectorAll("a[href$='.html'], a[href*='.html?']");92 93  for (const anchor of links) {94    if (rows.length >= limit) break;95    const tid = threadIdFromHref(anchor.getAttribute("href") || anchor.href);96    if (!tid || seen.has(tid)) continue;97    const title = normalizeText(anchor.querySelector(".t-title")?.textContent || anchor.textContent);98    if (!title || title.length < 4) continue;99    seen.add(tid);100    rows.push({101      rank: rows.length + 1,102      tid,103      title,104      lights: null,105      replies: null,106      forum: "",107      is_hot: (anchor.getAttribute("class") || "").split(/\s+/).includes("hot"),108      url: `${HUPU_HOST}/${tid}.html`,109    });110  }111 112  return rows;113}114 115function extractThreads(doc, limit) {116  const scoped = extractFromInfoRows(doc, limit);117  return scoped.length > 0 ? scoped : extractFromThreadLinks(doc, limit);118}119 120async function waitForThreads(doc, limit) {121  const deadline = Date.now() + 8000;122  let rows = extractThreads(doc, limit);123  while (Date.now() < deadline && rows.length === 0) {124    const blocked = detectBlockedPage(doc);125    if (blocked) throw new Error(`${blocked}; open the page normally in the browser and run again`);126    await sleep(250);127    rows = extractThreads(doc, limit);128  }129  return rows;130}131 132const limit = integerParam("limit", 20, 1, 100);133assertHupuPage();134 135const blocked = detectBlockedPage(document);136if (blocked) throw new Error(`${blocked}; open the page normally in the browser and run again`);137 138const items = await waitForThreads(document, limit);139if (items.length === 0) {140  throw new Error("No Hupu threads found; the page may be empty, blocked, or its structure changed");141}142 143return items.slice(0, limit);