匹配范围
*://tieba.baidu.com/*tieba-forum-posts用途 读取指定百度贴吧的公开帖子列表,返回帖子标题、作者、回复数、最后回复时间和帖子链接。 执行前提 请在 tieba.baidu.com 域名下的页面执行。脚本会根据参数请求 https://tieba.baidu.com/f?kw=<forum>&ie=utf-8&pn=<offset>,也可以在当前页面正好是目标贴吧目标页时直接读取当前 DOM。 如…
*://tieba.baidu.com/*dom.read1return await (async () => {2 const input = typeof params === "object" && params ? params : {};3 4 function fail(message) {5 throw new Error(message);6 }7 8 function cleanText(value) {9 return String(value || "").replace(/\s+/g, " ").trim();10 }11 12 function readInt(value, fallback, min, max, name) {13 const raw = value === undefined || value === null || value === "" ? fallback : value;14 const number = Number(raw);15 if (!Number.isInteger(number)) {16 fail(name + " 必须是整数。");17 }18 if (number < min || number > max) {19 fail(name + " 必须在 " + min + " 到 " + max + " 之间。");20 }21 return number;22 }23 24 function absoluteUrl(href) {25 try {26 return new URL(href, "https://tieba.baidu.com").href;27 } catch (_error) {28 return "";29 }30 }31 32 function canonicalPostUrl(id) {33 return "https://tieba.baidu.com/p/" + id;34 }35 36 function firstText(root, selectors) {37 for (const selector of selectors) {38 const node = root.querySelector(selector);39 const text = cleanText(node && node.textContent);40 if (text) return text;41 }42 return "";43 }44 45 function parseDataField(node) {46 const raw = node && node.getAttribute && node.getAttribute("data-field");47 if (!raw) return null;48 try {49 return JSON.parse(raw);50 } catch (_error) {51 return null;52 }53 }54 55 function readNumberText(text) {56 const match = cleanText(text).replace(/,/g, "").match(/\d+/);57 return match ? Number(match[0]) : null;58 }59 60 function detectBlocked(doc) {61 const title = cleanText(doc.title);62 const bodyText = cleanText(doc.body && (doc.body.innerText || doc.body.textContent));63 return /百度安全验证|安全验证|请完成验证|验证码|登录后|请登录|访问受限|风险/.test(title + " " + bodyText);64 }65 66 function hasPostLinks(doc) {67 return Boolean(doc.querySelector('a[href*="/p/"]'));68 }69 70 function readForumFromPage() {71 const urlForum = new URLSearchParams(location.search).get("kw");72 if (urlForum) return cleanText(urlForum);73 const title = cleanText(document.title);74 const match = title.match(/^(.+?)吧(?:_|-|—|_百度贴吧| 百度贴吧|$)/) || title.match(/^(.+?)(?:_|-|—)?百度贴吧/);75 return match ? cleanText(match[1].replace(/吧$/, "")) : "";76 }77 78 function getForum() {79 const value = input.forum === undefined || input.forum === null ? "" : cleanText(input.forum);80 if (value) return value;81 const inferred = readForumFromPage();82 if (!inferred) {83 fail("缺少 forum 参数,且无法从当前 URL 或页面标题提取贴吧名称。");84 }85 return inferred;86 }87 88 function currentPageMatches(forum, page) {89 if (location.hostname !== "tieba.baidu.com") return false;90 if (location.pathname !== "/f") return false;91 const search = new URLSearchParams(location.search);92 const currentForum = cleanText(search.get("kw"));93 const currentPn = Number(search.get("pn") || "0");94 return currentForum === forum && currentPn === (page - 1) * 50;95 }96 97 async function fetchDocument(url, canUseCurrent) {98 if (canUseCurrent && !detectBlocked(document) && hasPostLinks(document)) {99 return document;100 }101 102 let fetchError = null;103 try {104 const response = await fetch(url, {105 credentials: "include",106 headers: {107 Accept: "text/html,application/xhtml+xml",108 },109 });110 if (!response.ok) {111 fail("请求贴吧论坛页失败,HTTP 状态码:" + response.status + "。");112 }113 const html = await response.text();114 const parsed = new DOMParser().parseFromString(html, "text/html");115 if (detectBlocked(parsed)) {116 fail("页面出现安全验证或登录墙,无法读取公开帖子列表。");117 }118 return parsed;119 } catch (error) {120 fetchError = error;121 }122 123 if (canUseCurrent) {124 if (detectBlocked(document)) {125 fail("当前页面出现安全验证或登录墙,无法读取公开帖子列表。");126 }127 return document;128 }129 130 if (fetchError && fetchError.message) {131 fail(fetchError.message);132 }133 fail("请求贴吧论坛页失败,请稍后重试。");134 }135 136 function extractLastReplyTime(row) {137 const text = firstText(row, [138 ".threadlist_reply_date",139 ".threadlist_author .pull-right",140 ".last_reply_time",141 ".last-time",142 ".reply-time",143 ".time",144 ]);145 if (text) return text;146 const all = cleanText(row.textContent);147 const matches = all.match(/\d{4}-\d{1,2}-\d{1,2}|\d{1,2}-\d{1,2}|\d{1,2}:\d{2}/g);148 return matches && matches.length ? matches[matches.length - 1] : "";149 }150 151 function extractReplies(row, data) {152 const dataValue =153 data && (data.reply_num || data.replyNum || data.reply_count || data.replyCount || data.comment_num);154 if (dataValue !== undefined && dataValue !== null && dataValue !== "") {155 const number = Number(dataValue);156 if (Number.isFinite(number)) return number;157 }158 159 const text = firstText(row, [160 ".threadlist_rep_num",161 ".rep_num",162 ".reply_num",163 ".reply-count",164 ".list-item-reply-num",165 ]);166 const parsed = readNumberText(text);167 return parsed === null ? 0 : parsed;168 }169 170 function extractAuthor(row, data) {171 const dataAuthor =172 data &&173 (data.author_name ||174 data.authorName ||175 data.user_name ||176 data.userName ||177 (data.author && (data.author.user_name || data.author.userName || data.author.name)));178 if (dataAuthor) return cleanText(dataAuthor);179 return firstText(row, [180 ".frs-author-name",181 ".threadlist_author a",182 ".tb_icon_author",183 ".p_author_name",184 ".user_name",185 ".user-name",186 ".j_user_card",187 ]);188 }189 190 function findRow(anchor) {191 return (192 anchor.closest("li.j_thread_list, li.threadlist_li, div.threadlist_li, li[data-field], .threadlist_text, .list-item") ||193 anchor.closest("li") ||194 anchor.parentElement ||195 anchor196 );197 }198 199 function parsePosts(doc, forum, limit) {200 const seen = new Set();201 const posts = [];202 const anchors = Array.from(doc.querySelectorAll('a[href*="/p/"]'));203 204 for (const anchor of anchors) {205 const href = anchor.getAttribute("href") || "";206 const url = absoluteUrl(href);207 const idMatch = url.match(/\/p\/(\d+)/);208 if (!idMatch) continue;209 210 const id = idMatch[1];211 if (seen.has(id)) continue;212 213 const title =214 firstText(anchor, [215 ".thread-title",216 ".title-wrap",217 ".title-content-wrap",218 ".threadlist_title",219 ".j_th_tit",220 ]) || cleanText(anchor.getAttribute("title") || anchor.textContent);221 if (!title || /^\d+$/.test(title) || /^(回复|评论|只看楼主)$/.test(title)) continue;222 223 const row = findRow(anchor);224 const data = parseDataField(row) || {};225 seen.add(id);226 posts.push({227 rank: posts.length + 1,228 id,229 title,230 forum,231 author: extractAuthor(row, data),232 replies: extractReplies(row, data),233 lastReplyTime: extractLastReplyTime(row),234 url: canonicalPostUrl(id),235 });236 237 if (posts.length >= limit) break;238 }239 240 return posts;241 }242 243 if (location.hostname !== "tieba.baidu.com") {244 fail("请在 tieba.baidu.com 页面执行该脚本。");245 }246 247 const forum = getForum();248 const page = readInt(input.page, 1, 1, 20, "page");249 const limit = readInt(input.limit, 20, 1, 50, "limit");250 const targetUrl =251 "https://tieba.baidu.com/f?kw=" + encodeURIComponent(forum) + "&ie=utf-8&pn=" + String((page - 1) * 50);252 const doc = await fetchDocument(targetUrl, currentPageMatches(forum, page));253 254 if (detectBlocked(doc)) {255 fail("页面出现安全验证或登录墙,无法读取公开帖子列表。");256 }257 258 const posts = parsePosts(doc, forum, limit);259 if (!posts.length) {260 fail("未读取到公开帖子,请确认贴吧名称、页码,或检查页面是否出现安全验证。");261 }262 263 return posts;264})();