匹配范围
*://tieba.baidu.com/*tieba-thread-read用途 读取百度贴吧公开帖子详情,返回帖子标题、主楼内容和回复内容。 执行前提 请在 tieba.baidu.com 域名下的页面执行。脚本会根据参数请求 https://tieba.baidu.com/p/<id>?pn=<page>,也可以在当前页面正好是目标帖子目标页时直接读取当前 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 readBoolean(value, fallback, name) {25 if (value === undefined || value === null || value === "") return fallback;26 if (value === true || value === "true") return true;27 if (value === false || value === "false") return false;28 fail(name + " 必须是布尔值。");29 }30 31 function firstText(root, selectors) {32 for (const selector of selectors) {33 const node = root.querySelector(selector);34 const text = cleanText(node && node.textContent);35 if (text) return text;36 }37 return "";38 }39 40 function parseDataField(node) {41 const raw = node && node.getAttribute && node.getAttribute("data-field");42 if (!raw) return null;43 try {44 return JSON.parse(raw);45 } catch (_error) {46 return null;47 }48 }49 50 function detectBlocked(doc) {51 const title = cleanText(doc.title);52 const bodyText = cleanText(doc.body && (doc.body.innerText || doc.body.textContent));53 return /百度安全验证|安全验证|请完成验证|验证码|登录后|请登录|访问受限|风险|帖子不存在|主题不存在/.test(54 title + " " + bodyText,55 );56 }57 58 function hasThreadContent(doc) {59 return Boolean(doc.querySelector(".pb-content-wrap, .pb-comment-item, .core_title_txt, .l_post, .d_post_content"));60 }61 62 function idFromCurrentUrl() {63 const match = location.pathname.match(/\/p\/(\d+)/);64 return match ? match[1] : "";65 }66 67 function getThreadId() {68 const raw = input.id === undefined || input.id === null ? "" : cleanText(input.id);69 const value = raw || idFromCurrentUrl();70 if (!value) {71 fail("缺少 id 参数,且无法从当前 URL 提取帖子 ID。");72 }73 if (!/^\d+$/.test(value)) {74 fail("id 必须是数字帖子 ID。");75 }76 return value;77 }78 79 function currentPageMatches(id, page) {80 if (location.hostname !== "tieba.baidu.com") return false;81 if (idFromCurrentUrl() !== id) return false;82 const currentPage = Number(new URLSearchParams(location.search).get("pn") || "1");83 return currentPage === page;84 }85 86 async function fetchDocument(url, canUseCurrent) {87 if (canUseCurrent && !detectBlocked(document) && hasThreadContent(document)) {88 return document;89 }90 91 let fetchError = null;92 try {93 const response = await fetch(url, {94 credentials: "include",95 headers: {96 Accept: "text/html,application/xhtml+xml",97 },98 });99 if (!response.ok) {100 fail("请求贴吧帖子页失败,HTTP 状态码:" + response.status + "。");101 }102 const html = await response.text();103 const parsed = new DOMParser().parseFromString(html, "text/html");104 if (detectBlocked(parsed)) {105 fail("页面出现安全验证、登录墙或帖子不存在,无法读取公开帖子。");106 }107 return parsed;108 } catch (error) {109 fetchError = error;110 }111 112 if (canUseCurrent) {113 if (detectBlocked(document)) {114 fail("当前页面出现安全验证、登录墙或帖子不存在,无法读取公开帖子。");115 }116 return document;117 }118 119 if (fetchError && fetchError.message) {120 fail(fetchError.message);121 }122 fail("请求贴吧帖子页失败,请稍后重试。");123 }124 125 function readTitle(doc) {126 const title =127 firstText(doc, [".pb-title-wrap.pc-pb-title h1", ".pb-title-wrap.pc-pb-title", ".core_title_txt", "h1"]) ||128 cleanText((doc.title || "").replace(/_百度贴吧.*$/, "").replace(/-百度贴吧.*$/, ""));129 return title;130 }131 132 function dataAuthor(data) {133 return cleanText(134 data &&135 (data.author_name ||136 data.authorName ||137 data.user_name ||138 data.userName ||139 (data.author && (data.author.user_name || data.author.userName || data.author.name))),140 );141 }142 143 function dataContent(data) {144 return cleanText(145 data &&146 data.content &&147 (data.content.content || data.content.text || data.content.post_content || data.content.postContent),148 );149 }150 151 function dataTime(data) {152 return cleanText(153 data &&154 data.content &&155 (data.content.date || data.content.time || data.content.create_time || data.content.createTime),156 );157 }158 159 function dataFloor(data) {160 const value =161 data && data.content && (data.content.post_no || data.content.postNo || data.content.floor || data.content.floor_no);162 const number = Number(value);163 return Number.isFinite(number) && number > 0 ? number : null;164 }165 166 function extractAuthor(node, data) {167 return (168 dataAuthor(data) ||169 firstText(node, [170 ".head-name",171 ".p_author_name",172 ".d_name a",173 ".user_name",174 ".user-name",175 ".j_user_card",176 ".author",177 ])178 );179 }180 181 function extractContent(node, data) {182 if (183 node &&184 node.matches &&185 node.matches(".comment-content .text, .d_post_content, .pb-content-wrap, .post_bubble_middle_inner, .content, .text")186 ) {187 const direct = cleanText(node.textContent);188 if (direct) return direct;189 }190 191 const value =192 dataContent(data) ||193 firstText(node, [194 ".comment-content .text",195 ".d_post_content",196 ".pb-content-wrap",197 ".post_bubble_middle_inner",198 ".content",199 ".text",200 ]);201 return value;202 }203 204 function extractTime(node, data) {205 const fromData = dataTime(data);206 if (fromData) return fromData;207 208 const candidates = Array.from(209 node.querySelectorAll(".tail-info, .comment-time, .pb-comment-time, .post-time, .time, [class*='time']"),210 )211 .map((item) => cleanText(item.textContent))212 .filter(Boolean);213 const matched = candidates.find((item) => /\d{4}-\d{1,2}-\d{1,2}|\d{1,2}-\d{1,2}|\d{1,2}:\d{2}/.test(item));214 return matched || "";215 }216 217 function extractFloor(node, data, fallback) {218 const fromData = dataFloor(data);219 if (fromData) return fromData;220 221 const text = firstText(node, [".floor", ".post-floor", ".tail-info", ".louzhubiaoshi_wrap"]);222 const match = text.match(/(\d+)\s*楼/);223 return match ? Number(match[1]) : fallback;224 }225 226 function normalizeMainPost(node, data, id) {227 if (!node) return null;228 const content = extractContent(node, data);229 if (!content) return null;230 return {231 author: extractAuthor(node, data),232 content,233 time: extractTime(node, data),234 url: "https://tieba.baidu.com/p/" + id,235 };236 }237 238 function normalizeReply(node, fallbackFloor) {239 const data = parseDataField(node) || {};240 const content = extractContent(node, data);241 if (!content) return null;242 return {243 floor: extractFloor(node, data, fallbackFloor),244 author: extractAuthor(node, data),245 content,246 time: extractTime(node, data),247 };248 }249 250 function parseOldPosts(doc, id, page, includeMainPost, limit) {251 const oldPosts = Array.from(doc.querySelectorAll(".l_post"));252 if (!oldPosts.length) return null;253 254 const mainData = parseDataField(oldPosts[0]) || {};255 const mainPost = includeMainPost && page === 1 ? normalizeMainPost(oldPosts[0], mainData, id) : null;256 const replyNodes = page === 1 ? oldPosts.slice(1) : oldPosts;257 const replies = [];258 259 const fallbackBase = page === 1 ? 2 : 1;260 for (const node of replyNodes) {261 const reply = normalizeReply(node, fallbackBase + replies.length);262 if (!reply) continue;263 replies.push(reply);264 if (replies.length >= limit) break;265 }266 267 return { mainPost, replies };268 }269 270 function parseNewPosts(doc, id, page, includeMainPost, limit) {271 const mainContent = doc.querySelector(".pb-content-wrap");272 const mainNode =273 mainContent &&274 (mainContent.closest(".l_post, .pb-post-area, .pb-main, .pb-thread-content, article, section") || mainContent);275 const mainPost = includeMainPost && page === 1 ? normalizeMainPost(mainNode, parseDataField(mainNode) || {}, id) : null;276 const replyNodes = Array.from(doc.querySelectorAll(".pb-comment-item"));277 const replies = [];278 279 const fallbackBase = page === 1 ? 2 : 1;280 for (const node of replyNodes) {281 const reply = normalizeReply(node, fallbackBase + replies.length);282 if (!reply) continue;283 replies.push(reply);284 if (replies.length >= limit) break;285 }286 287 if (!replies.length) {288 const contentNodes = Array.from(doc.querySelectorAll(".pb-content-wrap")).slice(page === 1 && mainContent ? 1 : 0);289 for (const node of contentNodes) {290 const reply = normalizeReply(node.closest("article, section, div") || node, replies.length + 1);291 if (!reply) continue;292 replies.push(reply);293 if (replies.length >= limit) break;294 }295 }296 297 return { mainPost, replies };298 }299 300 if (location.hostname !== "tieba.baidu.com") {301 fail("请在 tieba.baidu.com 页面执行该脚本。");302 }303 304 const id = getThreadId();305 const page = readInt(input.page, 1, 1, 50, "page");306 const limit = readInt(input.limit, 30, 1, 100, "limit");307 const includeMainPost = readBoolean(input.includeMainPost, true, "includeMainPost");308 const targetUrl = "https://tieba.baidu.com/p/" + id + "?pn=" + String(page);309 const doc = await fetchDocument(targetUrl, currentPageMatches(id, page));310 311 if (detectBlocked(doc)) {312 fail("页面出现安全验证、登录墙或帖子不存在,无法读取公开帖子。");313 }314 315 const title = readTitle(doc);316 const parsed = parseOldPosts(doc, id, page, includeMainPost, limit) || parseNewPosts(doc, id, page, includeMainPost, limit);317 const mainPost = parsed ? parsed.mainPost : null;318 const replies = parsed ? parsed.replies.slice(0, limit) : [];319 320 if (!title && !mainPost && !replies.length) {321 fail("未读取到公开帖子内容,请确认帖子 ID、页码,或检查页面是否出现安全验证。");322 }323 324 return {325 id,326 page,327 title,328 mainPost,329 replies,330 url: "https://tieba.baidu.com/p/" + id + "?pn=" + String(page),331 };332})();