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