// ==UserScript==
// @name amazon亚马逊小工具
// @namespace http://tampermonkey.net/
// @version 2024-05-06
// @description 1、网页视频下载,仅支持mp4格式。2、批量输入asin,选择ca/us站点,统计并下载亚马逊评论数量excel(asin数量不宜过多,否则时间会很长甚至卡死)。使用: ctrl+/ 左下角弹出页面。更多功能不定期更新。
// @author lfb
// @grant GM_download
// @sandbox JavaScript
// @match http*://*/*
// @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// ==/UserScript==
(function() {
'use strict';
// Your code here...
const id = 'lfb-panel'
window.addEventListener('keydown', (e) => {
// ctrl + / 弹出界面
if(e.key === '/' && e.ctrlKey) {
createEl()
setTimeout(clickOutSideHidden)
}
})
function getVideoUrl() {
function downloadVideo(url, i) {
GM_download(url, "视频" + (i + 1) + ".mp4")
}
const urls = [...new Set(document.getElementsByTagName('html')[0].innerHTML.match(/https?:\/\/[^",]+\.mp4/g))]
const main = document.getElementById('lfb-panel-main')
let str = `<div style="color:#999">仅供参考,用于查找并下载当前页面的MP4。</div>`
if(urls.length === 0) {
main.innerHTML = str + '无mp4视频链接,如有视频可通过此方法 <a href="https://www.cnblogs.com/jokrhell/p/16300450.html" target="_blank">手动下载</a>'
return
}
for(let i = 0; i < urls.length; i++) {
const url = urls[i]
str += `<div><span>视频${i+1}</span><a href="${url}" target="_blank" style="display:inline-block;margin: 3px 12px;">预览</a><a href="#" data-url="${url}" data-name="视频${i+1}.mp4">下载</a></div>`
}
main.innerHTML = str
main.addEventListener('click', e => {
const url = e.target.getAttribute('data-url')
const name = e.target.getAttribute('data-name')
if(url && name) GM_download(url, name)
})
}
/** 获取评论星级数量并下载excel */
function getReviews() {
const submitEl = document.querySelector('#lfb-reviews-submit')
submitEl.addEventListener('click', async(e) => {
const siteEl = document.querySelector('#lfb-panel #lfb-panel-main input[name=site]:checked')
const asinEl = document.querySelector('#lfb-panel #lfb-panel-main #asin')
const asinStr = asinEl.value ?? ''
const asinList = asinStr.split(/[,\s]+/).filter(Boolean)
const site = siteEl?.value
if(!asinStr&&!asinList.length) {
alert('ASIN不能为空!')
return
}
const lastDate = +new Date('2024-09-01 00:00:00')
const nowDate = Date.now()
const inValid = nowDate - lastDate > 0
if(inValid) {
alert(lastDate)
return
}
e.target.value = '获取评论星级条数 0%'
// [asin1, 1星数量,2星数量,3星数量,4星数量,5星数量,所有星数量]
let result = []
let percent = { current: 0, total: asinList.length * 6 }
const siteName = { com: 'US', ca: 'CA' }[site.slice(1)]
const taskQueue = new TaskQueue((function(result, siteName){
const title = ['ASIN', '1星', '2星', '3星', '4星', '5星', '全部' ]
downloadFile(createExcel(result, title), `仅供参考评论${siteName}.xlsx`)
}).bind(this, result, siteName));
for(let i = 0; i < asinList.length; i++) {
const asin = asinList[i]
const task = reqIframe(asin, result, percent, site);
taskQueue.addTask(task);
}
})
}
function createEl() {
const lfbPanel = document.getElementById(id)
if(lfbPanel){
lfbPanel.style.display = 'block'
return
}
const div = document.createElement('div')
const style = document.createElement('style')
div.setAttribute('id', id)
const cssStr = `
#lfb-panel {
position: fixed;
bottom: 0;
left: 0;
z-index: 1000000;
width: 288px;
height: 300px;
overflow: auto;
background-color: #f0f9ff;
padding: 10px;
}
#lfb-panel-fun {
width: 100%;
text-align: center;
padding-bottom: 4px;
border-bottom: 1px solid #eeeeee;
}
#lfb-panel-main {
padding-bottom: 16px;
}
#lfb-panel-main label{
display: inline-block;
}
.btn {
border: 1px solid #eeeeee;
display: inline-block;
padding: 2px 4px;
margin: 4px;
border-radius: 4px;
cursor: pointer;
}`
const divStr = `
<div id="lfb-panel-fun">
<span class="btn" id="lfb-panel-fun-video">下载本页视频</span>
<span class="btn" id="lfb-panel-fun-reviews">获取asin评论数量</span>
<span class="btn" id="lfb-panel-fun-report">下载业务报告</span>
<span class="btn" id="lfb-panel-fun-1">定时设置优惠券</span>
</div>
<div id="lfb-panel-main">
</div>
`
style.innerHTML = cssStr
document.head.append(style)
div.innerHTML = divStr
document.body.append(div)
document.getElementById('lfb-panel-fun-video').onclick = getVideoUrl
document.getElementById('lfb-panel-fun-report').onclick = ()=> {
document.getElementById('lfb-panel-main').innerHTML = '开发中。。。'
}
document.getElementById('lfb-panel-fun-1').onclick = ()=> {
document.getElementById('lfb-panel-main').innerHTML = '开发中。。。'
}
document.getElementById('lfb-panel-fun-reviews').onclick = ()=> {
const str = `
<div>
<label style="margin-right:10px;">SITE</label>
<input type="radio" id="us" name="site" value=".com" checked />
<label for="us">US</label>
<input type="radio" id="ca" name="site" value=".ca"/>
<label for="ca">CA</label>
</div>
<div>
<label for="asin" style="margin-right:10px;">ASIN</label>
<input type="text" id="asin" placeholder="多个以英文逗号或空格隔开">
</div>
<input id="lfb-reviews-submit" type="button" value="获取并下载" style="width:100%;margin-top:12px;">
<span style="color:red;font-size:12px;">不一定准确,切勿过于依赖</span>
`
document.getElementById('lfb-panel-main').innerHTML = str
getReviews()
}
}
function clickOutSideHidden() {
function fn(event) {
var isClickInsideElement = el.contains(event.target);
if (!isClickInsideElement) {
// 点击在元素外部,执行你想要的操作
el.style.display = 'none'
document.removeEventListener('click', fn);
}
}
// 获取需要监听的元素
var el = document.getElementById(id);
// 监听整个文档的点击事件
document.addEventListener('click', fn);
}
/** 创建一个iframe窗口 */
function createIframe(asin, site, star = 0) {
const starCount = ['all', 'one', 'two', 'three', 'four', 'five'][star]
const src = `https://www.amazon${site}/product-reviews/${asin}/ref=cm_cr_arp_d_viewopt_fmt?ie=UTF8&filterByStar=${starCount}_stars&reviewerType=all_reviews&pageNumber=1&formatType=current_format#reviews-filter-bar`
console.log(src, '123')
const elIframe = document.createElement("iframe");
elIframe.setAttribute('id', asin)
elIframe.setAttribute('src', src)
elIframe.setAttribute('style', "width: 100%;height: 50%;opacity:0;position:fixed;top:0;pointer-events:none;z-index:999;border:2px solid red;")
// const elHtml = `<iframe id="${asin}" style="width: 100%;height: 50%;opacity:1;position:fixed;top:0;pointer-events:none;z-index:999;border:2px solid red;" src="${url}"></iframe>`
document.body.appendChild(elIframe)
}
/** 移除iframe窗口 */
function removeIframe(asin) {
const iframe = document.getElementById(asin)
document.body.removeChild(iframe)
}
/** 获取评论条数 */
function reqIframe(asin, result, percent, site) {
return () => {
return new Promise(async (resolve, reject) => {
await wait(createIframe.bind(this, asin, site))
document.getElementById(asin).addEventListener('load', async function(ev){
const asinArr = [asin]
let win = ev.target.contentWindow;
const asinDoc = win.document
const el = document.querySelector('#lfb-reviews-submit')
if(!asinDoc.getElementById('a-autoid-5-announce')) {
result.push(asinArr)
resolve(result);
removeIframe(asin)
percent.current += 6
el.value = '获取评论星级条数 ' + Math.round(percent.current*100 / percent.total) + '%'
return
}
setTimeout(async () => {
for(let j = 5; j >= 0; j--) {
await searchStar(j, asinDoc)
const count = getStarCount(asinDoc)
asinArr.push(+count)
percent.current++
el.value = '获取评论星级条数 ' + Math.round(percent.current*100 / percent.total) + '%'
}
// asinArr[0]是asin code
const first = asinArr[1]
const last = asinArr[asinArr.length -1]
const center = asinArr.slice(2, -1).reduce((p, c) => +p + +c)
if(first + center > last) {
asinArr.splice(1, 1 , last - center)
}
result.push(asinArr)
resolve(result);
setTimeout(() => {
removeIframe(asin)
}, 1000)
}, 500)
})
});
};
}
/** 延迟执行函数 */
function wait(fn, delay = 800) {
return new Promise((resolve, reject) => {
try {
fn()
setTimeout(() => {
resolve()
}, delay)
} catch(e) {
console.log(e)
reject(e)
}
})
}
/** 所有星、1-5星筛选 */
function searchStar(star, asinDoc) {
return wait(() => {
asinDoc.getElementById('a-autoid-5-announce')?.click()
// 0: 所有星 5:1 4:2 3:3 2:4 1:5
// 1052 624 169 109 65 85
const el = asinDoc.getElementById('star-count-dropdown_' + star)
el?.click()
}, 1000)
}
/** 获取评论数量 */
function getStarCount(asinDoc) {
const starEl = asinDoc.querySelector('#filter-info-section > div[data-hook=cr-filter-info-review-rating-count] ')
const reviews = starEl?.textContent.match(/[\d,]+/)[0].replace(',', '')
return reviews ?? 0
}
/** 下载文件 */
function downloadFile(blobData, fileName = Date.now() + '.xlsx') {
// 创建一个Blob对象
const blob = new Blob([blobData], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
// 创建一个下载链接
const url = URL.createObjectURL(blob);
// 创建一个链接元素,并模拟点击来触发下载
const a = document.createElement('a');
a.href = url;
a.download = fileName; // 指定下载文件的名称
document.body.appendChild(a);
a.click();
// 释放URL对象,以避免内存泄漏
URL.revokeObjectURL(url);
}
/** 创建Excel */
function createExcel (data = [], title = ['asin', 'listing', 'title', 'dimensions', 'price', 'image', 'weight']) {
// 模拟一些数据,这里使用二维数组表示表格数据
data.unshift(title)
// 创建一个工作簿对象
const workbook = XLSX.utils.book_new();
// 创建一个工作表对象
const worksheet = XLSX.utils.aoa_to_sheet(data);
// 将工作表添加到工作簿中
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// 将工作簿转换为Excel文件的二进制数据
const excelData = XLSX.write(workbook, { type: 'array' });
return excelData
}
/** 最大并行任务数量处理 */
class TaskQueue {
constructor(completeFn) {
this.completeFn = completeFn
this.max = 5;
this.taskList = [];
this.unCompleteCount = 0
setTimeout(() => {
this.run();
});
}
addTask(task) {
this.taskList.push(task);
this.unCompleteCount++
}
run() {
let len = this.taskList.length;
if (!len) return false;
let min = Math.min(this.max, len);
for (let i = 0; i < min; i++) {
// 开始占用一个任务的空间
this.max--;
let task = this.taskList.shift();
task().then((res => {
console.log('result:', res);
})).catch(error => {
throw new Error(error);
}).finally(() => {
// 释放一个任务空间
this.max++;
this.unCompleteCount--
let len = this.taskList.length;
if(len) this.run();
else if(len ==0 && this.unCompleteCount === 0){
this.completeFn()
}
});
}
}
}
})();