匹配范围
*://weread.qq.com/*weread-book-search用途 在微信读书公开搜索中按关键词查找图书,返回标题、作者、bookId 和阅读页链接。 执行前提 在 https://weread.qq.com/ 域名下的任意页面执行。脚本读取公开搜索接口和公开搜索结果页,不要求登录;如果站点临时限制访问,可能返回 HTTP 错误或空结果。 参数 query:必填,搜索关键词。也兼容 keyword。 limit:可选…
*://weread.qq.com/*dom.read1return await (async () => {2 const input = params && typeof params === "object" ? params : {};3 const query = String(input.query || input.keyword || "").trim();4 const limit = Math.max(1, Math.min(Number(input.limit || 10) || 10, 50));5 6 if (!query) {7 throw new Error("缺少参数 query");8 }9 10 const origin = "https://weread.qq.com";11 12 const fetchJson = async (url) => {13 const response = await fetch(url.toString(), {14 credentials: "include",15 headers: {16 Accept: "application/json, text/plain, */*",17 },18 });19 20 if (!response.ok) {21 throw new Error(`请求失败:HTTP ${response.status}`);22 }23 24 return await response.json();25 };26 27 const decodeText = (value) => String(value || "").replace(/\s+/g, " ").trim();28 const buildIdentity = (title, author) => `${decodeText(title)}\u0000${decodeText(author)}`;29 30 const countBy = (items, getKey) => {31 const counts = new Map();32 for (const item of items) {33 const key = getKey(item);34 if (!key || key === "\u0000") {35 continue;36 }37 counts.set(key, (counts.get(key) || 0) + 1);38 }39 return counts;40 };41 42 const searchUrl = new URL("/web/search/global", origin);43 searchUrl.searchParams.set("keyword", query);44 45 const htmlUrl = new URL("/web/search/books", origin);46 htmlUrl.searchParams.set("keyword", query);47 48 const [data, htmlResponse] = await Promise.all([49 fetchJson(searchUrl),50 fetch(htmlUrl.toString(), {51 credentials: "include",52 headers: {53 Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",54 },55 }),56 ]);57 58 if (!htmlResponse.ok) {59 throw new Error(`搜索页面请求失败:HTTP ${htmlResponse.status}`);60 }61 62 const html = await htmlResponse.text();63 const doc = new DOMParser().parseFromString(html, "text/html");64 const htmlEntries = Array.from(doc.querySelectorAll("li.wr_bookList_item"))65 .map((item) => {66 const link = item.querySelector("a.wr_bookList_item_link");67 const title = decodeText(item.querySelector(".wr_bookList_item_title")?.textContent);68 const author = decodeText(item.querySelector(".wr_bookList_item_author")?.textContent);69 const href = link?.getAttribute("href") || "";70 71 return {72 title,73 author,74 readerUrl: href ? new URL(href, origin).toString() : "",75 };76 })77 .filter((item) => item.title && item.readerUrl);78 79 const books = Array.isArray(data?.books) ? data.books : [];80 const normalizedBooks = books.map((item) => ({81 title: decodeText(item?.bookInfo?.title),82 author: decodeText(item?.bookInfo?.author),83 bookId: decodeText(item?.bookInfo?.bookId),84 }));85 86 const exactQueues = new Map();87 const titleOnlyQueues = new Map();88 89 for (const entry of htmlEntries) {90 const key = entry.author ? buildIdentity(entry.title, entry.author) : entry.title;91 const queues = entry.author ? exactQueues : titleOnlyQueues;92 const current = queues.get(key) || [];93 current.push(entry.readerUrl);94 queues.set(key, current);95 }96 97 const apiIdentityCounts = countBy(normalizedBooks, (item) => buildIdentity(item.title, item.author));98 const htmlIdentityCounts = countBy(htmlEntries.filter((item) => item.author), (item) => buildIdentity(item.title, item.author));99 const apiTitleCounts = countBy(normalizedBooks, (item) => item.title);100 const htmlTitleCounts = countBy(htmlEntries, (item) => item.title);101 102 const takeReaderUrl = (book) => {103 const identityKey = buildIdentity(book.title, book.author);104 const titleKey = book.title;105 106 if ((apiIdentityCounts.get(identityKey) || 0) <= 1 && (htmlIdentityCounts.get(identityKey) || 0) <= 1) {107 const exact = exactQueues.get(identityKey);108 if (exact && exact.length > 0) {109 return exact.shift();110 }111 }112 113 if ((apiTitleCounts.get(titleKey) || 0) > 1 || (htmlTitleCounts.get(titleKey) || 0) > 1) {114 return "";115 }116 117 const titleOnly = titleOnlyQueues.get(titleKey);118 return titleOnly && titleOnly.length > 0 ? titleOnly.shift() : "";119 };120 121 return normalizedBooks.slice(0, limit).map((book, index) => ({122 rank: index + 1,123 title: book.title,124 author: book.author,125 bookId: book.bookId,126 readerUrl: takeReaderUrl(book),127 }));128})();