Greasy Fork

小红书广告数据查询与展示

在当前页面插入悬浮元素,点击展开窗口并发送请求,将结果展示在表格中并提供Excel下载按钮

// ==UserScript==
// @name         小红书广告数据查询与展示
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  在当前页面插入悬浮元素,点击展开窗口并发送请求,将结果展示在表格中并提供Excel下载按钮
// @author       You
// @match        https://ad.xiaohongshu.com/aurora*
// @grant        GM_xmlhttpRequest
// @connect      ad.xiaohongshu.com
// @license MIT
// ==/UserScript==

(function () {
  "use strict";


  const sheet = new CSSStyleSheet();
  sheet.replaceSync(`
.loading {
  width: 30px;
  height: 30px;
  border: 2px solid #000;
  border-top-color: transparent;
  border-radius: 100%;

  animation: circle infinite 0.75s linear;
}

// 转转转动画
@keyframes circle {
  0% {
    transform: rotate(0);
  }
  100% {
    transform: rotate(360deg);
  }
}

  `);

  function addStyleSheet(sheet) {
    const allSheets = [...document.adoptedStyleSheets, sheet];
    document.adoptedStyleSheets=allSheets

  }
  addStyleSheet(sheet)
  // 存储请求返回的数据
  let responseData = [];
  function addBallAttributes(ball,text){
    ball.style.cssText = `
      position: fixed; 
      right: 10px; 
      bottom: 10px; 
      width: 60px; 
      height: 60px; 
      background-color: #0066CC; 
      border-radius: 50%; 
      cursor: pointer; 
      display: flex; 
      align-items: center; 
      justify-content: center; 
      font-size: 12px; 
      color: white; 
      z-index: 9999;
    `;
    ball.textContent = text;
  }
  // 创建隐藏的小球元素
  const ball = document.createElement("div");
  addBallAttributes(ball,"检查链接")
  document.body.appendChild(ball);

  // 创建悬浮窗口元素
  const floatingWindow = document.createElement("div");
  floatingWindow.style.cssText = `
    position: fixed; 
    right: 20px; 
    top: 20px; 
    width: 50vw; 
    height: 80vh; 
    background-color: white; 
    border: 1px solid #0066CC; 
    border-radius: 8px; 
    display: none; 
    padding: 20px; 
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 
    z-index: 9999;
  `; // 改成 cssom
  document.body.appendChild(floatingWindow);

  // 创建关闭按钮
  const closeButton = document.createElement("div");
  closeButton.style.cssText = `
    position: absolute; 
    right: 10px; 
    top: 10px; 
    cursor: pointer; 
    font-size: 20px; 
    color: #0066CC;
  `; // 改成 cssom
  closeButton.innerHTML = "×";
  floatingWindow.appendChild(closeButton);

  // 创建按钮容器
  const buttonContainer = document.createElement("div");
  buttonContainer.style.cssText = `
    margin-bottom: 20px;
  `; // 改成 cssom
  floatingWindow.appendChild(buttonContainer);
  function addButtonAttributes(checkButton,text){
    checkButton.textContent = text;//"检查";
    checkButton.style.cssText = `
      margin-right: 10px; 
      padding: 8px 16px; 
      background-color: #0066CC; 
      color: white; 
      border: none; 
      border-radius: 4px; 
      cursor: pointer;
    `; // 改成 cssom
  }
  // 创建检查按钮
  const checkButton = document.createElement("button");
  addButtonAttributes(checkButton,"检查")
  buttonContainer.appendChild(checkButton);

  // 创建下载按钮
  const downloadButton = document.createElement("button");
  addButtonAttributes(downloadButton,"下载")
  buttonContainer.appendChild(downloadButton);

  // 创建链接统计文本容器
  const linkStatsContainer = document.createElement("div");
  linkStatsContainer.style.cssText = `
    margin-bottom: 20px; 
    font-size: 14px; 
    color: #333333;
  `; // 改成 cssom
  floatingWindow.appendChild(linkStatsContainer);

  // 创建表格容器
  const tableContainer = document.createElement("div");
  tableContainer.style.cssText = `
    height: calc(100% - 100px); 
    overflow-y: auto; 
    overflow-x: auto;
  `; // 改成 cssom
  floatingWindow.appendChild(tableContainer);

  // 创建进度条
  const progressBar = document.createElement("progress");
  progressBar.style.cssText = `
    width: 100%; 
    margin-top: 10px; 
    display: none;
  `; // 改成 cssom
  progressBar.value = 0;
  progressBar.max = 100;
  floatingWindow.appendChild(progressBar);

  // 显示小球
  ball.style.display = "flex";

  // 点击小球展开悬浮窗口
  ball.addEventListener("click", function () {
    floatingWindow.style.display = "block";
    ball.style.display = "none";
  });

  // 点击关闭按钮
  closeButton.addEventListener("click", function () {
    floatingWindow.style.display = "none";
    ball.style.display = "flex";
    tableContainer.innerHTML = "";
    linkStatsContainer.innerHTML = "";
    responseData = []; // 清空存储的数据
  });

  // 点击检查按钮发送请求
  checkButton.addEventListener("click", function () {
    checkButton.disabled = true;
    checkButton.style.backgroundColor ="rgb(105, 102, 102)"
    const loading = document.createElement("div");
    loading.className = "loading";
    loading.id = "loading-indicator"; // 添加一个唯一ID
    tableContainer.appendChild(loading);
    sendRequest().then(() => {
      checkButton.disabled = false;
      checkButton.style.backgroundColor ="rgb(0, 102, 204)"  
      
      // 使用更安全的方式移除loading
      const loadingElement = document.getElementById("loading-indicator");
      if (loadingElement) {
        loadingElement.remove();
      }
    });
  });

  // 点击下载按钮
  downloadButton.addEventListener("click", function () {
    if (responseData.length > 0) {
      downloadExcel(responseData);
    } else {
      alert("没有可下载的数据!");
    }
  });

  // 发送请求
  async function sendRequest() {

    let maxPageNum = 4;
    let pageNum = 1;
    let totalPage = 1;
    let allData = [];
    do {
      const response = await fetch(
        `https://ad.xiaohongshu.com/api/leona/rtb/creativity/list?pageNum=${pageNum}&pageSize=50`,
        {
          method: "POST",
          headers: {
            accept: "application/json, text/plain, */*",
            "accept-language":
              "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
            "content-type": "application/json;charset=UTF-8",
            priority: "u=1, i",
            "sec-ch-ua":
              '"Not A(Brand";v="8", "Chromium";v="132", "Microsoft Edge";v="132"',
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": '"Windows"',
            "sec-fetch-dest": "empty",
            "sec-fetch-mode": "cors",
            "sec-fetch-site": "same-origin",
          },
          body: JSON.stringify({
            startTime: new Date().toISOString().split("T")[0],
            endTime: new Date().toISOString().split("T")[0],
            pageNum: pageNum,
            pageSize: 50,
          }),
        }
      );
      const data = await response.json();
      console.log(data.data.list);

      allData = allData.concat(data.data.list);
      totalPage = data.data.totalPage;
      pageNum++;
    } while (pageNum <= totalPage || pageNum <= maxPageNum);

    try {
      // 保存响应数据
      responseData = allData;

      // 统计链接重复次数
      const clickUrlCounts = {};
      const expoUrlCounts = {};

      allData.forEach((item) => {
          if (item.clickUrls && item.clickUrls[0]) {
              const clickUrl = extractUrlParam(item.clickUrls[0]);
              clickUrlCounts[clickUrl] = (clickUrlCounts[clickUrl] || 0) + 1;
          }
          if (item.expoUrls && item.expoUrls[0]) {
              const expoUrl = extractUrlParam(item.expoUrls[0]);
              expoUrlCounts[expoUrl] = (expoUrlCounts[expoUrl] || 0) + 1;
          }
      });

      // 读取option 本地持久化的存储
      const storedOption = localStorage.getItem('data');
      const option = JSON.parse(storedOption);
      option.forEach((item) => {
          const link = item.link;
          const relink = link.match(/e=[^&]+/);
          if (relink) {
              item.relink = relink[0];
          } else {
              item.relink = link
          }
      });
      console.log(option);

      if (storedOption) {
          Object.entries(clickUrlCounts).forEach(([url, count]) => {
              if (url) {
                  const div = document.createElement("div");
                  div.style.cssText = `
                    margin-bottom: 5px;
                  `; // 改成 cssom
                  div.textContent = `点击链接:${url} 有${count}条`;
                  const relink = url.match(/e=.*?(&|$)/)[0];
                  if (option.some(item => item.relink === relink)) {
                      div.textContent += ` 配置符合:${option.find(item => item.relink === relink).name}`;
                  } else {
                      div.textContent += " 无配置符合";
                  }
                  linkStatsContainer.appendChild(div);
              }
          });

          Object.entries(expoUrlCounts).forEach(([url, count]) => {
              if (url) {
                  const div = document.createElement("div");
                  div.style.cssText = `
                    margin-bottom: 5px;
                  `; // 改成 cssom
                  div.textContent = `曝光链接:${url} 有${count}条`;
                  const relink = url.match(/e=.*?(&|$)/)[0];
                  if (option.some(item => item.relink === relink)) {
                      div.textContent += ` 配置符合:${option.find(item => item.relink === relink).name}`;

                  } else {
                      div.textContent += " 无配置符合";
                  }
                  linkStatsContainer.appendChild(div);
              }
          });
      } else {
          Object.entries(clickUrlCounts).forEach(([url, count]) => {
              if (url) {
                  const div = document.createElement("div");
                  div.style.cssText = `
                    margin-bottom: 5px;
                  `; // 改成 cssom
                  div.textContent = `点击链接:${url} 有${count}条`;
                  div.textContent += " 无配置符合";
                  linkStatsContainer.appendChild(div);
              }
          });

          Object.entries(expoUrlCounts).forEach(([url, count]) => {
              if (url) {
                  const div = document.createElement("div");
                  div.style.cssText = `
                    margin-bottom: 5px;
                  `; // 改成 cssom
                  div.textContent = `曝光链接:${url} 有${count}条`;
                  div.textContent += " 无配置符合";
                  linkStatsContainer.appendChild(div);
              }
          });
      }

      createTable(allData);
    } catch (error) {
      console.error("解析响应数据时出错:", error);
    }
  }

  // 提取URL参数
  function extractUrlParam(url) {
    const match = url.match(/https:\/\/magellan.alimama.com\/(.*?)&/);
    return match ? match[1] : "";
  }

  // 创建表格
  function createTable(data) {
    const table = document.createElement("table");
    table.style.cssText = `
      width: 100%; 
      border-collapse: collapse; 
      background-color: white;
    `; // 改成 cssom

    // 创建表头
    const thead = document.createElement("thead");
    thead.style.cssText = `
      background-color: #0066CC;
    `; // 改成 cssom
    const headerRow = document.createElement("tr");
    const headers = ["创建时间", "创意名", "创意ID", "点击链接", "曝光链接"];
    headers.forEach((headerText) => {
      const th = document.createElement("th");
      th.style.cssText = `
        border: 1px solid #0066CC; 
        padding: 12px; 
        color: white; 
        font-weight: bold;
      `; // 改成 cssom
      th.textContent = headerText;
      headerRow.appendChild(th);
    });
    thead.appendChild(headerRow);
    table.appendChild(thead);

    // 创建表体
    const tbody = document.createElement("tbody");
    data.forEach((item, index) => {
      const row = document.createElement("tr");
      row.style.cssText = `
        background-color: ${index % 2 === 0 ? "#F5F8FA" : "white"};
      `; // 改成 cssom

      const createTime = item.creativityCreateTime;
      const creativityName = item.creativityName;
      const creativityId = item.creativityId;
      const clickUrl = item.clickUrls
        ? JSON.stringify(item.clickUrls.map(extractUrlParam)).replace(
            /,/g,
            "\n"
          )
        : "";
      const expoUrl = item.expoUrls
        ? JSON.stringify(item.expoUrls.map(extractUrlParam)).replace(/,/g, "\n")
        : "";

      const values = [
        createTime,
        creativityName,
        creativityId,
        clickUrl,
        expoUrl,
      ];
      values.forEach((value) => {
        const td = document.createElement("td");
        td.style.cssText = `
          border: 1px solid #E5E5E5; 
          padding: 12px; 
          color: #333333;
        `; // 改成 cssom
        td.textContent = value;
        row.appendChild(td);
      });
      tbody.appendChild(row);
    });
    table.appendChild(tbody);

    // 清空表格容器并插入新表格
    tableContainer.innerHTML = "";
    tableContainer.appendChild(table);
  }

  // 下载Excel
  function downloadExcel(data) {
    // 添加BOM头,解决中文乱码问题
    const BOM = "\uFEFF";
    const headers = ["创建时间", "创意名", "创意ID", "点击链接", "曝光链接"];
    const csvContent = [headers.join(",")];

    data.forEach((item) => {
      const createTime = item.creativityCreateTime || "";
      const creativityName = item.creativityName || "";
      const creativityId = item.creativityId || "";
      const clickUrl =
        item.clickUrls && item.clickUrls[0]
          ? JSON.stringify(item.clickUrls.map(extractUrlParam)).replace(
              /,/g,
              "\n"
            )
          : "";
      const expoUrl =
        item.expoUrls && item.expoUrls[0]
          ? JSON.stringify(item.expoUrls.map(extractUrlParam)).replace(
              /,/g,
              "\n"
            )
          : "";

      // 处理CSV中的特殊字符
      const escapeCsvValue = (value) => {
        if (typeof value !== "string") return value;
        if (
          value.includes(",") ||
          value.includes('"') ||
          value.includes("\n")
        ) {
          return `"${value.replace(/"/g, '""')}"`;
        }
        return value;
      };

      const values = [
        createTime,
        creativityName,
        creativityId,
        clickUrl,
        expoUrl,
      ].map(escapeCsvValue);
      csvContent.push(values.join(","));
    });

    const csv = BOM + csvContent.join("\n");
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    const timestamp = new Date().toLocaleString().replace(/[/:]/g, "-");

    link.setAttribute("href", url);
    link.setAttribute("download", `data_${timestamp}.csv`);
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  }



  function option(){
      const div = document.createElement('div');
      div.style.cssText = `
        position: absolute; 
        top: 50%; 
        left: 50%; 
        transform: translate(-50%, -50%); 
        z-index: 9999; 
        width: 500px; 
        height: 300px; 
        overflow-x: auto; 
        overflow-y: auto; 
        background: white; 
        border: 1px solid rgb(0, 102, 204); 
        border-radius: 8px;
      `; // 改成 cssom
      const table = document.createElement('table');
      table.style.cssText = `
        width: 100%; 
        border-collapse: collapse;
      `; // 改成 cssom

      const thead = document.createElement('thead');
      const tr = document.createElement('tr');
      const th1 = document.createElement('th');
      th1.textContent = '链接';
      tr.appendChild(th1);
      const th2 = document.createElement('th');
      th2.textContent = '名字';
      tr.appendChild(th2);
      thead.appendChild(tr);
      table.appendChild(thead);

      const tbody = document.createElement('tbody');
      const storedData = localStorage.getItem('data');
      if (storedData) {
          const data = JSON.parse(storedData);
          data.forEach((item) => {
              const row = document.createElement('tr');
              const td1 = document.createElement('td');
              const linkInput = document.createElement('input');
              linkInput.type = 'text';
              linkInput.value = item.link;
              td1.appendChild(linkInput);
              row.appendChild(td1);
              const td2 = document.createElement('td');
              const nameInput = document.createElement('input');
              nameInput.type = 'text';
              nameInput.value = item.name;
              td2.appendChild(nameInput);
              row.appendChild(td2);
              tbody.appendChild(row);
          });
      } else {
          const row = document.createElement('tr');
          const td1 = document.createElement('td');
          const linkInput = document.createElement('input');
          linkInput.type = 'text';
          td1.appendChild(linkInput);
          row.appendChild(td1);
          const td2 = document.createElement('td');
          const nameInput = document.createElement('input');
          nameInput.type = 'text';
          td2.appendChild(nameInput);
          row.appendChild(td2);
          tbody.appendChild(row);
      }
      table.appendChild(tbody);

      const addButton = document.createElement('button');
      addButton.textContent = '+';
      addButton.onclick = () => {
          const newRow = document.createElement('tr');
          const newTd1 = document.createElement('td');
          const newLinkInput = document.createElement('input');
          newLinkInput.type = 'text';
          newTd1.appendChild(newLinkInput);
          newRow.appendChild(newTd1);
          const newTd2 = document.createElement('td');
          const newNameInput = document.createElement('input');
          newNameInput.type = 'text';
          newTd2.appendChild(newNameInput);
          newRow.appendChild(newTd2);
          if (tbody.children.length > 0) {
              tbody.insertBefore(newRow, tbody.children[0]);
          } else {
              tbody.appendChild(newRow);
          }
      };
      table.appendChild(addButton);

      const saveButton = document.createElement('button');
      saveButton.textContent = '保存';
      saveButton.onclick = () => {
          const rows = tbody.rows;
          const data = [];
          for (let i = 0; i < rows.length; i++) {
              const link = rows[i].cells[0].children[0].value;
              const name = rows[i].cells[1].children[0].value;
              data.push({ link, name });
          }
          localStorage.setItem('data', JSON.stringify(data));
          div.style.display = 'none'; // 点击保存后隐藏当前元素
      };
      table.appendChild(saveButton);

      div.appendChild(table);
      document.body.appendChild(div);
  }
  //======================改名
  // 创建打开卡片的按钮
  const openButton = document.createElement('button');
  addButtonAttributes(openButton, "修改单元内计划字符");

  // 为打开按钮添加点击事件监听器
  openButton.addEventListener('click', function () {
    const sheet = new CSSStyleSheet();
    sheet.replaceSync(`
      #overlay {
        position: fixed;
        top: 50%;
        left: 50%;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.5);
        z-index: 999;
        transform: translate(-50%, -50%);
      }
      #card {
        position: fixed;
        top: 50%;
        left: 50%;
        width: 500px; /* 设置卡片的宽度 */
        height: 300px; /* 设置卡片的高度 */
        background-color: white;
        border: 1px solid #ccc;
        padding: 20px;
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        z-index: 2147483647;
        transform: translate(-50%, -50%);
      }
    `);
    const allSheets = [...document.adoptedStyleSheets, sheet];
    document.adoptedStyleSheets=allSheets
    // 创建遮罩层
    const overlay = document.createElement('div');
    overlay.id = 'overlay';

    // 创建卡片
    const card = document.createElement('div');
    card.id = 'card';

    // 创建关闭按钮
    const closeButton = document.createElement('span');
    closeButton.textContent = 'x';
    closeButton.style.position = 'absolute';
    closeButton.style.top = '10px';
    closeButton.style.right = '10px';
    closeButton.style.cursor = 'pointer';
    closeButton.addEventListener('click', function () {
        document.body.removeChild(overlay);
        document.body.removeChild(card);
    });

    // 创建第一个输入框及其标签
    const oldCharLabel = document.createElement('label');
    oldCharLabel.textContent = '旧字符';
    const oldCharInput = document.createElement('input');
    oldCharInput.type = 'text';

    // 创建第二个输入框及其标签
    const newCharLabel = document.createElement('label');
    newCharLabel.textContent = '新字符';
    const newCharInput = document.createElement('input');
    newCharInput.type = 'text';

    // 创建替换按钮
    const replaceButton = document.createElement('button');
    replaceButton.textContent = '替换';
    replaceButton.addEventListener('click', async function () {
        const oldChar = oldCharInput.value;
        const newChar = newCharInput.value;
        await mainSend(oldChar, newChar)

        // 这里可以执行其他脚本,例如替换页面上的文本
        console.log(`将 "${oldChar}" 替换为 "${newChar}"`);
    });

    // 将元素添加到卡片中
    card.appendChild(closeButton);
    card.appendChild(oldCharLabel);
    card.appendChild(oldCharInput);
    card.appendChild(newCharLabel);
    card.appendChild(newCharInput);
    card.appendChild(replaceButton);

    // 将遮罩层和卡片添加到页面中
    document.body.appendChild(overlay);
    document.body.appendChild(card);
  });


async function mainSend(old1,new1) {
  const n = location.href.match(/[0-9]{1,20}/g)
  const r = await fetch("https://ad.xiaohongshu.com/api/leona/rtb/creativity/list", {
      "headers": {
        "accept": "application/json, text/plain, */*",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
        "content-type": "application/json;charset=UTF-8",
        "priority": "u=1, i",
        "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Microsoft Edge\";v=\"133\", \"Chromium\";v=\"133\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"macOS\"",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-origin",
        "x-b3-traceid": "37e8ffc9e25f798c"
      },
      "referrer": "https://ad.xiaohongshu.com/aurora/ad/manage/103584356/192658846/creativity",
      "referrerPolicy": "strict-origin-when-cross-origin",
      "body": `{\"campaignId\":${n[0]},\"unitId\":${n[1]},\"startTime\":\"2025-02-27\",\"endTime\":\"2025-02-27\",\"pageNum\":1,\"pageSize\":50}`,
      "method": "POST",
      "mode": "cors",
      "credentials": "include"
    });

    const j = await r.json()
    const total = j.data.list.length;
    let completed = 0;
    const sheet = new CSSStyleSheet();
    sheet.replaceSync(`
      #progressBar {
        width: 0;
        height: 20px;
        background-color: #54FF9F;
        position: fixed;
        bottom: 10px;
        left: 50%;
        transform: translateX(-50%);
        z-index: 2147483647;
      }
    `);
    addStyleSheet(sheet)
    // document.adoptedStyleSheets = [sheet];
    const progressBar = document.createElement('div');
    progressBar.id = 'progressBar';
    progressBar.innerText = "进度";
    document.body.appendChild(progressBar);

    for (let index = 0; index < j.data.list.length; index++) {
      const element = j.data.list[index];
      const oldName = element.creativityName
      const regex = new RegExp(old1, 'g');
      const newName = oldName.replace(regex,new1)
      await reanmeSend(element.creativityId,newName)
      completed++;
      const progress = (completed / total) * 100;
      progressBar.style.width = `${progress}%`;
    }

    document.body.removeChild(progressBar);
    alert('完成');
}
async function reanmeSend(id,name) {
return  fetch("https://ad.xiaohongshu.com/api/leona/rtb/creativity/batch/update/name", {
      "headers": {
        "accept": "application/json, text/plain, */*",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
        "content-type": "application/json;charset=UTF-8",
        "priority": "u=1, i",
        "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Microsoft Edge\";v=\"133\", \"Chromium\";v=\"133\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"macOS\"",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-origin",
        "x-b3-traceid": "dee287ce9b6526cc"
      },
      //"referrer": "https://ad.xiaohongshu.com/aurora/ad/manage/103596579/192712174/creativity",
      "referrerPolicy": "strict-origin-when-cross-origin",
      "body": `{\"creativityId\":${id},\"creativityName\":\"${name}\"}`,
      "method": "POST",
      "mode": "cors",
      "credentials": "include"
    });
}

  //======================


  // 创建配置按钮
  const optionButton = document.createElement("button");
    addButtonAttributes(optionButton,"配置")

  optionButton.onclick = option;
  buttonContainer.appendChild(optionButton);
buttonContainer.appendChild(openButton);
})();