亚马逊关键词排名-Amazon keywords Positioning by Asin

1.在亚马逊搜索结果页上定位ASIN, 获取排名 2.代码重构————dom操作->fetch+DOMParser 3.结果面板 4.批量导入excel关键词表,返回关键词排名表.xlsx

  1. // ==UserScript==
  2. // @name 亚马逊关键词排名-Amazon keywords Positioning by Asin
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.5.0
  5. // @description 1.在亚马逊搜索结果页上定位ASIN, 获取排名 2.代码重构————dom操作->fetch+DOMParser 3.结果面板 4.批量导入excel关键词表,返回关键词排名表.xlsx
  6. // @author You
  7. // @match https://www.amazon.com/*
  8. // @match https://www.amazon.co.uk/*
  9. // @match https://www.amazon.ca/*
  10. // @icon https://www.amazon.com/favicon.ico
  11. // @license MIT
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15.  
  16. (async function () {
  17. 'use strict';
  18.  
  19. // —— 配置区 ——
  20. const DEFAULT_MAX_PAGES = 2; // 默认最多搜索页数
  21. const STYLE = `
  22. /* 容器 */
  23. #tm-asin-container {
  24. position: fixed;
  25. top: 60px;
  26. left: 0; right: 0;
  27. padding: 6px 12px;
  28. background: #fff;
  29. box-shadow: 0 2px 12px rgba(0,0,0,0.1);
  30. font-family: "Helvetica Neue", Arial, sans-serif;
  31. z-index: 9999;
  32. display: flex;
  33. align-items: center;
  34. }
  35.  
  36. /* tag-wrapper-css */
  37. #tm-asin-container #tag-wrapper {
  38. display: flex;
  39. flex-wrap: wrap;
  40. align-items: center;
  41. gap: 8px;
  42. margin-right: 6px;
  43. }
  44.  
  45. .tag-item {
  46. display: inline-flex;
  47. align-items: center;
  48. height: 28px;
  49. padding: 0 8px;
  50. font-size: 14px;
  51. background: #ecf5ff;
  52. color: #409eff;
  53. border: 1px solid #b3d8ff;
  54. border-radius: 4px;
  55. }
  56.  
  57. .tag-item .tag-close {
  58. display: inline-block;
  59. margin-left: 4px;
  60. font-style: normal;
  61. cursor: pointer;
  62. color: #409eff;
  63. font-weight: bold;
  64. }
  65.  
  66. .tag-item .tag-close:hover {
  67. color: #66b1ff;
  68. }
  69.  
  70. .tag-add-btn {
  71. display: inline-flex;
  72. align-items: center;
  73. height: 32px;
  74. padding: 0 12px;
  75. font-size: 14px;
  76. color: #409eff;
  77. background: #fff;
  78. border: 1px solid #409eff;
  79. border-radius: 4px;
  80. cursor: pointer;
  81. transition: background-color .2s;
  82. }
  83.  
  84. .tag-add-btn:hover {
  85. background-color: #ecf5ff;
  86. }
  87.  
  88. /* 临时输入框 */
  89. .tag-input {
  90. flex: 1;
  91. min-width: 100px;
  92. height: 28px;
  93. padding: 0 6px;
  94. font-size: 14px;
  95. border: 1px solid #dcdfe6;
  96. border-radius: 4px;
  97. outline: none;
  98. }
  99. /* input错误提示 */
  100. .input-error {
  101. border-color: red;
  102. outline: none;
  103. box-shadow: 0 0 5px red;
  104. }
  105.  
  106. /* ASIN 和页数输入框 */
  107. #tm-asin-container input[type="number"] {
  108. margin-right: 14px;
  109. font-size: 16px;
  110. border: 1px solid #dcdfe6;
  111. border-radius: 4px;
  112. color: #606266;
  113. outline: none;
  114. transition: border-color .2s, box-shadow .2s;
  115. width: 200px;
  116. box-sizing: border-box;
  117. }
  118. #tm-asin-container input:focus {
  119. border-color: #409eff;
  120. box-shadow: 0 0 2px rgba(64,158,255,0.2);
  121. }
  122.  
  123. /* 文件上传按钮 追加 ElementUI Button 样式 */
  124. .el-button {
  125. display: inline-block;
  126. line-height: 1.5;
  127. white-space: nowrap;
  128. font-size: 14px;
  129. font-weight: 500;
  130. padding: 6px 12px;
  131. border: 1px solid #dcdfe6;
  132. border-radius: 4px;
  133. cursor: pointer;
  134. user-select: none;
  135. background-color: #fff;
  136. color: #606266;
  137. transition: background-color .2s, border-color .2s, color .2s;
  138. margin-right: 12px;
  139. }
  140. .el-button--primary {
  141. background-color: #409eff;
  142. border-color: #409eff;
  143. color: #fff;
  144. }
  145. .el-button--primary:hover {
  146. background-color: #66b1ff;
  147. border-color: #66b1ff;
  148. }
  149. /* 按钮 */
  150. #tm-asin-container button {
  151. margin-right: 12px;
  152. padding: 5px 10px;
  153. font-size: 14px;
  154. font-weight: 500;
  155. color: #fff;
  156. background-color: #409eff;
  157. border: 1px solid #409eff;
  158. border-radius: 4px;
  159. cursor: pointer;
  160. transition: background-color .2s, border-color .2s;
  161. }
  162. #tm-asin-container button:hover:not([disabled]) {
  163. background-color: #66b1ff;
  164. border-color: #66b1ff;
  165. }
  166.  
  167. #tm-asin-container span {
  168. font-size: 16px;
  169. }
  170. /* 状态文字:紧跟按钮后面 */
  171. #tm-asin-container span#tm-status {
  172. margin-left: 12px;
  173. margin-right: 12px;
  174. font-size: 16px;
  175. color:rgb(110, 111, 111);
  176. }
  177. /* 面板button样式 */
  178. #batch-results-panel .rp-jump-btn {
  179. margin-top: 2px;
  180. margin-left: 6px;
  181. margin-bottom: 3.5px;
  182. line-height: 12px;
  183. color: #5ba7f4;
  184. background-color: #ecf5ff;
  185. border: 1px solid;
  186. border-radius: 5px;
  187. padding-top: 2px;
  188. cursor: pointer;
  189. transition: background-color .2s, color .2s, border-color .2s;
  190. }
  191.  
  192. #batch-results-panel .rp-jump-btn:hover {
  193. background-color: #5ba7f4;
  194. color: #ffffff;
  195. border-color: #5ba7f4;
  196. }
  197.  
  198. #batch-results-panel .dw-jump-btn {
  199. style='width: 0px;
  200. background-color: #ffffff;
  201. border: 0px;
  202. line-height: 12px;
  203. margin-top: -3px;
  204. font-size: 18px;
  205. padding: 0px;
  206. cursor: pointer;'
  207. transition: font-size .2s;
  208. }
  209.  
  210. #batch-results-panel .dw-jump-btn:hover {
  211. font-size: 20px;
  212. }
  213. `;
  214.  
  215. // —— 状态 ——
  216. let results = {};
  217. let maxPages = DEFAULT_MAX_PAGES;
  218. let keywords = [];
  219. //tag-wrapper-2 初始化数据
  220. let tagAsins = []
  221. const maxTags = 3
  222.  
  223. // —— 注入样式 & UI ——
  224. const styleEl = document.createElement('style');
  225. styleEl.textContent = STYLE;
  226. document.head.appendChild(styleEl);
  227.  
  228. // container框
  229. const container = document.createElement('div');
  230. container.id = 'tm-asin-container';
  231. // tag-wrapper-1
  232. const tagWrapper = document.createElement('div');
  233. tagWrapper.id = 'tag-wrapper'
  234. container.insertBefore(tagWrapper, container.firstChild);
  235. // Max🔎Pages
  236. const maxPageText = document.createElement('span');
  237. maxPageText.textContent = 'Max🔎Pages:';
  238. // maxpage input
  239. const inputPages = document.createElement('input');
  240. inputPages.type = 'number';
  241. inputPages.min = '1';
  242. inputPages.max = '9'
  243. inputPages.value = DEFAULT_MAX_PAGES;
  244. inputPages.style.width = '60px';
  245. // search button
  246. const btnSearch = document.createElement('button');
  247. btnSearch.textContent = '搜索排名';
  248. // clear storage
  249. const btnClearCache = document.createElement('button');
  250. btnClearCache.textContent = '清除缓存';
  251. // upload button
  252. const fileInput = document.createElement('input');
  253. fileInput.type = 'file';
  254. fileInput.accept = '.xlsx,.xls';
  255. fileInput.style.display = 'none';
  256. // status的div的元素
  257. const status = document.createElement('span');
  258. status.setAttribute("id", "tm-status");
  259. status.textContent = '请填写 ASIN, 点击"搜索排名"';
  260. // 创建一个 ElementUI 风格的标签按钮
  261. const uploadLabel = document.createElement('label');
  262. uploadLabel.className = 'el-button el-button--primary';
  263. uploadLabel.textContent = '⬆上传关键词';
  264. uploadLabel.appendChild(fileInput); // 把 fileInput 内嵌到 label
  265. // 批量搜索按钮
  266. const batchSearchBtn = document.createElement('button');
  267. batchSearchBtn.className = 'el-button el-button--primary';
  268. batchSearchBtn.textContent = '批量搜索🔍';
  269. // 下载按钮
  270. const downloadBtn = document.createElement('button');
  271. downloadBtn.className = 'el-button el-button--primary';
  272. downloadBtn.textContent = '下载结果表';
  273. /* 动画过渡——container栏的伸缩 */
  274. container.style.transition = 'top 0.4s ease';
  275. let ticking = false;
  276. let lastScrollY = window.scrollY;
  277. window.addEventListener('scroll', e => {
  278. if (!ticking) {
  279. window.requestAnimationFrame(() => {
  280. container.style.top = window.scrollY > lastScrollY ? '0' : '55px';
  281. lastScrollY = window.scrollY;
  282. ticking = false;
  283. });
  284. ticking = true;
  285. }
  286. }, { passive: true });
  287.  
  288. // —— 初始化时尝试读取缓存 ——
  289. const storedTags = sessionStorage.getItem('tm_tagAsins');
  290. if (storedTags) {
  291. try {
  292. tagAsins = JSON.parse(storedTags);
  293. } catch { }
  294. }
  295. const storedResults = sessionStorage.getItem('tm_results');
  296. if (storedResults) {
  297. try {
  298. results = JSON.parse(storedResults);
  299. } catch { }
  300. }
  301. batchSearchBtn.disabled = true
  302. const keywordResult = sessionStorage.getItem('tm_keywords');
  303. if (keywordResult) {
  304. batchSearchBtn.disabled = false
  305. console.log(`已有缓存keywords 可以直接批量搜索`);
  306. try {
  307. keywords = JSON.parse(keywordResult);
  308. } catch { }
  309. }
  310. const storedBatch = sessionStorage.getItem('tm_batch_table');
  311. if (storedBatch) {
  312. try {
  313. const table = JSON.parse(storedBatch);
  314. renderResultsPanelFromTable(table);
  315. } catch { }
  316. }
  317. // tag-wrapper-3 渲染
  318. function renderTags() {
  319. tagWrapper.innerHTML = '';
  320. // 渲染每个 tag
  321. tagAsins.forEach((tag, idx) => {
  322. const span = document.createElement('span');
  323. span.className = 'tag-item';
  324. span.textContent = tag;
  325. // close按钮
  326. const close = document.createElement('i');
  327. close.className = 'tag-close';
  328. close.textContent = '×';
  329. close.addEventListener('click', () => {
  330. tagAsins.splice(idx, 1);
  331. renderTags();
  332. // 可在此触发“close”事件回调
  333. });
  334. span.appendChild(close);
  335. tagWrapper.appendChild(span);
  336. });
  337. // 渲染"+ New Asin"按钮
  338. const btnAdd = document.createElement('button');
  339. btnAdd.className = 'tag-add-btn';
  340. btnAdd.textContent = '+ New Asin';
  341. btnAdd.addEventListener('click', showInput);
  342. tagWrapper.appendChild(btnAdd);
  343. }
  344.  
  345. // tag-wrapper-4 显示输入框新增
  346. function showInput() {
  347. // 如果已经有输入框,直接聚焦
  348. const existingInput = tagWrapper.querySelector('input.tag-input');
  349. if (existingInput) {
  350. existingInput.focus();
  351. return;
  352. }
  353.  
  354. const input = document.createElement('input');
  355. input.className = 'tag-input';
  356. input.placeholder = 'Enter ASIN';
  357. // Asin检验格式
  358. const asinRegex = /^B0[A-Z0-9]{8}$/;
  359. // 插到按钮前
  360. tagWrapper.insertBefore(input, tagWrapper.querySelector('.tag-add-btn'));
  361. input.focus();
  362.  
  363. // 只保留一个 confirmInput,接收事件对象
  364. const confirmInput = (e) => {
  365. const v = input.value.trim().replace(/,$/, '');
  366.  
  367. // —— 1. 如果是 blur 触发,只处理“空值移除”或“合法新值添加”
  368. if (e.type === 'blur') {
  369. if (!v) {
  370. input.remove();
  371. renderTags();
  372. } else if (!tagAsins.includes(v) && tagAsins.length < maxTags && /^B0[A-Z0-9]{8}$/.test(v)) {
  373. tagAsins.push(v);
  374. input.remove();
  375. renderTags();
  376. }
  377. // 其它情况(重复/不合法/超限)都不 alert,也不移除,让用户继续改
  378. return;
  379. }
  380.  
  381. // —— 2. 如果是 keydown 且回车,做完整校验
  382. if (e.type === 'keydown' && e.key === 'Enter') {
  383. e.preventDefault();
  384.  
  385. // 空值 —— 直接移除
  386. if (!v) {
  387. input.remove();
  388. renderTags();
  389. return;
  390. }
  391. // 格式不对
  392. if (!/^B0[A-Z0-9]{8}$/.test(v)) {
  393. input.classList.add('input-error');
  394. alert(`ASIN "${v}" 格式不正确!`);
  395. input.focus();
  396. return;
  397. }
  398. // 重复
  399. if (tagAsins.includes(v)) {
  400. input.classList.add('input-error');
  401. alert(`ASIN "${v}" 已存在!`);
  402. input.focus();
  403. return;
  404. }
  405. // 超限
  406. if (tagAsins.length >= maxTags) {
  407. input.classList.add('input-error');
  408. alert(`最多只能添加 ${maxTags} ASIN!`);
  409. input.focus();
  410. return;
  411. }
  412. // 全部通过——添加并移除
  413. tagAsins.push(v);
  414. input.remove();
  415. renderTags();
  416. }
  417. }
  418.  
  419. input.addEventListener('keydown', confirmInput);
  420. input.addEventListener('blur', confirmInput);
  421. }
  422.  
  423. // tag-wrapper-5 初次渲染
  424. renderTags();
  425. // 如果有旧的 results 就直接渲染面板
  426. if (Object.keys(results).length) {
  427. renderResultsPanel(results);
  428. }
  429.  
  430. [maxPageText, inputPages, btnSearch, btnClearCache, status, uploadLabel, batchSearchBtn, downloadBtn].forEach(el => container.appendChild(el));
  431. document.body.appendChild(container);
  432.  
  433. // —— 状态更新 ——
  434. const updateStatus = txt => { status.textContent = txt; };
  435.  
  436. // —— 主搜索逻辑 ——
  437. btnSearch.addEventListener('click', async () => {
  438. // 搜索前清楚存储
  439. sessionStorage.removeItem('tm_results');
  440. // search-1 参数
  441. maxPages = parseInt(inputPages.value, 10) || DEFAULT_MAX_PAGES;
  442. if (!tagAsins.length) return alert('请先添加至少一个 ASIN!');
  443. // search-2 初始化结果存储
  444. results = {};
  445. tagAsins.forEach(a => results[a] = { found: false });
  446.  
  447. // search-3 删除原有 page 参数
  448. const baseUrl = new URL(location.href);
  449. baseUrl.searchParams.delete('page');
  450.  
  451. // search-4 顺序翻页
  452. updateStatus(`🔎 开始搜索 ${tagAsins.length}个 ASIN,最多 ${maxPages} 页......`);
  453. for (let page = 1; page <= maxPages; page++) {
  454. updateStatus(`🔎 正在搜索第 ${page} 页…`);
  455. const url = new URL(baseUrl);
  456. // 重新设置 page 参数
  457. url.searchParams.set('page', page);
  458.  
  459. // search-4.1 拉取解析HTML
  460. // fetch获取html字符串 DOMParser转成document对象 再搜索
  461. let doc
  462. try {
  463. // 表示跨域请求时会带上 cookie(登录态)
  464. const resp = await fetch(url.href, { credentials: 'include' });
  465. const html = await resp.text();
  466. doc = new DOMParser().parseFromString(html, 'text/html');
  467. } catch (e) {
  468. console.error(e);
  469. updateStatus('❌ 网络请求出错,请重试');
  470. return;
  471. }
  472. // search-4.2 扫描当前页 统计所有未找到的 ASIN
  473. const items = doc.querySelectorAll('div[data-asin]')
  474. let nat = 0, sp = 0;
  475. for (const node of items) {
  476. // 带有购物车按钮的才算有效位置
  477. if (!node.querySelector('button.a-button-text, a.a-button-text')) continue;
  478. const asin = node.getAttribute('data-asin');
  479. // 广告位
  480. const isAd = !!node.querySelector('a.puis-label-popover.puis-sponsored-label-text');
  481. if (isAd) {
  482. sp++;
  483. continue;
  484. }
  485. nat++;
  486. // 如果ASIN在tagAsins列表里&切还未找到&不是广告位
  487. if (tagAsins.includes(asin) && !results[asin].found && !isAd) {
  488. results[asin] = {
  489. found: true,
  490. page,
  491. position: isAd ? sp : nat,
  492. isAd
  493. };
  494. }
  495. }
  496. // search-4.3 如果所有 ASIN 都已找到,则提前退出翻页
  497. const unfinished = tagAsins.filter(asin => !results[asin].found);
  498. if (unfinished.length === 0) {
  499. updateStatus(`✅ 全部 ASIN ${page} 页内找到`);
  500. break;
  501. }
  502. }
  503. // search-5 更新最终状态 & (可选) 渲染结果
  504. const notFound = tagAsins.filter(asin => !results[asin].found);
  505. if (notFound.length) {
  506. updateStatus(`❌ 未找到:${notFound.join(', ')}`);
  507. } else {
  508. updateStatus(`✅ 全部 ASIN 已定位`);
  509. }
  510. // session存储
  511. sessionStorage.setItem('tm_tagAsins', JSON.stringify(tagAsins));
  512. sessionStorage.setItem('tm_results', JSON.stringify(results));
  513. renderResultsPanel(results)
  514. });
  515.  
  516. // —— 搜索结果面板 ——
  517. function renderResultsPanel(results) {
  518. let panel = document.getElementById('results-panel');
  519. if (!panel) {
  520. panel = document.createElement('div');
  521. panel.id = 'results-panel';
  522. Object.assign(panel.style, {
  523. position: 'fixed',
  524. top: '100px', // 根据你的 tm-asin-container 高度适当调整
  525. left: '5px',
  526. background: 'rgba(255,255,255,0.95)',
  527. border: '1px solid #ddd',
  528. borderRadius: '4px',
  529. padding: '5px',
  530. boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
  531. zIndex: '9999',
  532. fontSize: '14px',
  533. width: '290px',
  534. lineHeight: '1.4'
  535. });
  536. document.body.appendChild(panel);
  537. }
  538. // 1. 构造 HTML:带一个 drag-handle 和每行的“跳转”按钮
  539. const lines = Object.entries(results).map(([asin, r]) => {
  540. let text;
  541. if (r.found) {
  542. const totalRank = (r.page - 1) * 48 + r.position;
  543. text = `第${r.page}页${r.position}位-排名${totalRank}`;
  544. } else {
  545. text = `<span style="color:#f56c6c;">❌未找到</span>`;
  546. }
  547. // 如果找到了,就显示一个按钮,点击后跳转到该 ASIN 所在页
  548. const btn = r.found
  549. ? `
  550. <button class="rp-jump-btn" data-page="${r.page}"
  551. style="
  552. margin-top: 2px;
  553. margin-left: 6px;
  554. margin-bottom: 3.5px;
  555. line-height: 12px;
  556. color: #5ba7f4;
  557. background-color: #ecf5ff;
  558. border: 1px solid;
  559. border-radius: 5px;
  560. padding-top: 2px;
  561. cursor: pointer;"
  562. onmouseover="this.style.backgroundColor='#5ba7f4'; this.style.color='#ffffff'; this.style.borderColor='#5ba7f4';"
  563. onmouseout="this.style.backgroundColor='#ecf5ff'; this.style.color='#5ba7f4'; this.style.borderColor='initial';"
  564. >➡</button>
  565. <button class="dw-jump-btn" data-asin="${asin}"
  566. style='width: 0px;
  567. background-color: #ffffff;
  568. border: 0px;
  569. line-height: 12px;
  570. margin-top: -3px;
  571. font-size: 18px;
  572. padding: 0px;
  573. cursor: pointer;'
  574. onmouseover="this.style.fontSize='20px';"
  575. onmouseout="this.style.fontSize='18px';"
  576. >📍</button>`
  577. : '';
  578. return `<li style="margin-top:4px;list-style:none;">
  579. <strong>${asin}</strong>:${text}${btn}
  580. </li>`;
  581. }).join('');
  582.  
  583. panel.innerHTML = `
  584. <div class="drag-handle" style="
  585. cursor: move;
  586. background:#f5f5f5;
  587. padding:6px;
  588. border-bottom:1px solid #ddd;
  589. font-weight:500;
  590. font-size: 16px;
  591. ">查询结果</div>
  592. <ul style="padding:4px;margin:0;">${lines}</ul>
  593. `;
  594.  
  595. // 2. 拖拽:只绑定到 .drag-handle
  596. const handle = panel.querySelector('.drag-handle');
  597. handle.onmousedown = e => {
  598. const rect = panel.getBoundingClientRect();
  599. const shiftX = e.clientX - rect.left;
  600. const shiftY = e.clientY - rect.top;
  601. function onMouseMove(e) {
  602. panel.style.left = (e.clientX - shiftX) + 'px';
  603. panel.style.top = (e.clientY - shiftY) + 'px';
  604. }
  605. document.addEventListener('mousemove', onMouseMove);
  606. document.onmouseup = () => {
  607. document.removeEventListener('mousemove', onMouseMove);
  608. document.onmouseup = null;
  609. };
  610. e.preventDefault();
  611. };
  612. handle.ondragstart = () => false;
  613.  
  614. // 3. 点击 “跳转” 按钮 的事件委托
  615. panel.onclick = e => {
  616. if (e.target.matches('.rp-jump-btn')) {
  617. const page = parseInt(e.target.dataset.page, 10);
  618. // 当前页page
  619. const currentUrl = new URL(location.href);
  620. const currentPage = parseInt(currentUrl.searchParams.get('page'), 10) || 1;
  621.  
  622. // 如果要跳转的页就是当前页
  623. if (page === currentPage) {
  624. console.log(`Already on page ${page}, no navigation.`);
  625. return;
  626. }
  627. // 跳转
  628. const gotoUrl = currentUrl;
  629. gotoUrl.searchParams.delete('page');
  630. if (page > 1) gotoUrl.searchParams.set('page', page);
  631. location.href = gotoUrl.href;
  632. }
  633. if (e.target.matches('.dw-jump-btn')) {
  634. // 需要高亮的asin
  635. const highLightAsin = e.target.dataset.asin;
  636. // 只在 Amazon 搜索结果区查找&非广告位
  637. const candidates = document.querySelectorAll(`.s-main-slot > [data-asin="${highLightAsin}"]`);
  638. const elem = Array.from(candidates).find(node =>
  639. !node.querySelector('a.puis-label-popover.puis-sponsored-label-text')
  640. );
  641. if (!elem) {
  642. return alert(`未能在当前页面找到ASIN-${highLightAsin}, 点击前方页面跳转按钮`);
  643. }
  644. // 4. 高亮 & 滚动到视图
  645. elem.style.border = '2px solid red';
  646. elem.style.padding = '5px';
  647. elem.scrollIntoView({ behavior: 'smooth', block: 'center' });
  648. }
  649. }
  650. };
  651.  
  652. // —— 缓存清除 ——
  653. btnClearCache.addEventListener('click', () => {
  654. sessionStorage.removeItem('tm_tagAsins');
  655. sessionStorage.removeItem('tm_results');
  656. sessionStorage.removeItem('tm_keywords');
  657. sessionStorage.removeItem('tm_batch_table');
  658.  
  659. // 请填写 ASIN, 点击"搜索排名
  660. updateStatus('请填写 ASIN, 点击"搜索排名')
  661.  
  662. tagAsins = [];
  663. results = {};
  664. keywords = [];
  665.  
  666. renderTags();
  667. renderResultsPanelFromTable()
  668.  
  669. const panel = document.getElementById('results-panel');
  670. if (panel) panel.remove();
  671.  
  672. const batchPanel = document.getElementById('batch-results-panel');
  673. if (batchPanel) batchPanel.remove();
  674. });
  675.  
  676. // 动态加载 SheetJS(xlsx.full.min.js),确保全局有 XLSX
  677. async function loadSheetJSLib() {
  678. return new Promise((resolve, reject) => {
  679. if (window.XLSX) return resolve();
  680. const s = document.createElement('script');
  681. s.src = 'https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js';
  682. s.onload = () => resolve();
  683. s.onerror = () => reject(new Error('加载 XLSX 库失败'));
  684. document.head.appendChild(s);
  685. });
  686. }
  687.  
  688. // 然后在设置 fileInput 监听之前,先调用它
  689. await loadSheetJSLib();
  690.  
  691. // —— excel文件解析 ——
  692. fileInput.addEventListener('change', async e => {
  693. const file = e.target.files[0];
  694. if (!file) alert('未选择文件');
  695.  
  696. // 校验文件大小:不超过 1MB
  697. const maxSize = 1 * 1024 * 1024; // 1MB
  698. if (file.size > maxSize) {
  699. alert('Excel 文件不能大于 1MB,请选择更小的文件。');
  700. fileInput.value = ''; // 清空选中文件
  701. return;
  702. }
  703.  
  704. // 读取并解析
  705. const data = await file.arrayBuffer();
  706. const wb = XLSX.read(data, { type: 'array' });
  707. const sheet = wb.Sheets[wb.SheetNames[0]];
  708. const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 });
  709.  
  710. // 只取每行第一列,过滤空值并 trim
  711. keywords = rows
  712. .map(row => row[0])
  713. .filter(cell => typeof cell === 'string' && cell.trim().length > 0)
  714. .map(cell => cell.trim());
  715.  
  716. sessionStorage.setItem('tm_keywords', JSON.stringify(keywords));
  717. batchSearchBtn.disabled = false;
  718. alert(`已导入并缓存 ${keywords.length} 条关键词`);
  719. console.log('keywords keywords keywords', keywords);
  720. });
  721.  
  722. // 工具-睡眠和随机数
  723. function sleep(ms) {
  724. return new Promise(resolve => setTimeout(resolve, ms));
  725. }
  726. function randomBetween(min, max) {
  727. return Math.floor(Math.random() * (max - min + 1)) + min;
  728. }
  729.  
  730. // 包装后的 fetch 函数,包含随机延迟 & 错误退避
  731. async function fetchAsinWithDelay(keyword, asin, maxPages) {
  732. // 在真正请求之前,先等待 1-3 秒(随机)
  733. await sleep(randomBetween(3000, 5000));
  734.  
  735. try {
  736. return await fetchAsinPosition(keyword, asin, maxPages);
  737. } catch (err) {
  738. console.warn(`Request failed for ${keyword} / ${asin}:`, err);
  739. // 碰到错误(网络、429等),退避 30–60 秒再重试一次
  740. await sleep(randomBetween(30000, 60000));
  741. return fetchAsinPosition(keyword, asin, maxPages);
  742. }
  743. }
  744.  
  745. // 批量搜索fetch函数
  746. async function fetchAsinPosition(keyword, asin, maxPages) {
  747. const base = new URL(location.href);
  748. base.searchParams.set('k', keyword);
  749. base.searchParams.delete('page');
  750.  
  751. for (let page = 1; page <= maxPages; page++) {
  752. base.searchParams.set('page', page);
  753. const html = await fetch(base.href, { credentials: 'include' })
  754. .then(r => r.text());
  755. const doc = new DOMParser().parseFromString(html, 'text/html');
  756. let nat = 0;
  757. for (const node of doc.querySelectorAll('div[data-asin]')) {
  758. if (!node.querySelector('button.a-button-text, a.a-button-text')) continue;
  759. if (node.querySelector('.puis-sponsored-label-text')) continue;
  760. // 只加自然位
  761. nat++;
  762. if (node.getAttribute('data-asin') === asin) {
  763. return { page, position: nat };
  764. }
  765. }
  766. }
  767. // 确保不返回undefined
  768. return { page: null, position: null };
  769. }
  770.  
  771. // 渲染批量结果面板
  772. function renderResultsPanelFromTable(table) {
  773. let panel = document.getElementById('batch-results-panel');
  774. if (!panel) {
  775. panel = document.createElement('div');
  776. panel.id = 'batch-results-panel';
  777. Object.assign(panel.style, {
  778. position: 'fixed',
  779. top: '100px',
  780. left: '10px',
  781. background: 'rgba(255,255,255,0.95)',
  782. border: '1px solid #ddd',
  783. borderRadius: '4px',
  784. boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
  785. zIndex: '9999',
  786. width: '320px',
  787. fontSize: '14px',
  788. lineHeight: '1.4',
  789. overflow: 'hidden'
  790. });
  791. document.body.appendChild(panel);
  792.  
  793. const header = document.createElement('div');
  794. header.id = 'results-header';
  795. header.textContent = '查询结果';
  796. Object.assign(header.style, {
  797. cursor: 'move',
  798. background: '#f5f5f5',
  799. padding: '6px 8px',
  800. borderBottom: '1px solid #ddd',
  801. fontWeight: '600',
  802. fontSize: '16px'
  803. });
  804. panel.appendChild(header);
  805.  
  806. header.addEventListener('mousedown', e => {
  807. const rect = panel.getBoundingClientRect();
  808. const dx = e.clientX - rect.left;
  809. const dy = e.clientY - rect.top;
  810. function mm(ev) {
  811. panel.style.left = ev.clientX - dx + 'px';
  812. panel.style.top = ev.clientY - dy + 'px';
  813. }
  814. document.addEventListener('mousemove', mm);
  815. document.addEventListener('mouseup', () => {
  816. document.removeEventListener('mousemove', mm);
  817. }, { once: true });
  818. e.preventDefault();
  819. });
  820. } else {
  821. panel.innerHTML = '';
  822. const header = document.createElement('div');
  823. header.id = 'batch-results-header';
  824. header.textContent = '查询结果';
  825. Object.assign(header.style, {
  826. cursor: 'move',
  827. background: '#f5f5f5',
  828. padding: '6px 8px',
  829. borderBottom: '1px solid #ddd',
  830. fontWeight: '600',
  831. fontSize: '16px'
  832. });
  833. panel.appendChild(header);
  834. header.addEventListener('mousedown', e => {
  835. const rect = panel.getBoundingClientRect();
  836. const dx = e.clientX - rect.left;
  837. const dy = e.clientY - rect.top;
  838. function mm(ev) {
  839. panel.style.left = ev.clientX - dx + 'px';
  840. panel.style.top = ev.clientY - dy + 'px';
  841. }
  842. document.addEventListener('mousemove', mm);
  843. document.addEventListener('mouseup', () => {
  844. document.removeEventListener('mousemove', mm);
  845. }, { once: true });
  846. e.preventDefault();
  847. });
  848. }
  849.  
  850. const ul = document.createElement('ul');
  851. ul.style.listStyle = 'none';
  852. ul.style.padding = '8px';
  853. ul.style.margin = '0';
  854.  
  855. table.forEach(({ keyword, asin, page, position }) => {
  856. const li = document.createElement('li');
  857. li.style.marginBottom = '6px';
  858.  
  859. const text = document.createElement('span');
  860. const totalRank = (page - 1) * 48 + position;
  861. text.innerHTML = `<strong>${keyword}</strong> | ASIN: ${asin} | ` +
  862. (page ? `第${page}页 ${position}位 总排名${totalRank}` : `<span style="color:#f56c6c;">未找到</span>`);
  863. li.appendChild(text);
  864.  
  865. if (page) {
  866. const btnJump = document.createElement('button');
  867. btnJump.className = 'rp-jump-btn';
  868. btnJump.dataset.page = page;
  869. btnJump.dataset.keyword = keyword
  870. btnJump.textContent = '➡';
  871. btnJump.style.marginLeft = '8px';
  872. li.appendChild(btnJump);
  873.  
  874. const btnLoc = document.createElement('button');
  875. btnLoc.className = 'dw-jump-btn';
  876. btnLoc.dataset.asin = asin;
  877. btnLoc.textContent = '📍';
  878. btnLoc.style.marginLeft = '4px';
  879. li.appendChild(btnLoc);
  880. }
  881.  
  882. ul.appendChild(li);
  883. });
  884.  
  885. panel.appendChild(ul);
  886.  
  887. panel.onclick = e => {
  888. const jump = e.target.closest('.rp-jump-btn');
  889. if (jump) {
  890. const page = +jump.dataset.page;
  891. const keyword = jump.dataset.keyword;
  892. // 构造新的搜索 URL:带上 k=keyword 和 page=page(page>1 时)
  893. const url = new URL(window.location.origin + '/s');
  894. url.searchParams.set('k', keyword);
  895. if (page > 1) url.searchParams.set('page', page);
  896. location.href = url.href;
  897. return;
  898. }
  899. const loc = e.target.closest('.dw-jump-btn');
  900. if (loc) {
  901. const a = loc.dataset.asin;
  902. const nodes = Array.from(document.querySelectorAll(`.s-main-slot > [data-asin="${a}"]`));
  903. const el = nodes.find(n => !n.querySelector('.s-sponsored-label, .puis-sponsored-label-text'));
  904. if (el) {
  905. el.style.border = '2px solid red';
  906. el.style.padding = '5px';
  907. el.scrollIntoView({ behavior: 'smooth', block: 'center' });
  908. } else {
  909. alert(`当前页未找到 ASIN${a}`);
  910. }
  911. }
  912. };
  913. }
  914.  
  915. // excel导出函数
  916. async function exportToExcel(data) {
  917. // data: [ { keyword, asin ,page, position }, … ]
  918. await loadSheetJSLib();
  919.  
  920. // 2. 预处理数据:page/position 为空替换为 "-"
  921. const processed = data.map(({ asin, keyword, page, position, totalRank }) => ({
  922. asin,
  923. keyword,
  924. page: page == null ? "-" : page,
  925. position: position == null ? "-" : position,
  926. totalRank: totalRank == null ? "-" : totalRank
  927. }));
  928.  
  929. // 把 JSON 转为工作表
  930. const ws = XLSX.utils.json_to_sheet(data, {
  931. header: ['关键词', 'ASIN', '页数', '位置', '总排名']
  932. });
  933.  
  934. // 4. 给表头加粗且居中
  935. const range = XLSX.utils.decode_range(ws['!ref']);
  936. for (let C = range.s.c; C <= range.e.c; ++C) {
  937. const cellAddress = XLSX.utils.encode_cell({ r: 0, c: C });
  938. const cell = ws[cellAddress];
  939. if (!cell) continue;
  940. cell.s = {
  941. font: { bold: true },
  942. alignment: { horizontal: 'center' }
  943. };
  944. }
  945.  
  946. // 新建工作簿并追加工作表
  947. const wb = XLSX.utils.book_new();
  948. XLSX.utils.book_append_sheet(wb, ws, '排名结果');
  949.  
  950. // 6. 生成文件名:YYYY/M/D-站点-AsinKwRank
  951. const host = window.location.host;
  952. const siteMap = {
  953. 'www.amazon.com': 'US',
  954. 'www.amazon.co.uk': 'UK',
  955. 'www.amazon.ca': 'CA',
  956. 'www.amazon.de': 'DE',
  957. 'www.amazon.fr': 'FR',
  958. 'www.amazon.es': 'ES',
  959. 'www.amazon.it': 'IT'
  960. };
  961. const siteCode = siteMap[host] || host;
  962. const now = new Date();
  963. const fileName = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}-${siteCode}-AsinKwRank.xlsx`;
  964.  
  965. // 生成二进制数组
  966. const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
  967.  
  968. // 创建 Blob 并触发下载
  969. const blob = new Blob([wbout], { type: 'application/octet-stream' });
  970. const url = URL.createObjectURL(blob);
  971. const a = document.createElement('a');
  972. a.href = url;
  973. a.download = 'asin_keyword_rankings.xlsx';
  974. document.body.appendChild(a);
  975. a.click();
  976. document.body.removeChild(a);
  977. URL.revokeObjectURL(url);
  978. }
  979.  
  980. // —— 批量搜索按钮 ——
  981. batchSearchBtn.addEventListener('click', async () => {
  982. if (!keywords.length) {
  983. return alert('请先导入关键词文件');
  984. }
  985. if (!tagAsins.length) {
  986. return alert('请先添加至少一个 ASIN');
  987. }
  988. const tasks = [];
  989. for (const keyword of keywords) {
  990. for (const asin of tagAsins) {
  991. // {'winter gloves men', B0xxxxxxxx}
  992. tasks.push({ keyword, asin });
  993. }
  994. }
  995. // 循环执行,每次查询一个"关键词 × ASIN"
  996. const table = [];
  997. updateStatus(`🔎 开始批量查询:${tasks.length} 次`);
  998. for (const { keyword, asin } of tasks) {
  999. updateStatus(`🔎 查询 "${keyword}" ASIN-${asin}`);
  1000. // fetchAsinWithDelay 返回 { page, position }
  1001. const { page, position } = await fetchAsinWithDelay(keyword, asin, maxPages);
  1002. const totalRank = (page - 1) * 48 + position;
  1003. table.push({ keyword, asin, page, position, totalRank });
  1004. }
  1005. console.log('结果table 结果table 结果table', table);
  1006. sessionStorage.setItem('tm_batch_table', JSON.stringify(table));
  1007. sessionStorage.setItem('tm_tagAsins', JSON.stringify(tagAsins));
  1008. alert('搜索完成,共 ' + table.length + ' 条记录');
  1009. renderResultsPanelFromTable(table);
  1010. })
  1011.  
  1012. // 点击时,从 sessionStorage 取出缓存的 table,并调用 exportToExcel
  1013. downloadBtn.addEventListener('click', async () => {
  1014. const raw = sessionStorage.getItem('tm_batch_table');
  1015. if (!raw) {
  1016. return alert('当前没有可下载的查询结果,请先执行批量搜索。');
  1017. }
  1018. let table;
  1019. try {
  1020. table = JSON.parse(raw);
  1021. } catch {
  1022. return alert('结果数据解析失败。');
  1023. }
  1024. // 调用之前定义的导出函数
  1025. await exportToExcel(table);
  1026. });
  1027. })();