- // ==UserScript==
- // @name 亚马逊关键词排名-Amazon keywords Positioning by Asin
- // @namespace http://tampermonkey.net/
- // @version 3.5.0
- // @description 1.在亚马逊搜索结果页上定位ASIN, 获取排名 2.代码重构————dom操作->fetch+DOMParser 3.结果面板 4.批量导入excel关键词表,返回关键词排名表.xlsx
- // @author You
- // @match https://www.amazon.com/*
- // @match https://www.amazon.co.uk/*
- // @match https://www.amazon.ca/*
- // @icon https://www.amazon.com/favicon.ico
- // @license MIT
- // @grant none
- // ==/UserScript==
-
-
- (async function () {
- 'use strict';
-
- // —— 配置区 ——
- const DEFAULT_MAX_PAGES = 2; // 默认最多搜索页数
- const STYLE = `
- /* 容器 */
- #tm-asin-container {
- position: fixed;
- top: 60px;
- left: 0; right: 0;
- padding: 6px 12px;
- background: #fff;
- box-shadow: 0 2px 12px rgba(0,0,0,0.1);
- font-family: "Helvetica Neue", Arial, sans-serif;
- z-index: 9999;
- display: flex;
- align-items: center;
- }
-
- /* tag-wrapper-css */
- #tm-asin-container #tag-wrapper {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 8px;
- margin-right: 6px;
- }
-
- .tag-item {
- display: inline-flex;
- align-items: center;
- height: 28px;
- padding: 0 8px;
- font-size: 14px;
- background: #ecf5ff;
- color: #409eff;
- border: 1px solid #b3d8ff;
- border-radius: 4px;
- }
-
- .tag-item .tag-close {
- display: inline-block;
- margin-left: 4px;
- font-style: normal;
- cursor: pointer;
- color: #409eff;
- font-weight: bold;
- }
-
- .tag-item .tag-close:hover {
- color: #66b1ff;
- }
-
- .tag-add-btn {
- display: inline-flex;
- align-items: center;
- height: 32px;
- padding: 0 12px;
- font-size: 14px;
- color: #409eff;
- background: #fff;
- border: 1px solid #409eff;
- border-radius: 4px;
- cursor: pointer;
- transition: background-color .2s;
- }
-
- .tag-add-btn:hover {
- background-color: #ecf5ff;
- }
-
- /* 临时输入框 */
- .tag-input {
- flex: 1;
- min-width: 100px;
- height: 28px;
- padding: 0 6px;
- font-size: 14px;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- outline: none;
- }
- /* input错误提示 */
- .input-error {
- border-color: red;
- outline: none;
- box-shadow: 0 0 5px red;
- }
-
- /* ASIN 和页数输入框 */
- #tm-asin-container input[type="number"] {
- margin-right: 14px;
- font-size: 16px;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- color: #606266;
- outline: none;
- transition: border-color .2s, box-shadow .2s;
- width: 200px;
- box-sizing: border-box;
- }
- #tm-asin-container input:focus {
- border-color: #409eff;
- box-shadow: 0 0 2px rgba(64,158,255,0.2);
- }
-
- /* 文件上传按钮 追加 ElementUI Button 样式 */
- .el-button {
- display: inline-block;
- line-height: 1.5;
- white-space: nowrap;
- font-size: 14px;
- font-weight: 500;
- padding: 6px 12px;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- cursor: pointer;
- user-select: none;
- background-color: #fff;
- color: #606266;
- transition: background-color .2s, border-color .2s, color .2s;
- margin-right: 12px;
- }
- .el-button--primary {
- background-color: #409eff;
- border-color: #409eff;
- color: #fff;
- }
- .el-button--primary:hover {
- background-color: #66b1ff;
- border-color: #66b1ff;
- }
- /* 按钮 */
- #tm-asin-container button {
- margin-right: 12px;
- padding: 5px 10px;
- font-size: 14px;
- font-weight: 500;
- color: #fff;
- background-color: #409eff;
- border: 1px solid #409eff;
- border-radius: 4px;
- cursor: pointer;
- transition: background-color .2s, border-color .2s;
- }
- #tm-asin-container button:hover:not([disabled]) {
- background-color: #66b1ff;
- border-color: #66b1ff;
- }
-
- #tm-asin-container span {
- font-size: 16px;
- }
- /* 状态文字:紧跟按钮后面 */
- #tm-asin-container span#tm-status {
- margin-left: 12px;
- margin-right: 12px;
- font-size: 16px;
- color:rgb(110, 111, 111);
- }
- /* 面板button样式 */
- #batch-results-panel .rp-jump-btn {
- margin-top: 2px;
- margin-left: 6px;
- margin-bottom: 3.5px;
- line-height: 12px;
- color: #5ba7f4;
- background-color: #ecf5ff;
- border: 1px solid;
- border-radius: 5px;
- padding-top: 2px;
- cursor: pointer;
- transition: background-color .2s, color .2s, border-color .2s;
- }
-
- #batch-results-panel .rp-jump-btn:hover {
- background-color: #5ba7f4;
- color: #ffffff;
- border-color: #5ba7f4;
- }
-
- #batch-results-panel .dw-jump-btn {
- style='width: 0px;
- background-color: #ffffff;
- border: 0px;
- line-height: 12px;
- margin-top: -3px;
- font-size: 18px;
- padding: 0px;
- cursor: pointer;'
- transition: font-size .2s;
- }
-
- #batch-results-panel .dw-jump-btn:hover {
- font-size: 20px;
- }
- `;
-
- // —— 状态 ——
- let results = {};
- let maxPages = DEFAULT_MAX_PAGES;
- let keywords = [];
- //tag-wrapper-2 初始化数据
- let tagAsins = []
- const maxTags = 3
-
- // —— 注入样式 & UI ——
- const styleEl = document.createElement('style');
- styleEl.textContent = STYLE;
- document.head.appendChild(styleEl);
-
- // container框
- const container = document.createElement('div');
- container.id = 'tm-asin-container';
- // tag-wrapper-1
- const tagWrapper = document.createElement('div');
- tagWrapper.id = 'tag-wrapper'
- container.insertBefore(tagWrapper, container.firstChild);
- // Max🔎Pages
- const maxPageText = document.createElement('span');
- maxPageText.textContent = 'Max🔎Pages:';
- // maxpage input
- const inputPages = document.createElement('input');
- inputPages.type = 'number';
- inputPages.min = '1';
- inputPages.max = '9'
- inputPages.value = DEFAULT_MAX_PAGES;
- inputPages.style.width = '60px';
- // search button
- const btnSearch = document.createElement('button');
- btnSearch.textContent = '搜索排名';
- // clear storage
- const btnClearCache = document.createElement('button');
- btnClearCache.textContent = '清除缓存';
- // upload button
- const fileInput = document.createElement('input');
- fileInput.type = 'file';
- fileInput.accept = '.xlsx,.xls';
- fileInput.style.display = 'none';
- // status的div的元素
- const status = document.createElement('span');
- status.setAttribute("id", "tm-status");
- status.textContent = '请填写 ASIN, 点击"搜索排名"';
- // 创建一个 ElementUI 风格的标签按钮
- const uploadLabel = document.createElement('label');
- uploadLabel.className = 'el-button el-button--primary';
- uploadLabel.textContent = '⬆上传关键词';
- uploadLabel.appendChild(fileInput); // 把 fileInput 内嵌到 label
- // 批量搜索按钮
- const batchSearchBtn = document.createElement('button');
- batchSearchBtn.className = 'el-button el-button--primary';
- batchSearchBtn.textContent = '批量搜索🔍';
- // 下载按钮
- const downloadBtn = document.createElement('button');
- downloadBtn.className = 'el-button el-button--primary';
- downloadBtn.textContent = '下载结果表';
- /* 动画过渡——container栏的伸缩 */
- container.style.transition = 'top 0.4s ease';
- let ticking = false;
- let lastScrollY = window.scrollY;
- window.addEventListener('scroll', e => {
- if (!ticking) {
- window.requestAnimationFrame(() => {
- container.style.top = window.scrollY > lastScrollY ? '0' : '55px';
- lastScrollY = window.scrollY;
- ticking = false;
- });
- ticking = true;
- }
- }, { passive: true });
-
- // —— 初始化时尝试读取缓存 ——
- const storedTags = sessionStorage.getItem('tm_tagAsins');
- if (storedTags) {
- try {
- tagAsins = JSON.parse(storedTags);
- } catch { }
- }
- const storedResults = sessionStorage.getItem('tm_results');
- if (storedResults) {
- try {
- results = JSON.parse(storedResults);
- } catch { }
- }
- batchSearchBtn.disabled = true
- const keywordResult = sessionStorage.getItem('tm_keywords');
- if (keywordResult) {
- batchSearchBtn.disabled = false
- console.log(`已有缓存keywords 可以直接批量搜索`);
- try {
- keywords = JSON.parse(keywordResult);
- } catch { }
- }
- const storedBatch = sessionStorage.getItem('tm_batch_table');
- if (storedBatch) {
- try {
- const table = JSON.parse(storedBatch);
- renderResultsPanelFromTable(table);
- } catch { }
- }
- // tag-wrapper-3 渲染
- function renderTags() {
- tagWrapper.innerHTML = '';
- // 渲染每个 tag
- tagAsins.forEach((tag, idx) => {
- const span = document.createElement('span');
- span.className = 'tag-item';
- span.textContent = tag;
- // close按钮
- const close = document.createElement('i');
- close.className = 'tag-close';
- close.textContent = '×';
- close.addEventListener('click', () => {
- tagAsins.splice(idx, 1);
- renderTags();
- // 可在此触发“close”事件回调
- });
- span.appendChild(close);
- tagWrapper.appendChild(span);
- });
- // 渲染"+ New Asin"按钮
- const btnAdd = document.createElement('button');
- btnAdd.className = 'tag-add-btn';
- btnAdd.textContent = '+ New Asin';
- btnAdd.addEventListener('click', showInput);
- tagWrapper.appendChild(btnAdd);
- }
-
- // tag-wrapper-4 显示输入框新增
- function showInput() {
- // 如果已经有输入框,直接聚焦
- const existingInput = tagWrapper.querySelector('input.tag-input');
- if (existingInput) {
- existingInput.focus();
- return;
- }
-
- const input = document.createElement('input');
- input.className = 'tag-input';
- input.placeholder = 'Enter ASIN';
- // Asin检验格式
- const asinRegex = /^B0[A-Z0-9]{8}$/;
- // 插到按钮前
- tagWrapper.insertBefore(input, tagWrapper.querySelector('.tag-add-btn'));
- input.focus();
-
- // 只保留一个 confirmInput,接收事件对象
- const confirmInput = (e) => {
- const v = input.value.trim().replace(/,$/, '');
-
- // —— 1. 如果是 blur 触发,只处理“空值移除”或“合法新值添加”
- if (e.type === 'blur') {
- if (!v) {
- input.remove();
- renderTags();
- } else if (!tagAsins.includes(v) && tagAsins.length < maxTags && /^B0[A-Z0-9]{8}$/.test(v)) {
- tagAsins.push(v);
- input.remove();
- renderTags();
- }
- // 其它情况(重复/不合法/超限)都不 alert,也不移除,让用户继续改
- return;
- }
-
- // —— 2. 如果是 keydown 且回车,做完整校验
- if (e.type === 'keydown' && e.key === 'Enter') {
- e.preventDefault();
-
- // 空值 —— 直接移除
- if (!v) {
- input.remove();
- renderTags();
- return;
- }
- // 格式不对
- if (!/^B0[A-Z0-9]{8}$/.test(v)) {
- input.classList.add('input-error');
- alert(`ASIN "${v}" 格式不正确!`);
- input.focus();
- return;
- }
- // 重复
- if (tagAsins.includes(v)) {
- input.classList.add('input-error');
- alert(`ASIN "${v}" 已存在!`);
- input.focus();
- return;
- }
- // 超限
- if (tagAsins.length >= maxTags) {
- input.classList.add('input-error');
- alert(`最多只能添加 ${maxTags} 个 ASIN!`);
- input.focus();
- return;
- }
- // 全部通过——添加并移除
- tagAsins.push(v);
- input.remove();
- renderTags();
- }
- }
-
- input.addEventListener('keydown', confirmInput);
- input.addEventListener('blur', confirmInput);
- }
-
- // tag-wrapper-5 初次渲染
- renderTags();
- // 如果有旧的 results 就直接渲染面板
- if (Object.keys(results).length) {
- renderResultsPanel(results);
- }
-
- [maxPageText, inputPages, btnSearch, btnClearCache, status, uploadLabel, batchSearchBtn, downloadBtn].forEach(el => container.appendChild(el));
- document.body.appendChild(container);
-
- // —— 状态更新 ——
- const updateStatus = txt => { status.textContent = txt; };
-
- // —— 主搜索逻辑 ——
- btnSearch.addEventListener('click', async () => {
- // 搜索前清楚存储
- sessionStorage.removeItem('tm_results');
- // search-1 参数
- maxPages = parseInt(inputPages.value, 10) || DEFAULT_MAX_PAGES;
- if (!tagAsins.length) return alert('请先添加至少一个 ASIN!');
- // search-2 初始化结果存储
- results = {};
- tagAsins.forEach(a => results[a] = { found: false });
-
- // search-3 删除原有 page 参数
- const baseUrl = new URL(location.href);
- baseUrl.searchParams.delete('page');
-
- // search-4 顺序翻页
- updateStatus(`🔎 开始搜索 ${tagAsins.length}个 ASIN,最多 ${maxPages} 页......`);
- for (let page = 1; page <= maxPages; page++) {
- updateStatus(`🔎 正在搜索第 ${page} 页…`);
- const url = new URL(baseUrl);
- // 重新设置 page 参数
- url.searchParams.set('page', page);
-
- // search-4.1 拉取解析HTML
- // fetch获取html字符串 DOMParser转成document对象 再搜索
- let doc
- try {
- // 表示跨域请求时会带上 cookie(登录态)
- const resp = await fetch(url.href, { credentials: 'include' });
- const html = await resp.text();
- doc = new DOMParser().parseFromString(html, 'text/html');
- } catch (e) {
- console.error(e);
- updateStatus('❌ 网络请求出错,请重试');
- return;
- }
- // search-4.2 扫描当前页 统计所有未找到的 ASIN
- const items = doc.querySelectorAll('div[data-asin]')
- let nat = 0, sp = 0;
- for (const node of items) {
- // 带有购物车按钮的才算有效位置
- if (!node.querySelector('button.a-button-text, a.a-button-text')) continue;
- const asin = node.getAttribute('data-asin');
- // 广告位
- const isAd = !!node.querySelector('a.puis-label-popover.puis-sponsored-label-text');
- if (isAd) {
- sp++;
- continue;
- }
- nat++;
- // 如果ASIN在tagAsins列表里&切还未找到&不是广告位
- if (tagAsins.includes(asin) && !results[asin].found && !isAd) {
- results[asin] = {
- found: true,
- page,
- position: isAd ? sp : nat,
- isAd
- };
- }
- }
- // search-4.3 如果所有 ASIN 都已找到,则提前退出翻页
- const unfinished = tagAsins.filter(asin => !results[asin].found);
- if (unfinished.length === 0) {
- updateStatus(`✅ 全部 ASIN 在 ${page} 页内找到`);
- break;
- }
- }
- // search-5 更新最终状态 & (可选) 渲染结果
- const notFound = tagAsins.filter(asin => !results[asin].found);
- if (notFound.length) {
- updateStatus(`❌ 未找到:${notFound.join(', ')}`);
- } else {
- updateStatus(`✅ 全部 ASIN 已定位`);
- }
- // session存储
- sessionStorage.setItem('tm_tagAsins', JSON.stringify(tagAsins));
- sessionStorage.setItem('tm_results', JSON.stringify(results));
- renderResultsPanel(results)
- });
-
- // —— 搜索结果面板 ——
- function renderResultsPanel(results) {
- let panel = document.getElementById('results-panel');
- if (!panel) {
- panel = document.createElement('div');
- panel.id = 'results-panel';
- Object.assign(panel.style, {
- position: 'fixed',
- top: '100px', // 根据你的 tm-asin-container 高度适当调整
- left: '5px',
- background: 'rgba(255,255,255,0.95)',
- border: '1px solid #ddd',
- borderRadius: '4px',
- padding: '5px',
- boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
- zIndex: '9999',
- fontSize: '14px',
- width: '290px',
- lineHeight: '1.4'
- });
- document.body.appendChild(panel);
- }
- // 1. 构造 HTML:带一个 drag-handle 和每行的“跳转”按钮
- const lines = Object.entries(results).map(([asin, r]) => {
- let text;
- if (r.found) {
- const totalRank = (r.page - 1) * 48 + r.position;
- text = `第${r.page}页${r.position}位-排名${totalRank}`;
- } else {
- text = `<span style="color:#f56c6c;">❌未找到</span>`;
- }
- // 如果找到了,就显示一个按钮,点击后跳转到该 ASIN 所在页
- const btn = r.found
- ? `
- <button class="rp-jump-btn" data-page="${r.page}"
- style="
- margin-top: 2px;
- margin-left: 6px;
- margin-bottom: 3.5px;
- line-height: 12px;
- color: #5ba7f4;
- background-color: #ecf5ff;
- border: 1px solid;
- border-radius: 5px;
- padding-top: 2px;
- cursor: pointer;"
- onmouseover="this.style.backgroundColor='#5ba7f4'; this.style.color='#ffffff'; this.style.borderColor='#5ba7f4';"
- onmouseout="this.style.backgroundColor='#ecf5ff'; this.style.color='#5ba7f4'; this.style.borderColor='initial';"
- >➡</button>
- <button class="dw-jump-btn" data-asin="${asin}"
- style='width: 0px;
- background-color: #ffffff;
- border: 0px;
- line-height: 12px;
- margin-top: -3px;
- font-size: 18px;
- padding: 0px;
- cursor: pointer;'
- onmouseover="this.style.fontSize='20px';"
- onmouseout="this.style.fontSize='18px';"
- >📍</button>`
- : '';
- return `<li style="margin-top:4px;list-style:none;">
- <strong>${asin}</strong>:${text}${btn}
- </li>`;
- }).join('');
-
- panel.innerHTML = `
- <div class="drag-handle" style="
- cursor: move;
- background:#f5f5f5;
- padding:6px;
- border-bottom:1px solid #ddd;
- font-weight:500;
- font-size: 16px;
- ">查询结果</div>
- <ul style="padding:4px;margin:0;">${lines}</ul>
- `;
-
- // 2. 拖拽:只绑定到 .drag-handle
- const handle = panel.querySelector('.drag-handle');
- handle.onmousedown = e => {
- const rect = panel.getBoundingClientRect();
- const shiftX = e.clientX - rect.left;
- const shiftY = e.clientY - rect.top;
- function onMouseMove(e) {
- panel.style.left = (e.clientX - shiftX) + 'px';
- panel.style.top = (e.clientY - shiftY) + 'px';
- }
- document.addEventListener('mousemove', onMouseMove);
- document.onmouseup = () => {
- document.removeEventListener('mousemove', onMouseMove);
- document.onmouseup = null;
- };
- e.preventDefault();
- };
- handle.ondragstart = () => false;
-
- // 3. 点击 “跳转” 按钮 的事件委托
- panel.onclick = e => {
- if (e.target.matches('.rp-jump-btn')) {
- const page = parseInt(e.target.dataset.page, 10);
- // 当前页page
- const currentUrl = new URL(location.href);
- const currentPage = parseInt(currentUrl.searchParams.get('page'), 10) || 1;
-
- // 如果要跳转的页就是当前页
- if (page === currentPage) {
- console.log(`Already on page ${page}, no navigation.`);
- return;
- }
- // 跳转
- const gotoUrl = currentUrl;
- gotoUrl.searchParams.delete('page');
- if (page > 1) gotoUrl.searchParams.set('page', page);
- location.href = gotoUrl.href;
- }
- if (e.target.matches('.dw-jump-btn')) {
- // 需要高亮的asin
- const highLightAsin = e.target.dataset.asin;
- // 只在 Amazon 搜索结果区查找&非广告位
- const candidates = document.querySelectorAll(`.s-main-slot > [data-asin="${highLightAsin}"]`);
- const elem = Array.from(candidates).find(node =>
- !node.querySelector('a.puis-label-popover.puis-sponsored-label-text')
- );
- if (!elem) {
- return alert(`未能在当前页面找到ASIN-${highLightAsin}, 点击前方页面跳转按钮`);
- }
- // 4. 高亮 & 滚动到视图
- elem.style.border = '2px solid red';
- elem.style.padding = '5px';
- elem.scrollIntoView({ behavior: 'smooth', block: 'center' });
- }
- }
- };
-
- // —— 缓存清除 ——
- btnClearCache.addEventListener('click', () => {
- sessionStorage.removeItem('tm_tagAsins');
- sessionStorage.removeItem('tm_results');
- sessionStorage.removeItem('tm_keywords');
- sessionStorage.removeItem('tm_batch_table');
-
- // 请填写 ASIN, 点击"搜索排名
- updateStatus('请填写 ASIN, 点击"搜索排名')
-
- tagAsins = [];
- results = {};
- keywords = [];
-
- renderTags();
- renderResultsPanelFromTable()
-
- const panel = document.getElementById('results-panel');
- if (panel) panel.remove();
-
- const batchPanel = document.getElementById('batch-results-panel');
- if (batchPanel) batchPanel.remove();
- });
-
- // 动态加载 SheetJS(xlsx.full.min.js),确保全局有 XLSX
- async function loadSheetJSLib() {
- return new Promise((resolve, reject) => {
- if (window.XLSX) return resolve();
- const s = document.createElement('script');
- s.src = 'https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js';
- s.onload = () => resolve();
- s.onerror = () => reject(new Error('加载 XLSX 库失败'));
- document.head.appendChild(s);
- });
- }
-
- // 然后在设置 fileInput 监听之前,先调用它
- await loadSheetJSLib();
-
- // —— excel文件解析 ——
- fileInput.addEventListener('change', async e => {
- const file = e.target.files[0];
- if (!file) alert('未选择文件');
-
- // 校验文件大小:不超过 1MB
- const maxSize = 1 * 1024 * 1024; // 1MB
- if (file.size > maxSize) {
- alert('Excel 文件不能大于 1MB,请选择更小的文件。');
- fileInput.value = ''; // 清空选中文件
- return;
- }
-
- // 读取并解析
- const data = await file.arrayBuffer();
- const wb = XLSX.read(data, { type: 'array' });
- const sheet = wb.Sheets[wb.SheetNames[0]];
- const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 });
-
- // 只取每行第一列,过滤空值并 trim
- keywords = rows
- .map(row => row[0])
- .filter(cell => typeof cell === 'string' && cell.trim().length > 0)
- .map(cell => cell.trim());
-
- sessionStorage.setItem('tm_keywords', JSON.stringify(keywords));
- batchSearchBtn.disabled = false;
- alert(`已导入并缓存 ${keywords.length} 条关键词`);
- console.log('keywords keywords keywords', keywords);
- });
-
- // 工具-睡眠和随机数
- function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- function randomBetween(min, max) {
- return Math.floor(Math.random() * (max - min + 1)) + min;
- }
-
- // 包装后的 fetch 函数,包含随机延迟 & 错误退避
- async function fetchAsinWithDelay(keyword, asin, maxPages) {
- // 在真正请求之前,先等待 1-3 秒(随机)
- await sleep(randomBetween(3000, 5000));
-
- try {
- return await fetchAsinPosition(keyword, asin, maxPages);
- } catch (err) {
- console.warn(`Request failed for ${keyword} / ${asin}:`, err);
- // 碰到错误(网络、429等),退避 30–60 秒再重试一次
- await sleep(randomBetween(30000, 60000));
- return fetchAsinPosition(keyword, asin, maxPages);
- }
- }
-
- // 批量搜索fetch函数
- async function fetchAsinPosition(keyword, asin, maxPages) {
- const base = new URL(location.href);
- base.searchParams.set('k', keyword);
- base.searchParams.delete('page');
-
- for (let page = 1; page <= maxPages; page++) {
- base.searchParams.set('page', page);
- const html = await fetch(base.href, { credentials: 'include' })
- .then(r => r.text());
- const doc = new DOMParser().parseFromString(html, 'text/html');
- let nat = 0;
- for (const node of doc.querySelectorAll('div[data-asin]')) {
- if (!node.querySelector('button.a-button-text, a.a-button-text')) continue;
- if (node.querySelector('.puis-sponsored-label-text')) continue;
- // 只加自然位
- nat++;
- if (node.getAttribute('data-asin') === asin) {
- return { page, position: nat };
- }
- }
- }
- // 确保不返回undefined
- return { page: null, position: null };
- }
-
- // 渲染批量结果面板
- function renderResultsPanelFromTable(table) {
- let panel = document.getElementById('batch-results-panel');
- if (!panel) {
- panel = document.createElement('div');
- panel.id = 'batch-results-panel';
- Object.assign(panel.style, {
- position: 'fixed',
- top: '100px',
- left: '10px',
- background: 'rgba(255,255,255,0.95)',
- border: '1px solid #ddd',
- borderRadius: '4px',
- boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
- zIndex: '9999',
- width: '320px',
- fontSize: '14px',
- lineHeight: '1.4',
- overflow: 'hidden'
- });
- document.body.appendChild(panel);
-
- const header = document.createElement('div');
- header.id = 'results-header';
- header.textContent = '查询结果';
- Object.assign(header.style, {
- cursor: 'move',
- background: '#f5f5f5',
- padding: '6px 8px',
- borderBottom: '1px solid #ddd',
- fontWeight: '600',
- fontSize: '16px'
- });
- panel.appendChild(header);
-
- header.addEventListener('mousedown', e => {
- const rect = panel.getBoundingClientRect();
- const dx = e.clientX - rect.left;
- const dy = e.clientY - rect.top;
- function mm(ev) {
- panel.style.left = ev.clientX - dx + 'px';
- panel.style.top = ev.clientY - dy + 'px';
- }
- document.addEventListener('mousemove', mm);
- document.addEventListener('mouseup', () => {
- document.removeEventListener('mousemove', mm);
- }, { once: true });
- e.preventDefault();
- });
- } else {
- panel.innerHTML = '';
- const header = document.createElement('div');
- header.id = 'batch-results-header';
- header.textContent = '查询结果';
- Object.assign(header.style, {
- cursor: 'move',
- background: '#f5f5f5',
- padding: '6px 8px',
- borderBottom: '1px solid #ddd',
- fontWeight: '600',
- fontSize: '16px'
- });
- panel.appendChild(header);
- header.addEventListener('mousedown', e => {
- const rect = panel.getBoundingClientRect();
- const dx = e.clientX - rect.left;
- const dy = e.clientY - rect.top;
- function mm(ev) {
- panel.style.left = ev.clientX - dx + 'px';
- panel.style.top = ev.clientY - dy + 'px';
- }
- document.addEventListener('mousemove', mm);
- document.addEventListener('mouseup', () => {
- document.removeEventListener('mousemove', mm);
- }, { once: true });
- e.preventDefault();
- });
- }
-
- const ul = document.createElement('ul');
- ul.style.listStyle = 'none';
- ul.style.padding = '8px';
- ul.style.margin = '0';
-
- table.forEach(({ keyword, asin, page, position }) => {
- const li = document.createElement('li');
- li.style.marginBottom = '6px';
-
- const text = document.createElement('span');
- const totalRank = (page - 1) * 48 + position;
- text.innerHTML = `<strong>${keyword}</strong> | ASIN: ${asin} | ` +
- (page ? `第${page}页 第${position}位 总排名${totalRank}` : `<span style="color:#f56c6c;">未找到</span>`);
- li.appendChild(text);
-
- if (page) {
- const btnJump = document.createElement('button');
- btnJump.className = 'rp-jump-btn';
- btnJump.dataset.page = page;
- btnJump.dataset.keyword = keyword
- btnJump.textContent = '➡';
- btnJump.style.marginLeft = '8px';
- li.appendChild(btnJump);
-
- const btnLoc = document.createElement('button');
- btnLoc.className = 'dw-jump-btn';
- btnLoc.dataset.asin = asin;
- btnLoc.textContent = '📍';
- btnLoc.style.marginLeft = '4px';
- li.appendChild(btnLoc);
- }
-
- ul.appendChild(li);
- });
-
- panel.appendChild(ul);
-
- panel.onclick = e => {
- const jump = e.target.closest('.rp-jump-btn');
- if (jump) {
- const page = +jump.dataset.page;
- const keyword = jump.dataset.keyword;
- // 构造新的搜索 URL:带上 k=keyword 和 page=page(page>1 时)
- const url = new URL(window.location.origin + '/s');
- url.searchParams.set('k', keyword);
- if (page > 1) url.searchParams.set('page', page);
- location.href = url.href;
- return;
- }
- const loc = e.target.closest('.dw-jump-btn');
- if (loc) {
- const a = loc.dataset.asin;
- const nodes = Array.from(document.querySelectorAll(`.s-main-slot > [data-asin="${a}"]`));
- const el = nodes.find(n => !n.querySelector('.s-sponsored-label, .puis-sponsored-label-text'));
- if (el) {
- el.style.border = '2px solid red';
- el.style.padding = '5px';
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
- } else {
- alert(`当前页未找到 ASIN:${a}`);
- }
- }
- };
- }
-
- // excel导出函数
- async function exportToExcel(data) {
- // data: [ { keyword, asin ,page, position }, … ]
- await loadSheetJSLib();
-
- // 2. 预处理数据:page/position 为空替换为 "-"
- const processed = data.map(({ asin, keyword, page, position, totalRank }) => ({
- asin,
- keyword,
- page: page == null ? "-" : page,
- position: position == null ? "-" : position,
- totalRank: totalRank == null ? "-" : totalRank
- }));
-
- // 把 JSON 转为工作表
- const ws = XLSX.utils.json_to_sheet(data, {
- header: ['关键词', 'ASIN', '页数', '位置', '总排名']
- });
-
- // 4. 给表头加粗且居中
- const range = XLSX.utils.decode_range(ws['!ref']);
- for (let C = range.s.c; C <= range.e.c; ++C) {
- const cellAddress = XLSX.utils.encode_cell({ r: 0, c: C });
- const cell = ws[cellAddress];
- if (!cell) continue;
- cell.s = {
- font: { bold: true },
- alignment: { horizontal: 'center' }
- };
- }
-
- // 新建工作簿并追加工作表
- const wb = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(wb, ws, '排名结果');
-
- // 6. 生成文件名:YYYY/M/D-站点-AsinKwRank
- const host = window.location.host;
- const siteMap = {
- 'www.amazon.com': 'US',
- 'www.amazon.co.uk': 'UK',
- 'www.amazon.ca': 'CA',
- 'www.amazon.de': 'DE',
- 'www.amazon.fr': 'FR',
- 'www.amazon.es': 'ES',
- 'www.amazon.it': 'IT'
- };
- const siteCode = siteMap[host] || host;
- const now = new Date();
- const fileName = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}-${siteCode}-AsinKwRank.xlsx`;
-
- // 生成二进制数组
- const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
-
- // 创建 Blob 并触发下载
- const blob = new Blob([wbout], { type: 'application/octet-stream' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'asin_keyword_rankings.xlsx';
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }
-
- // —— 批量搜索按钮 ——
- batchSearchBtn.addEventListener('click', async () => {
- if (!keywords.length) {
- return alert('请先导入关键词文件');
- }
- if (!tagAsins.length) {
- return alert('请先添加至少一个 ASIN');
- }
- const tasks = [];
- for (const keyword of keywords) {
- for (const asin of tagAsins) {
- // {'winter gloves men', B0xxxxxxxx}
- tasks.push({ keyword, asin });
- }
- }
- // 循环执行,每次查询一个"关键词 × ASIN"
- const table = [];
- updateStatus(`🔎 开始批量查询:${tasks.length} 次`);
- for (const { keyword, asin } of tasks) {
- updateStatus(`🔎 查询 "${keyword}" 下 ASIN-${asin}`);
- // fetchAsinWithDelay 返回 { page, position }
- const { page, position } = await fetchAsinWithDelay(keyword, asin, maxPages);
- const totalRank = (page - 1) * 48 + position;
- table.push({ keyword, asin, page, position, totalRank });
- }
- console.log('结果table 结果table 结果table', table);
- sessionStorage.setItem('tm_batch_table', JSON.stringify(table));
- sessionStorage.setItem('tm_tagAsins', JSON.stringify(tagAsins));
- alert('搜索完成,共 ' + table.length + ' 条记录');
- renderResultsPanelFromTable(table);
- })
-
- // 点击时,从 sessionStorage 取出缓存的 table,并调用 exportToExcel
- downloadBtn.addEventListener('click', async () => {
- const raw = sessionStorage.getItem('tm_batch_table');
- if (!raw) {
- return alert('当前没有可下载的查询结果,请先执行批量搜索。');
- }
- let table;
- try {
- table = JSON.parse(raw);
- } catch {
- return alert('结果数据解析失败。');
- }
- // 调用之前定义的导出函数
- await exportToExcel(table);
- });
- })();