匹配范围
*://search.smzdm.com/*smzdm-search-deals什么值得买搜索好价提取 用途 读取什么值得买搜索结果页中的好价条目,返回标题、价格、商城、更新时间、互动计数和详情地址。 执行前提 先打开 https://search.smzdm.com/?c=home&s=<keyword>&v=b 对应的搜索结果页,再用同一个 keyword 参数执行。搜索页可能受 Cookie、登录状态、访问频率或验证页影响;若页…
*://search.smzdm.com/*dom.read1const rawParams = params && typeof params === "object" ? params : {};2 3function integerParam(name, fallback, min, max) {4 const raw = rawParams[name];5 const value = raw === undefined || raw === null || raw === "" ? fallback : Number(raw);6 if (!Number.isInteger(value) || value < min || value > max) {7 throw new Error(`${name} must be an integer between ${min} and ${max}`);8 }9 return value;10}11 12function requiredKeyword() {13 const raw = rawParams.keyword ?? rawParams.query;14 const keyword = raw === undefined || raw === null ? "" : String(raw).trim();15 if (!keyword) throw new Error("keyword is required");16 return keyword;17}18 19function sleep(ms) {20 return new Promise(resolve => setTimeout(resolve, ms));21}22 23function normalizeText(value) {24 return (value || "").replace(/\s+/g, " ").trim();25}26 27function normalizeCount(text) {28 const raw = normalizeText(text).replace(/,/g, "");29 const match = raw.match(/(\d+(?:\.\d+)?)\s*([万kK]?)/);30 if (!match) return 0;31 const base = Number(match[1]);32 if (!Number.isFinite(base)) return 0;33 if (match[2] === "万") return Math.round(base * 10000);34 if (match[2] === "k" || match[2] === "K") return Math.round(base * 1000);35 return Math.round(base);36}37 38function trustedDealUrl(raw) {39 const text = normalizeText(raw);40 if (!text) return "";41 try {42 const url = text.startsWith("/")43 ? new URL(text, "https://www.smzdm.com")44 : new URL(text, location.href);45 const hostname = url.hostname.toLowerCase();46 if (url.protocol !== "https:") return "";47 if (hostname !== "www.smzdm.com" && hostname !== "post.smzdm.com") return "";48 return url.toString();49 } catch {50 return "";51 }52}53 54function directText(el) {55 if (!el) return "";56 return Array.from(el.childNodes)57 .filter(node => node.nodeType === Node.TEXT_NODE)58 .map(node => normalizeText(node.textContent))59 .filter(Boolean)60 .join(" ");61}62 63function extractDeals(doc, limit) {64 const rows = doc.querySelectorAll("li.feed-row-wide");65 const results = [];66 67 for (const row of rows) {68 if (results.length >= limit) break;69 const titleEl =70 row.querySelector("h5.feed-block-title > a") ||71 row.querySelector("h5 > a") ||72 row.querySelector("a[href*='/p/']");73 if (!titleEl) continue;74 75 const title = normalizeText(titleEl.getAttribute("title") || titleEl.textContent);76 const url = trustedDealUrl(titleEl.getAttribute("href") || titleEl.href || "");77 if (!title || !url) continue;78 79 const extrasEl =80 row.querySelector(".z-feed-foot-r .feed-block-extras") ||81 row.querySelector(".feed-block-extras");82 const mallEl =83 row.querySelector(".z-feed-foot-r .feed-block-extras span") ||84 row.querySelector(".z-feed-foot-r span");85 const zhiEl = row.querySelector(".price-btn-up .unvoted-wrap span");86 const buzhiEl = row.querySelector(".price-btn-down .unvoted-wrap span");87 const favEl = row.querySelector(".feed-btn-fav span");88 const commentEl = row.querySelector(".feed-btn-comment");89 90 results.push({91 rank: results.length + 1,92 title,93 price: normalizeText(row.querySelector(".z-highlight")?.textContent || ""),94 mall: normalizeText(mallEl?.textContent || ""),95 updated_at: directText(extrasEl),96 zhi_count: normalizeCount(zhiEl?.textContent || ""),97 buzhi_count: normalizeCount(buzhiEl?.textContent || ""),98 favorite_count: normalizeCount(favEl?.textContent || ""),99 comments: normalizeCount(commentEl?.textContent || commentEl?.getAttribute("title") || ""),100 url,101 });102 }103 104 return results;105}106 107async function waitForDeals(doc, limit, timeoutMs) {108 const deadline = Date.now() + timeoutMs;109 let items = extractDeals(doc, limit);110 while (Date.now() < deadline && items.length === 0) {111 await sleep(250);112 items = extractDeals(doc, limit);113 }114 return items;115}116 117function assertCurrentPage(targetUrl, keyword) {118 const current = new URL(location.href);119 if (current.hostname !== "search.smzdm.com" || current.searchParams.get("s") !== keyword) {120 throw new Error(`Open ${targetUrl} first, then run this script`);121 }122}123 124const keyword = requiredKeyword();125const limit = integerParam("limit", 20, 1, 100);126const timeoutMs = integerParam("timeoutMs", 8000, 1000, 30000);127const targetUrl = `https://search.smzdm.com/?c=home&s=${encodeURIComponent(keyword)}&v=b`;128assertCurrentPage(targetUrl, keyword);129 130const items = await waitForDeals(document, limit, timeoutMs);131 132if (items.length === 0) {133 throw new Error(`No deal results found on ${targetUrl}`);134}135 136return items.slice(0, limit);