scriptCat/yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.js
intelligrow 03d2c74dea feat: 添加人群画像批量下载功能
新增人群画像批量下载功能,用户可通过点击按钮下载选中人群的画像数据,并导出为CSV文件。该功能包括人群ID和名称的获取、画像数据的请求与处理、以及CSV文件的生成与下载。
2025-04-26 14:14:46 +08:00

286 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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