diff --git a/yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.js b/yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.js new file mode 100644 index 0000000..f4c766e --- /dev/null +++ b/yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.js @@ -0,0 +1,286 @@ +// ==UserScript== +// @name 批量下载人群画像 +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 0.3.2 +// @description try to take over the world! +// @author wan喜 +// @match https://yuntu.oceanengine.com/yuntu_ng/analysis/audience/list?* +// @match https://yuntu.oceanengine.com/yuntu_brand/analysis/audience/list?* +// @icon https://www.google.com/s2/favicons?domain=oceanengine.com +// @require https://cdn.staticfile.net/sweetalert2/11.10.0/sweetalert2.all.min.js +// ==/UserScript== + +(function () { + "use strict" + + var newButton = document.createElement("button") + newButton.innerHTML = "下载画像" + newButton.type = "button" + newButton.style.height = "34px" + newButton.style.lineHeight = "34px"; + newButton.style.align = "center"; //文本居中 + newButton.style.color = "white"; //按钮文字颜色 + newButton.style.background = "#1f4bd9"; //按钮底色 + newButton.style.border = "1px solid #1f4bd9"; //边框属性 + newButton.style.borderRadius = "3px"; //按钮四个角弧度 + newButton.style.marginLeft = '10px'; + newButton.style.fontSize = '12px'; + newButton.style.padding = '0 10px'; + newButton.className = "byted-btn byted-btn-size-md byted-btn-type-primary byted-btn-shape-angle byted-can-input-grouped" + newButton.style.marginLeft = "10px" + newButton.addEventListener("click", handleButtonClick) //监听按钮点击事件 + + function appendDoc() { + setTimeout(() => { + var a = document.querySelector(".rowFlexBox-GWvhwN") + if (a) { + a.append(newButton) + } + appendDoc() + }, 1000) + } + appendDoc() + + const appID = "1128" //抖音 + const neededFeatureNames = ['8大消费群体', '预测年龄段', '预测性别', '城市级别'] //需要的画像维度 + + // 在脚本的顶部定义一个全局变量 + var selectedCrowds = { + crowdIdList: [], + crowdNameList: [] + }; + + + + function getID() { + //从url中获取aadvid + const url = window.location.href + const aadvid = url.split('aadvid=')[1] + + // 选择所有选中的复选框所在的行 + const checkedRows = document.querySelectorAll('.yuntu_analysis-Table-Body .yuntu_analysis-Table-Row .yuntu_analysis-checkbox-checked') + console.log(checkedRows) + let crowdNameList = [] + let crowdIdList = [] + var invalidCrowds = [] + + checkedRows.forEach(row => { + // 获取人群ID,人群ID位于操作按钮的 data-log-value 属性中 + const crowdId = row.closest('tr').querySelector('.operationItem-lHPsZh[data-log-value]').getAttribute('data-log-value') + + // 获取人群名称,人群名称位于带有text-D50wBP类的div中 + const crowdName = row.closest('tr').querySelector('.text-D50wBP').textContent.trim() + + //获取人群包画像操作按钮 + const crowdProfileOperate = row.closest('tr').querySelector('.operationItem-lHPsZh[data-log-label]') + + //获取人群包画像状态 + const crowdProfileStatus = crowdProfileOperate.textContent.trim() + + // 检查画像状态是否满足条件 + if (crowdProfileStatus === "查看画像" && !crowdProfileOperate.classList.contains('disabled-I30WTJ')) { //如果是计算中的人群包,class中会有disabled-I30WTJ + crowdIdList.push(crowdId) + crowdNameList.push(crowdName) + } else { + console.log(crowdProfileStatus) + + invalidCrowds.push(crowdName) + } + }) + + let crowdList = { crowdIdList, crowdNameList } + console.log('crowdList:' + JSON.stringify(crowdList)) + console.log('invalidCrowds:' + invalidCrowds.join(',')) + + return { + aadvid, + crowdList + } + } + + async function fetchData(url, method, body = null) { + const headers = { + "content-type": "application/json" + } + + const options = { + method: method, + headers: headers + } + + if (body) { + options.body = JSON.stringify(body) + } + + const response = await fetch(url, options) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } else { + const json = await response.json() + return json + //只返回data以下的部分 + } + } + + + + + async function fetchProfile(aadvid, crowdId, crowdName) { + const url = "https://yuntu.oceanengine.com/yuntu_ng/api/v1/LabelDistribution?aadvid=" + aadvid + "&audience_id=" + crowdId + "&app=" + appID + "&page=1&page_size=100000&features=&label_all_cnt_filter=" + const profileJson = await fetchData(url, "GET") + const profileArr = profileJson.data.labels + const processedArr = profileArr + .filter(item => neededFeatureNames.includes(item.feature_name)) + .sort((a, b) => neededFeatureNames.indexOf(a.feature_name) - neededFeatureNames.indexOf(b.feature_name)) + .map(item => { + let result = { + feature_name: item.feature_name, + label_name: item.label_name + } + result[crowdName] = String(item.rate) // todo: crowdId 改成crowdName + return result + }) + + return processedArr + } + + function mergeArr(arr) { + let mergedArray = [] + let map = new Map() + + arr.forEach(subArr => { + subArr.forEach(item => { + let key = item['feature_name'] + '-' + item['label_name'] + let existingItem = map.get(key) + if (existingItem) { + existingItem = { ...existingItem, ...item } + } else { + existingItem = { ...item } + } + map.set(key, existingItem) + }) + }) + + map.forEach(value => { + mergedArray.push(value) + }) + + return mergedArray + } + + class chainFunction { + async msg() { + const Toast = Swal.mixin({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 3000, + }) + Toast.fire({ + icon: "success", + title: "任务开始" + }) + } + async getProfile() { + let profileArr = [] + for (let i = 0; i < this.crowdIdList.length; i++) { + let neededProfile = await fetchProfile(this.aadvid, this.crowdIdList[i], this.crowdNameList[i]) + console.log(neededProfile) //所需要的画像维度 + profileArr.push(neededProfile) + } + console.log(profileArr) + this.profileArr = profileArr + return this + } + async getMergedProfile() { + let mergedProfile = mergeArr(this.profileArr) + this.mergedProfile = mergedProfile + console.log(mergedProfile) //合并后的所有画像 + return this + } + + async downloadCsv() { + let csv = ''; + let keys = ['feature_name', 'label_name', ...Object.keys(this.mergedProfile[0]).filter(key => key !== 'feature_name' && key !== 'label_name')] + csv += keys.join(',') + '\n' + for (let i = 0; i < this.mergedProfile.length; i++) { + let values = keys.map(key => this.mergedProfile[i][key]) + csv += values.join(',') + '\n' + } + var hiddenElement = document.createElement('a') + var fileName = this.crowdNameList.map(name => name.length > 5 ? name.slice(0, 5) : name).join('+') + hiddenElement.href = 'data:text/csv;charset=utf-8,%EF%BB%BF' + encodeURI(csv) + hiddenElement.target = '_blank' + hiddenElement.download = fileName.substring(0, 30) + ".csv" //只保留30个字符 防止太长Excel打不开 + hiddenElement.click() + } + } + + + + // 使用链式函数 + async function handleButtonClick() { + try { + let ID = getID() + // 对当前页面选中的人群包进行去重处理 + ID.crowdList.crowdIdList.forEach((crowdId, index) => { + if (!selectedCrowds.crowdIdList.includes(crowdId)) { + selectedCrowds.crowdIdList.push(crowdId) + selectedCrowds.crowdNameList.push(ID.crowdList.crowdNameList[index]) + } + }) + //处理不可用的人群包 + /* + if (ID.invalidCrowds.length > 0) { + const Toast = Swal.mixin({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 3000, + }) + Toast.fire({ + title: "已自动剔除没有画像的人群包: " + ID.invalidCrowds.join(', '), + icon: "warning" + }) + ID.invalidCrowds = [] + } + */ + // 弹出 swal 对话框 + const { value } = await Swal.fire({ + title: '选择操作', + html: selectedCrowds.crowdNameList.join('
'), + icon: 'question', + showCancelButton: true, + confirmButtonText: '下载当前的人群', + cancelButtonText: '继续添加' + + }); + + + + if (value) { + // 如果用户选择直接下载 + let chain = new chainFunction(); + await chain.msg(); + chain.aadvid = ID.aadvid; + chain.crowdIdList = selectedCrowds.crowdIdList; + chain.crowdNameList = selectedCrowds.crowdNameList; + await chain.getProfile(); + await chain.getMergedProfile(); + await chain.downloadCsv(); + // 下载后清空全局变量 + selectedCrowds.crowdIdList = []; + selectedCrowds.crowdNameList = []; + } else { + // 如果用户选择继续添加人群包,什么也不做,允许用户切换页面继续选择 + } + } catch (error) { + console.error(error); + } + } + + + + +})() \ No newline at end of file diff --git a/yuntu/yuntuMarketInsightBrandDataDownload/yuntuMarketInsightBrandDataDownload.js b/yuntu/yuntuMarketInsightBrandDataDownload/yuntuMarketInsightBrandDataDownload.js new file mode 100644 index 0000000..e9cb4bf --- /dev/null +++ b/yuntu/yuntuMarketInsightBrandDataDownload/yuntuMarketInsightBrandDataDownload.js @@ -0,0 +1,188 @@ +// ==UserScript== +// @name 批量下载市场洞察品牌数据 +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 0.1.0 +// @description 下载抖音云图市场洞察品牌数据 +// @author wan喜 +// @match https://yuntu.oceanengine.com/yuntu_brand/ecom/product/segmentedMarketDetail/marketProduct?* +// @icon https://www.google.com/s2/favicons?domain=oceanengine.com +// @require https://cdn.staticfile.net/sweetalert2/11.10.0/sweetalert2.all.min.js +// ==/UserScript== + +(function () { + "use strict" + + // 创建下载按钮 + var newButton = document.createElement("button") + newButton.innerHTML = "下载品牌数据" + newButton.type = "button" + newButton.style.height = "34px" + newButton.style.lineHeight = "34px" + newButton.style.align = "center" // 文本居中 + newButton.style.color = "white" // 按钮文字颜色 + newButton.style.background = "#1f4bd9" // 按钮底色 + newButton.style.border = "1px solid #1f4bd9" // 边框属性 + newButton.style.borderRadius = "3px" // 按钮四个角弧度 + newButton.style.marginLeft = '10px' + newButton.style.fontSize = '12px' + newButton.style.padding = '0 10px' + newButton.className = "byted-btn byted-btn-size-md byted-btn-type-primary byted-btn-shape-angle byted-can-input-grouped" + newButton.addEventListener("click", handleButtonClick) // 监听按钮点击事件 + + // 添加按钮到页面 + function appendButton() { + setTimeout(() => { + // 这里需要根据实际页面结构找到合适的位置添加按钮 + var targetElement = document.querySelector(".操作区域的选择器") // 需要根据实际页面修改 + if (targetElement) { + targetElement.append(newButton) + console.log("按钮已添加") + } else { + console.log("未找到目标元素,继续尝试") + appendButton() + } + }, 1000) + } + appendButton() + + // 从URL获取参数 + function getParamsFromUrl() { + const url = window.location.href + const aadvid = url.split('aadvid=')[1]?.split('&')[0] + const reportId = url.split('reportId=')[1]?.split('&')[0] + + return { + aadvid, + reportId + } + } + + // 获取请求体参数 + function getRequestBody() { + // 这里需要根据实际情况获取或设置参数 + // 可以从页面元素中提取,或者让用户输入 + return { + reportId: "", // 需要获取 + categoryId: "0", + contentType: "ALL", + startDate: "0", + endDate: "2025-03-31", + dateType: "MONTH", + analysisType: "DRILL_DOWN", + itemType: "PRODUCT_BRAND", + sortKey: "salesAmount", + page: 1, + pageSize: 100, + order: "ascend" + } + } + + // 发送请求获取数据 + async function fetchBrandData(aadvid, body) { + const url = `https://yuntu.oceanengine.com/product_node/v2/api/industry/insightBrandStats?aadvid=${aadvid}` + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "accept": "application/json, text/plain, */*", + "content-type": "application/json" + }, + body: JSON.stringify(body), + credentials: "include" + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + return data + } catch (error) { + console.error("获取数据失败:", error) + throw error + } + } + + // 处理数据并下载CSV + function processAndDownloadData(data) { + // 假设data.data.list是品牌数据列表 + if (!data || !data.data || !data.data.list) { + throw new Error("数据格式不正确") + } + + const brandList = data.data.list + + // 创建CSV内容 + let csv = '' + + // 添加表头 + const headers = Object.keys(brandList[0]) + csv += headers.join(',') + '\n' + + // 添加数据行 + brandList.forEach(brand => { + const values = headers.map(header => { + let value = brand[header] + // 处理包含逗号的字符串 + if (typeof value === 'string' && value.includes(',')) { + return `"${value}"` + } + return value + }) + csv += values.join(',') + '\n' + }) + + // 下载CSV文件 + const hiddenElement = document.createElement('a') + hiddenElement.href = 'data:text/csv;charset=utf-8,%EF%BB%BF' + encodeURI(csv) + hiddenElement.target = '_blank' + hiddenElement.download = `品牌数据_${new Date().toISOString().slice(0, 10)}.csv` + hiddenElement.click() + } + + // 按钮点击处理函数 + async function handleButtonClick() { + try { + // 显示加载提示 + const Toast = Swal.mixin({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 3000, + }) + Toast.fire({ + icon: "info", + title: "正在获取数据..." + }) + + // 获取参数 + const { aadvid, reportId } = getParamsFromUrl() + + // 获取请求体 + const requestBody = getRequestBody() + requestBody.reportId = reportId + + // 获取数据 + const brandData = await fetchBrandData(aadvid, requestBody) + + // 处理并下载数据 + processAndDownloadData(brandData) + + // 显示成功提示 + Toast.fire({ + icon: "success", + title: "数据下载成功" + }) + } catch (error) { + console.error(error) + + // 显示错误提示 + Swal.fire({ + icon: "error", + title: "下载失败", + text: error.message + }) + } + } +})() \ No newline at end of file