feat: 添加人群画像批量下载功能
新增人群画像批量下载功能,用户可通过点击按钮下载选中人群的画像数据,并导出为CSV文件。该功能包括人群ID和名称的获取、画像数据的请求与处理、以及CSV文件的生成与下载。
This commit is contained in:
parent
3c3ecf8947
commit
03d2c74dea
286
yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.js
Normal file
286
yuntu/yuntuCrowdPortraitDownload/yuntuCrowdPortraitDownload.js
Normal file
@ -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('<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
})()
|
||||||
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
Loading…
x
Reference in New Issue
Block a user