京东商品参数对比工具

该脚本可用于对比不限数量的同类型商品(如:手机、笔记本)的详细参数

  1. // ==UserScript==
  2. // @name 京东商品参数对比工具
  3. // @namespace http://tampermonkey.net/
  4. // @version 2077.0.9
  5. // @description 该脚本可用于对比不限数量的同类型商品(如:手机、笔记本)的详细参数
  6. // @author Yihang Wang <wangyihanger@gmail.com>
  7. // @match https://item.jd.com/*
  8. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @license MIT
  12. // ==/UserScript==
  13. (function () {
  14. 'use strict';
  15.  
  16. var apiServer = "https://jd-compare.authu.online";
  17. var version = '2077.0.9';
  18. var itemIDs = GM_getValue("jd-price-compare-item-ids", []);
  19. var relatedItemIDs = getRelatedItemIDs(document);
  20. var userID = getUserID();
  21.  
  22. function pollUntil(conditionFn, interval = 1000, maxAttempts = 10) {
  23. return new Promise((resolve, reject) => {
  24. let attempts = 0;
  25. function checkCondition() {
  26. if (conditionFn()) {
  27. resolve();
  28. } else if (attempts < maxAttempts) {
  29. attempts++;
  30. setTimeout(checkCondition, interval);
  31. } else {
  32. reject(new Error('Polling timed out'));
  33. }
  34. }
  35. checkCondition();
  36. });
  37. }
  38.  
  39. function getItemID(doc) {
  40. let a = doc.querySelector('a.follow.J-follow[data-id]');
  41. return a ? a.getAttribute('data-id') : "unknown";
  42. }
  43.  
  44. function getRelatedItemIDs(doc) {
  45. var dataSkuValues = [getItemID(doc)];
  46. if (Object.keys(pageConfig.product.colorSize).length > 0) {
  47. pageConfig.product.colorSize.forEach(function (item) {
  48. dataSkuValues.push(item.skuId.toString());
  49. });
  50. }
  51. let itemIdSet = new Set(dataSkuValues);
  52. return Array.from(itemIdSet.values());
  53. }
  54.  
  55. function getPrice(doc) {
  56. const itemID = getItemID(doc);
  57. const targetSelector = `.price.J-p-${itemID}`;
  58. const targetNode = doc.querySelector(targetSelector);
  59. return parseFloat(targetNode.innerText);
  60. }
  61.  
  62. function getBasicInfo(doc) {
  63. const basicInfoElement = doc.querySelector('#detail > div.tab-con > div:nth-child(1) > div.p-parameter');
  64. const basicInfo = {};
  65.  
  66. basicInfoElement.querySelectorAll('li').forEach(dl => {
  67. let text = dl.textContent.trim();
  68. console.log(text);
  69. if (text.indexOf(":") < 0) {
  70. console.error(`invalid basic info: ${text}, colon is not present`);
  71. return;
  72. }
  73. let items = text.split(":");
  74. if (items.length != 2) {
  75. console.error(`invalid basic info: ${text}, incorrect number of items`);
  76. return;
  77. }
  78. const key = items[0]
  79. const value = items[1]
  80. basicInfo[key] = value;
  81. });
  82.  
  83. return basicInfo;
  84. }
  85.  
  86. function getMainInfo(doc) {
  87. const mainInfoElements = doc.querySelectorAll('.Ptable-item');
  88. const mainInfo = {};
  89.  
  90. mainInfoElements.forEach(item => {
  91. const key = item.querySelector('h3').textContent.trim();
  92. const values = {};
  93.  
  94. item.querySelectorAll('dl').forEach(dl => {
  95. const detailKey = dl.querySelector('dt').textContent.trim();
  96. const hintElement = dl.querySelector('dd.Ptable-tips');
  97. var tableHint = "";
  98. if (hintElement != null) {
  99. const tableHint = hintElement.textContent.trim();
  100. }
  101. const detailValue = dl.querySelector('dd:not(.Ptable-tips)').textContent.trim();
  102. const key = detailKey;
  103. values[key] = detailValue;
  104. });
  105.  
  106. mainInfo[key] = values;
  107. });
  108.  
  109. return mainInfo;
  110. }
  111.  
  112. function getPackageList(doc) {
  113. const packageListElement = doc.querySelector('.package-list p');
  114. return packageListElement.textContent.trim();
  115. }
  116.  
  117. function getImageUrl(doc) {
  118. const imageElement = doc.querySelector("#spec-list > ul > li:nth-child(1) > img");
  119. // https://img13.360buyimg.com/ n5/s54x54_jfs/t1/ 216364/18/36214/152585/65ade2ffFfabd3665/a7fe2701bfcd0c0a.jpg.avif
  120. // https://img10.360buyimg.com/ n0/jfs/t1/ 235161/18/13361/139190/65bef13aF2b8cfc2f/d4f68e1e90bd1677.jpg.avif
  121. let items = imageElement.src.split("/");
  122. items[3] = 'n0'
  123. items[4] = 'jfs'
  124. items[5] = 't1'
  125. return items.join("/");
  126. }
  127.  
  128. function parseItemWithDocument(doc) {
  129. let basicInfo = getBasicInfo(doc);
  130. let mainInfo = getMainInfo(doc);
  131. let packageList = getPackageList(doc);
  132. let imageUrl = getImageUrl(doc);
  133. let itemID = getItemID(doc);
  134. let data = {
  135. "商品编号": itemID,
  136. "基本信息": basicInfo,
  137. "主体信息": mainInfo,
  138. "包装信息": packageList,
  139. "价格": 'N/A',
  140. "图片": imageUrl,
  141. };
  142. return data;
  143. }
  144.  
  145. function parseItemByID(itemID) {
  146. return new Promise(function (resolve, reject) {
  147. let endpoint = `https://item.jd.com/${itemID}.html`;
  148. fetch(endpoint)
  149. .then(response => {
  150. if (!response.ok) {
  151. throw new Error(`HTTP error! Status: ${response.status}`);
  152. }
  153. resolve(response.text());
  154. })
  155. .catch(error => {
  156. console.error('Error:', error);
  157. });
  158. });
  159. }
  160.  
  161. async function parseItem(itemID) {
  162. if (itemID == getItemID(document)) {
  163. pollUntil(() => (!isNaN(getPrice(document))));
  164. let price = getPrice(document);
  165. let data = parseItemWithDocument(document);
  166. data["价格"] = price;
  167. return data;
  168. } else {
  169. let html = await parseItemByID(itemID);
  170. let parser = new DOMParser();
  171. let doc = parser.parseFromString(html, 'text/html');
  172. let item = parseItemWithDocument(doc);
  173. return item;
  174. }
  175. }
  176.  
  177. async function appendList(itemID) {
  178. let endpoint = `${apiServer}/api/v1/item`;
  179. let item = await parseItem(itemID);
  180. let body = JSON.stringify(item);
  181. fetch(endpoint, {
  182. method: 'POST',
  183. headers: {
  184. 'Content-Type': 'application/json',
  185. 'Jd-Compare-Version': version,
  186. 'Jd-Compare-User-Id': userID,
  187. },
  188. body: body,
  189. })
  190. .then(response => {
  191. if (!response.ok) {
  192. throw new Error(`HTTP error! Status: ${response.status}`);
  193. }
  194. console.log(response);
  195. return response.json();
  196. })
  197. .then(data => {
  198. let itemIdList = GM_getValue("jd-price-compare-item-ids", []);
  199. let itemIdSet = new Set(itemIdList)
  200. itemIdSet.add(itemID);
  201. GM_setValue("jd-price-compare-item-ids", Array.from(itemIdSet.values()));
  202. let statusLine = document.getElementById('jd-price-compare-status-line');
  203. statusLine.textContent = `已成功添加 ${itemIdSet.size} 个待对比商品`;
  204. })
  205. .catch(error => {
  206. console.error('Error:', error);
  207. });
  208. }
  209.  
  210. function createList() {
  211. let itemIdList = GM_getValue("jd-price-compare-item-ids", []);
  212. let statusLine = document.getElementById('jd-price-compare-status-line');
  213. if (itemIdList.length == 0) {
  214. statusLine.textContent = `当前待对比商品列表为空,请先点击添加商品按钮。`;
  215. return
  216. }
  217. statusLine.textContent = `正在创建 ${itemIdList.length} 个商品的对比列表...`;
  218. let endpoint = `${apiServer}/api/v1/list`;
  219. let listId = fetch(endpoint, {
  220. method: 'POST',
  221. headers: {
  222. 'Content-Type': 'application/json',
  223. 'Jd-Compare-Version': version,
  224. 'Jd-Compare-User-Id': userID,
  225. },
  226. body: JSON.stringify(itemIdList)
  227. })
  228. .then(response => {
  229. if (!response.ok) {
  230. throw new Error(`HTTP error! Status: ${response.status}`);
  231. }
  232. return response.json();
  233. })
  234. .then(data => {
  235. statusLine.textContent = `对比成功,正在自动打开结果页面...`;
  236. return compareList(data._id);
  237. })
  238. .catch(error => {
  239. console.error('Error:', error);
  240. });
  241. return listId;
  242. }
  243.  
  244. function compareList(listId) {
  245. let statusLine = document.getElementById('jd-price-compare-status-line');
  246. let endpoint = `${apiServer}/api/v1/list/${listId}/compare`;
  247. let resultUrl = fetch(endpoint, {
  248. method: 'GET',
  249. headers: {
  250. 'Jd-Compare-Version': version,
  251. 'Jd-Compare-User-Id': userID,
  252. },
  253. })
  254. .then(response => {
  255. if (!response.ok) {
  256. throw new Error(`HTTP error! Status: ${response.status}`);
  257. }
  258. return response.text();
  259. })
  260. .then(data => {
  261. let link = document.createElement('a');
  262. let url = `${apiServer}/${data}`;
  263. link.href = url;
  264. link.textContent = `列表 ID ${listId} `; // 修正属性名,应该是textContent
  265. statusLine.textContent = `对比完成,正在打开商品对比结果页面。`;
  266. statusLine.appendChild(document.createElement("br"));
  267. // 创建并添加<p>元素
  268. let p = document.createElement("p");
  269. p.textContent = `如未打开,也可以直接点击下方链接`;
  270. statusLine.appendChild(p);
  271. statusLine.appendChild(link);
  272. statusLine.appendChild(document.createElement("br"));
  273. // 创建并添加文本节点
  274. let textNode = document.createTextNode(`您可以继续添加其他待对比商品。`);
  275. statusLine.appendChild(textNode);
  276. GM_setValue("jd-price-compare-item-ids", []);
  277. window.open(url);
  278. })
  279. .catch(error => {
  280. console.error('Error:', error);
  281. });
  282. return resultUrl;
  283. }
  284.  
  285. function addSingleButton() {
  286. let button = document.createElement('a');
  287. let statusLine = document.getElementById('jd-price-compare-status-line');
  288. button.href = '#';
  289. button.id = 'jd-price-compare-add-single-button';
  290. button.textContent = `添加本商品`;
  291. button.style.backgroundColor = '#3498db';
  292. button.style.color = '#ffffff';
  293. button.style.padding = '3px';
  294. button.addEventListener("click", function (event) {
  295. event.preventDefault();
  296. let itemID = getItemID(document);
  297. statusLine.textContent = `正在获取商品(ID${itemID})详信息...`;
  298. appendList(itemID);
  299. updateCompareButton();
  300. });
  301. let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
  302. cartButtton.appendChild(button);
  303. }
  304.  
  305. function addAllButton() {
  306. let button = document.createElement('a');
  307. let statusLine = document.getElementById('jd-price-compare-status-line');
  308. button.href = '#';
  309. button.id = 'jd-price-compare-add-all-button';
  310. button.textContent = `添加所有 ${relatedItemIDs.length} 个型号`;
  311. button.style.backgroundColor = '#3498db';
  312. button.style.color = '#ffffff';
  313. button.style.padding = '3px';
  314. button.addEventListener("click", function (event) {
  315. event.preventDefault();
  316. relatedItemIDs.forEach(function (itemID) {
  317. statusLine.textContent = `正在获取商品(ID${itemID})详信息...`;
  318. appendList(itemID);
  319. });
  320. });
  321. let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
  322. cartButtton.appendChild(button);
  323. }
  324.  
  325. function updateCompareButton() {
  326. let element = document.getElementById('jd-price-compare-start-button');
  327. let itemIdList = GM_getValue("jd-price-compare-item-ids", []);
  328. element.textContent = `开始对比 (${itemIdList.length})`;
  329. }
  330.  
  331. function addCompareButton() {
  332. let button = document.createElement('a');
  333. button.href = '#';
  334. button.id = 'jd-price-compare-start-button';
  335. button.style.backgroundColor = '#3498db';
  336. button.style.color = '#ffffff';
  337. button.style.padding = '3px';
  338. button.textContent = `开始对比 (${itemIDs.length})`;
  339. button.addEventListener("click", function (event) {
  340. event.preventDefault();
  341. createList();
  342. });
  343. let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
  344. cartButtton.appendChild(button);
  345. }
  346.  
  347. function addFeedbackButton() {
  348. let button = document.createElement('a');
  349. button.href = '#';
  350. button.id = 'jd-price-add-feedback-button';
  351. button.style.backgroundColor = '#3498db';
  352. button.style.color = '#ffffff';
  353. button.style.padding = '3px';
  354. button.textContent = `意见反馈`;
  355. button.addEventListener("click", function (event) {
  356. event.preventDefault();
  357. const url = "https://gf.qytechs.cn/zh-CN/scripts/486915-%E4%BA%AC%E4%B8%9C%E5%95%86%E5%93%81%E5%8F%82%E6%95%B0%E5%AF%B9%E6%AF%94%E5%B7%A5%E5%85%B7/feedback";
  358. window.open(url);
  359. });
  360. let cartButtton = document.querySelector('#preview > div.preview-info > div.left-btns.shieldShopInfo');
  361. cartButtton.appendChild(button);
  362. }
  363.  
  364. function getUserID() {
  365. let userID = GM_getValue("jd-price-compare-user-id", "");
  366. if (userID == "") {
  367. userID = Math.random().toString(36).substring(2);
  368. GM_setValue("jd-price-compare-user-id", userID);
  369. }
  370. return userID;
  371. }
  372.  
  373. function addStatusLine() {
  374. let statusLine = document.createElement('p');
  375. statusLine.id = 'jd-price-compare-status-line';
  376. statusLine.style.color = '#000000';
  377. statusLine.style.padding = '3px';
  378. statusLine.textContent = `京东商品参数对比工具 v${version}`;
  379.  
  380. let statusLineDiv = document.createElement('div');
  381. statusLineDiv.id = 'jd-price-compare-status-line-div';
  382. statusLineDiv.style.textAlign = 'center';
  383. statusLineDiv.appendChild(statusLine);
  384.  
  385. let targetElement = document.querySelector('#preview > div.preview-info');
  386. targetElement.parentNode.insertBefore(statusLineDiv, targetElement.nextSibling);
  387. }
  388.  
  389. function main() {
  390. addStatusLine();
  391. addSingleButton();
  392. if (relatedItemIDs.length > 1) {
  393. addAllButton();
  394. }
  395. addCompareButton();
  396. addFeedbackButton();
  397. setInterval(updateCompareButton, 512);
  398. }
  399.  
  400. window.addEventListener('load', main);
  401. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址