匹配范围
*://*.36kr.com/hot-list/*36kr-hot-news36氪热榜文章提取 用途 读取 36氪热榜页面中已经渲染出的文章链接,返回排名、标题和文章地址。 执行前提 先按参数打开对应热榜页面后再执行。type 为 catalog 时打开 https://www.36kr.com/hot-list/catalog;type 为 renqi、zonghe 或 shoucang 时打开 https://www.36kr…
*://*.36kr.com/hot-list/*dom.read1const rawParams = params && typeof params === "object" ? params : {};2const typeLabels = {3 catalog: "热门资讯",4 renqi: "人气榜",5 zonghe: "综合榜",6 shoucang: "收藏榜",7};8 9function integerParam(name, fallback, min, max) {10 const raw = rawParams[name];11 const value = raw === undefined || raw === null || raw === "" ? fallback : Number(raw);12 if (!Number.isInteger(value) || value < min || value > max) {13 throw new Error(`${name} must be an integer between ${min} and ${max}`);14 }15 return value;16}17 18function stringParam(name, fallback) {19 const raw = rawParams[name];20 if (raw === undefined || raw === null || raw === "") return fallback;21 return String(raw).trim();22}23 24function shanghaiDate() {25 return new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString().slice(0, 10);26}27 28function normalizeDate(raw) {29 const value = raw || shanghaiDate();30 if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {31 throw new Error("date must use YYYY-MM-DD");32 }33 return value;34}35 36function buildHotUrl(type, date) {37 if (type === "catalog") return "https://www.36kr.com/hot-list/catalog";38 return `https://www.36kr.com/hot-list/${type}/${date}/1`;39}40 41function sleep(ms) {42 return new Promise(resolve => setTimeout(resolve, ms));43}44 45function normalizeText(value) {46 return (value || "").replace(/\s+/g, " ").trim();47}48 49function normalizeArticleUrl(href) {50 if (!href) return "";51 try {52 const url = new URL(href, "https://36kr.com");53 if (!/(\.|^)36kr\.com$/.test(url.hostname)) return "";54 if (!/\/p\/\d+/.test(url.pathname)) return "";55 return url.toString();56 } catch {57 return "";58 }59}60 61function extractItems(doc) {62 const seen = new Set();63 const results = [];64 const links = doc.querySelectorAll("a[href*='/p/'], a[href*=\"/p/\"]");65 66 for (const link of links) {67 const title = normalizeText(link.getAttribute("title") || link.textContent);68 const url = normalizeArticleUrl(link.getAttribute("href") || link.href || "");69 if (!title || title.length < 5 || !url) continue;70 const key = `${title}\n${url}`;71 if (seen.has(key) || seen.has(url) || seen.has(title)) continue;72 seen.add(key);73 seen.add(url);74 seen.add(title);75 results.push({ title, url });76 }77 78 return results;79}80 81async function waitForItems(doc, timeoutMs) {82 const deadline = Date.now() + timeoutMs;83 let items = extractItems(doc);84 while (Date.now() < deadline && items.length === 0) {85 await sleep(250);86 items = extractItems(doc);87 }88 return items;89}90 91function assertCurrentPage(targetUrl) {92 const current = new URL(location.href);93 const target = new URL(targetUrl);94 const currentPath = current.pathname.replace(/\/$/, "");95 const targetPath = target.pathname.replace(/\/$/, "");96 if (current.origin !== target.origin || currentPath !== targetPath) {97 throw new Error(`Open ${targetUrl} first, then run this script`);98 }99}100 101const limit = integerParam("limit", 20, 1, 50);102const timeoutMs = integerParam("timeoutMs", 8000, 1000, 30000);103const type = stringParam("type", "catalog");104if (!Object.prototype.hasOwnProperty.call(typeLabels, type)) {105 throw new Error(`type must be one of ${Object.keys(typeLabels).join(", ")}`);106}107 108const date = normalizeDate(stringParam("date", ""));109const targetUrl = buildHotUrl(type, date);110assertCurrentPage(targetUrl);111 112const items = await waitForItems(document, timeoutMs);113 114if (items.length === 0) {115 throw new Error(`No hot-list items found on ${targetUrl}`);116}117 118return items.slice(0, limit).map((item, index) => ({119 rank: index + 1,120 title: item.title,121 url: item.url,122}));