// ==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); } } })()