匹配范围
*://*.autohome.com.cn/*autohome-brand-series用途 读取汽车之家公开车系目录,按品牌中文名或 A-Z 首字母返回品牌下的车系列表。适合快速查询某个品牌的车系 ID、车系名称、指导价和相关页面链接。 执行前提 先打开汽车之家页面,建议使用 https://www.autohome.com.cn/。脚本会在浏览器页面主世界中请求 https://www.autohome.com.cn/grade/carh…
*://*.autohome.com.cn/*dom.read1return await (async () => {2 const input = typeof params === "object" && params !== null ? params : {};3 const rawBrand = typeof input.brand === "string" ? input.brand.trim() : "";4 const rawLimit = input.limit === undefined ? 20 : Number(input.limit);5 6 if (!rawBrand) {7 throw new Error("params.brand is required. Use a Chinese brand name or one A-Z initial.");8 }9 10 if (!Number.isFinite(rawLimit) || rawLimit <= 0) {11 throw new Error("params.limit must be a positive number.");12 }13 14 const limit = Math.min(Math.floor(rawLimit), 100);15 const isInitialQuery = /^[A-Za-z]$/.test(rawBrand);16 const queryText = rawBrand.replace(/\s+/g, "").toLowerCase();17 const initials = isInitialQuery18 ? [rawBrand.toUpperCase()]19 : "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");20 21 const cleanText = (value) =>22 String(value || "")23 .replace(/\s+/g, " ")24 .replace(/\u00a0/g, " ")25 .trim();26 27 const normalizeText = (value) => cleanText(value).replace(/\s+/g, "").toLowerCase();28 29 const absoluteUrl = (value) => {30 if (!value) return null;31 try {32 return new URL(value, window.location.href).href;33 } catch (error) {34 return null;35 }36 };37 38 const parseBrandDocument = (html, initial) => {39 const doc = new DOMParser().parseFromString(html, "text/html");40 const rows = [];41 const brands = [];42 43 for (const brandNode of Array.from(doc.querySelectorAll("dl[id]"))) {44 const brandId = brandNode.getAttribute("id") || null;45 const brandName = cleanText(46 brandNode.querySelector("dt div a")?.textContent ||47 brandNode.querySelector("dt a:last-of-type")?.textContent48 );49 50 if (!brandName) continue;51 52 const brandKey = normalizeText(brandName);53 const isBrandMatch =54 isInitialQuery ||55 brandKey === queryText ||56 brandKey.includes(queryText) ||57 queryText.includes(brandKey);58 59 if (!isBrandMatch) continue;60 brands.push({ brandId, brandName, initial });61 62 let makerName = brandName;63 const detailNode = brandNode.querySelector("dd");64 const children = detailNode ? Array.from(detailNode.children) : [];65 66 for (const child of children) {67 if (child.matches(".h3-tit")) {68 makerName = cleanText(child.textContent) || brandName;69 continue;70 }71 72 if (!child.matches("ul")) continue;73 74 for (const itemNode of Array.from(child.querySelectorAll("li[id^='s']"))) {75 if (rows.length >= limit) break;76 77 const seriesId = (itemNode.getAttribute("id") || "").replace(/^s/, "");78 const titleLink = itemNode.querySelector("h4 a[href]");79 const priceLink = itemNode.querySelector("a.red[href]");80 const reputationLink = Array.from(itemNode.querySelectorAll("a[href]")).find(81 (link) => link.textContent.includes("口碑") || link.href.includes("//k.autohome.com.cn/")82 );83 const forumLink = Array.from(itemNode.querySelectorAll("a[href]")).find((link) =>84 link.textContent.includes("论坛")85 );86 const text = cleanText(itemNode.textContent);87 let guidePrice = cleanText(priceLink?.textContent);88 89 if (!guidePrice && text.includes("指导价:暂无")) {90 guidePrice = "暂无";91 }92 93 rows.push({94 brandId,95 brandName,96 makerName,97 seriesId,98 seriesName: cleanText(titleLink?.textContent),99 guidePrice: guidePrice || null,100 seriesUrl: absoluteUrl(titleLink?.getAttribute("href")),101 priceUrl: absoluteUrl(priceLink?.getAttribute("href")),102 reputationUrl: absoluteUrl(reputationLink?.getAttribute("href")),103 forumUrl: absoluteUrl(forumLink?.getAttribute("href")),104 initial,105 });106 }107 108 if (rows.length >= limit) break;109 }110 }111 112 return { rows, brands };113 };114 115 const items = [];116 const matchedBrands = [];117 const searchedInitials = [];118 119 for (const initial of initials) {120 if (items.length >= limit) break;121 searchedInitials.push(initial);122 123 const response = await fetch(`https://www.autohome.com.cn/grade/carhtml/${initial}.html`, {124 credentials: "include",125 });126 127 if (!response.ok) {128 throw new Error(`Failed to fetch brand catalog for initial ${initial}: HTTP ${response.status}.`);129 }130 131 const html = await response.text();132 const parsed = parseBrandDocument(html, initial);133 items.push(...parsed.rows.slice(0, limit - items.length));134 135 for (const brand of parsed.brands) {136 if (!matchedBrands.some((item) => item.brandId === brand.brandId)) {137 matchedBrands.push(brand);138 }139 }140 141 if (!isInitialQuery && matchedBrands.length > 0) break;142 }143 144 return {145 query: rawBrand,146 limit,147 searchedInitials,148 matchedBrands,149 count: items.length,150 items,151 };152})();