匹配范围
*://*.ctrip.com/*ctrip-destination-search用途 在携程页面读取目的地联想结果,适合根据关键词获取城市、行政区、车站、地标等轻量候选项。 执行前提 需要先打开 https://www.ctrip.com/ 或其他 ctrip.com 页面后执行。 脚本在浏览器页面主世界请求公开目的地联想接口。若当前页面触发登录、验证码或网络策略限制,可能返回错误;这种情况下先在当前标签页正常完成页面访问后再执行。 …
*://*.ctrip.com/*dom.read1return await (async () => {2 const input = readParams();3 const query = toText(input.query);4 if (!query) {5 throw new Error("query is required.");6 }7 8 const limit = readInteger(input.limit, 10, 1, 30, "limit");9 const payload = {10 keyword: query,11 searchType: "D",12 pageSize: Math.max(limit, 15),13 head: {14 cid: "",15 ctok: "",16 cver: "1.0",17 lang: "01",18 sid: "8888",19 syscode: "09",20 auth: "",21 extension: [],22 },23 };24 25 const response = await fetch("https://m.ctrip.com/restapi/soa2/21881/json/gaHotelSearchEngine", {26 method: "POST",27 credentials: "include",28 headers: {29 accept: "application/json, text/plain, */*",30 "content-type": "application/json",31 },32 body: JSON.stringify(payload),33 });34 const text = await response.text();35 if (!response.ok) {36 throw new Error("Request failed with HTTP " + response.status + ".");37 }38 39 let data;40 try {41 data = JSON.parse(text);42 } catch (error) {43 throw new Error("The site returned a non-JSON response. Open the home page and try again.");44 }45 46 if (data && data.Result === false) {47 const message = toText(data.ErrorMsg || data.Message) || "The destination API returned an error.";48 throw new Error(message);49 }50 51 const responseBody = data.Response || data.response || {};52 const items = firstArray([53 responseBody.searchResults,54 responseBody.results,55 responseBody.list,56 data.searchResults,57 data.results,58 ]);59 const results = items.slice(0, limit).map(normalizeDestination);60 61 return {62 query,63 limit,64 count: results.length,65 results,66 };67 68 function readParams() {69 const value = typeof params === "undefined" ? {} : params;70 if (value == null) {71 return {};72 }73 if (typeof value !== "object" || Array.isArray(value)) {74 throw new Error("params must be an object.");75 }76 return value;77 }78 79 function normalizeDestination(item) {80 const latitude = firstNumber(item, ["gdLat", "lat", "gLat", "bdLat"]);81 const longitude = firstNumber(item, ["gdLon", "lon", "gLon", "bdLon"]);82 return {83 id: firstText(item, ["id", "poiId", "locationId"]),84 type: firstText(item, ["type", "resultsType"]),85 name: firstText(item, ["word", "name", "cityName", "displayName"]),86 displayName: firstText(item, ["displayName", "word", "name"]),87 displayType: firstText(item, ["displayType", "type"]),88 cityId: firstNumber(item, ["cityId"]),89 cityName: firstText(item, ["cityName"]),90 provinceName: firstText(item, ["provinceName"]),91 countryId: firstNumber(item, ["countryId"]),92 countryName: firstText(item, ["countryName"]),93 englishName: firstText(item, ["eName", "cityEName", "countryEName"]),94 domestic: item && typeof item.domestic === "boolean" ? item.domestic : null,95 mainland: item && typeof item.mainland === "boolean" ? item.mainland : null,96 latitude,97 longitude,98 };99 }100 101 function firstArray(values) {102 for (const value of values) {103 if (Array.isArray(value)) {104 return value;105 }106 }107 return [];108 }109 110 function firstText(item, keys) {111 for (const key of keys) {112 if (item && item[key] != null && item[key] !== "") {113 return toText(item[key]);114 }115 }116 return "";117 }118 119 function firstNumber(item, keys) {120 for (const key of keys) {121 if (item && item[key] != null && item[key] !== "") {122 const number = Number(item[key]);123 if (Number.isFinite(number)) {124 return number;125 }126 }127 }128 return null;129 }130 131 function readInteger(value, defaultValue, min, max, name) {132 const number = value == null || value === "" ? defaultValue : Number(value);133 if (!Number.isInteger(number) || number < min || number > max) {134 throw new Error(name + " must be an integer between " + min + " and " + max + ".");135 }136 return number;137 }138 139 function toText(value) {140 if (value == null) {141 return "";142 }143 if (Array.isArray(value)) {144 return value.map(toText).filter(Boolean).join(" ");145 }146 return String(value).replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();147 }148})();