匹配范围
*://*.36kr.com/search/articles/*36kr-search-news36氪文章搜索结果提取 用途 读取 36氪文章搜索结果页中已经渲染出的文章,返回排名、标题、页面附近日期和文章地址。 执行前提 先打开 https://www.36kr.com/search/articles/<keyword> 对应的搜索结果页,再用同一个 keyword 参数执行。页面公开可访问,但结果依赖客户端渲染,若页面显示验证、空结果或加载失败,…
*://*.36kr.com/search/articles/*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 normalizeArticleUrl(href) {28 if (!href) return "";29 try {30 const url = new URL(href, "https://36kr.com");31 if (!/(\.|^)36kr\.com$/.test(url.hostname)) return "";32 if (!/\/p\/\d+/.test(url.pathname)) return "";33 return url.toString();34 } catch {35 return "";36 }37}38 39function extractDate(text) {40 const normalized = normalizeText(text);41 const match =42 normalized.match(/\d{4}[-/.]\d{1,2}[-/.]\d{1,2}(?:\s+\d{1,2}:\d{2})?/) ||43 normalized.match(/\d{1,2}月\d{1,2}日(?:\s+\d{1,2}:\d{2})?/) ||44 normalized.match(/\d{1,2}:\d{2}/);45 return match ? match[0] : "";46}47 48function extractItems(doc) {49 const seen = new Set();50 const results = [];51 const preferredLinks = doc.querySelectorAll(52 ".article-item-title a[href*='/p/'], .article-item-title[href*='/p/'], .article-item-title a[href*=\"/p/\"], .article-item-title[href*=\"/p/\"]",53 );54 const links = preferredLinks.length > 055 ? preferredLinks56 : doc.querySelectorAll("a[href*='/p/'], a[href*=\"/p/\"]");57 58 for (const link of links) {59 const title = normalizeText(link.getAttribute("title") || link.textContent);60 const url = normalizeArticleUrl(link.getAttribute("href") || link.href || "");61 if (!title || title.length < 5 || !url) continue;62 const key = `${title}\n${url}`;63 if (seen.has(key) || seen.has(url) || seen.has(title)) continue;64 seen.add(key);65 seen.add(url);66 seen.add(title);67 68 const item = link.closest("[class*='article-item'], article, li, section") || link.parentElement;69 const dateNode = item?.querySelector("[class*='time'], [class*='date'], time");70 const date = normalizeText(dateNode?.textContent || "") || extractDate(item?.textContent || "");71 results.push({ title, date, url });72 }73 74 return results;75}76 77async function waitForItems(doc, timeoutMs) {78 const deadline = Date.now() + timeoutMs;79 let items = extractItems(doc);80 while (Date.now() < deadline && items.length === 0) {81 await sleep(250);82 items = extractItems(doc);83 }84 return items;85}86 87function assertCurrentPage(targetUrl) {88 const current = new URL(location.href);89 const target = new URL(targetUrl);90 if (current.origin !== target.origin || current.pathname !== target.pathname) {91 throw new Error(`Open ${targetUrl} first, then run this script`);92 }93}94 95const keyword = requiredKeyword();96const limit = integerParam("limit", 20, 1, 50);97const timeoutMs = integerParam("timeoutMs", 8000, 1000, 30000);98const targetUrl = `https://www.36kr.com/search/articles/${encodeURIComponent(keyword)}`;99assertCurrentPage(targetUrl);100 101const items = await waitForItems(document, timeoutMs);102 103if (items.length === 0) {104 throw new Error(`No search results found on ${targetUrl}`);105}106 107return items.slice(0, limit).map((item, index) => ({108 rank: index + 1,109 title: item.title,110 date: item.date,111 url: item.url,112}));