Metrics Extractor for Xiaohongshu Note Manager

在小红书创作者后台的笔记管理页面提取并展示指标数据(浏览量、评论数、点赞数、收藏数、转发数)。

当前为 2024-12-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Metrics Extractor for Xiaohongshu Note Manager
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.7
  5. // @description 在小红书创作者后台的笔记管理页面提取并展示指标数据(浏览量、评论数、点赞数、收藏数、转发数)。
  6. // @author 您的名字
  7. // @match https://creator.xiaohongshu.com/new/note-manager*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // 防止脚本多次运行
  15. if (document.getElementById('metrics-extractor-container')) {
  16. console.log('Metrics Extractor 已经运行,退出脚本。');
  17. return;
  18. }
  19.  
  20. const metrics = ['浏览量', '评论数', '点赞数', '收藏数', '转发数'];
  21. let categorizedData = [];
  22. let totals = {
  23. '浏览量': 0,
  24. '评论数': 0,
  25. '点赞数': 0,
  26. '收藏数': 0,
  27. '转发数': 0
  28. };
  29.  
  30. /**
  31. * 提取指标数据
  32. * @returns {boolean} 是否成功提取数据
  33. */
  34. function extractMetrics() {
  35. console.log('开始提取指标数据...');
  36. // 使用更通用的选择器
  37. const iconDivs = document.querySelectorAll('div.icon, div[class*="icon"]');
  38. console.log('找到的div.icon数量:', iconDivs.length);
  39. const numbers = [];
  40.  
  41. iconDivs.forEach(div => {
  42. const span = div.querySelector('span');
  43. if (span) {
  44. const numberText = span.textContent.trim();
  45. console.log(`提取到的span文本: "${numberText}"`);
  46. // 移除非数字字符(如逗号、空格等)
  47. const cleanedNumber = numberText.replace(/[^\d]/g, '');
  48. const number = parseInt(cleanedNumber, 10);
  49. if (!isNaN(number)) {
  50. numbers.push(number);
  51. } else {
  52. console.warn(`无法解析数字: "${numberText}"`);
  53. }
  54. } else {
  55. console.warn('未找到span元素');
  56. }
  57. });
  58.  
  59. console.log('提取到的数字:', numbers);
  60.  
  61. if (numbers.length === 0) {
  62. console.warn('未提取到任何数字。');
  63. return false;
  64. }
  65.  
  66. if (numbers.length % 5 !== 0) {
  67. console.warn('提取的数字数量不是5的倍数,可能存在数据缺失或多余。');
  68. }
  69.  
  70. categorizedData = [];
  71. for (let i = 0; i < numbers.length; i += 5) {
  72. const item = {};
  73. metrics.forEach((metric, index) => {
  74. if (i + index < numbers.length) {
  75. item[metric] = numbers[i + index];
  76. } else {
  77. item[metric] = null;
  78. }
  79. });
  80. categorizedData.push(item);
  81. }
  82.  
  83. console.log('分类后的数据:', categorizedData);
  84.  
  85. // 计算总数
  86. totals = {
  87. '浏览量': 0,
  88. '评论数': 0,
  89. '点赞数': 0,
  90. '收藏数': 0,
  91. '转发数': 0
  92. };
  93. categorizedData.forEach(item => {
  94. metrics.forEach(metric => {
  95. const value = item[metric];
  96. if (typeof value === 'number') {
  97. totals[metric] += value;
  98. }
  99. });
  100. });
  101.  
  102. console.log('各指标总数:', totals);
  103.  
  104. // 创建或更新面板
  105. if (document.getElementById('metrics-extractor-container')) {
  106. updateMetricsPanel();
  107. } else {
  108. createMetricsPanel();
  109. }
  110.  
  111. return true;
  112. }
  113.  
  114. /**
  115. * 创建指标展示面板
  116. */
  117. function createMetricsPanel() {
  118. console.log('创建指标面板...');
  119. // 创建样式
  120. const style = document.createElement('style');
  121. style.innerHTML = `
  122. #metrics-extractor-container {
  123. position: fixed;
  124. top: 20px;
  125. right: 20px;
  126. width: 320px;
  127. background: rgba(255, 255, 255, 0.95);
  128. border: 1px solid #ccc;
  129. border-radius: 8px;
  130. box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  131. padding: 15px;
  132. z-index: 10000;
  133. font-family: Arial, sans-serif;
  134. max-height: 90vh;
  135. overflow-y: auto;
  136. }
  137. #metrics-extractor-container h2 {
  138. text-align: center;
  139. margin-top: 0;
  140. font-size: 18px;
  141. }
  142. #metrics-extractor-container table {
  143. width: 100%;
  144. border-collapse: collapse;
  145. margin-bottom: 15px;
  146. }
  147. #metrics-extractor-container th, #metrics-extractor-container td {
  148. border: 1px solid #ddd;
  149. padding: 8px;
  150. text-align: center;
  151. font-size: 14px;
  152. }
  153. #metrics-extractor-container th {
  154. background-color: #f2f2f2;
  155. }
  156. #metrics-extractor-container button {
  157. width: 100%;
  158. padding: 10px;
  159. background-color: #4CAF50;
  160. color: white;
  161. border: none;
  162. border-radius: 4px;
  163. cursor: pointer;
  164. font-size: 14px;
  165. margin-bottom: 10px;
  166. }
  167. #metrics-extractor-container button:hover {
  168. background-color: #45a049;
  169. }
  170. #metrics-extractor-details {
  171. display: none;
  172. max-height: 300px;
  173. overflow-y: auto;
  174. }
  175. `;
  176. document.head.appendChild(style);
  177.  
  178. // 创建容器
  179. const container = document.createElement('div');
  180. container.id = 'metrics-extractor-container';
  181.  
  182. // 创建标题
  183. const title = document.createElement('h2');
  184. title.textContent = '指标总数';
  185. container.appendChild(title);
  186.  
  187. // 创建总数表格
  188. const totalsTable = document.createElement('table');
  189. const totalsThead = document.createElement('thead');
  190. const totalsHeaderRow = document.createElement('tr');
  191. const metricHeader = document.createElement('th');
  192. metricHeader.textContent = '指标';
  193. const totalHeader = document.createElement('th');
  194. totalHeader.textContent = '总数';
  195. totalsHeaderRow.appendChild(metricHeader);
  196. totalsHeaderRow.appendChild(totalHeader);
  197. totalsThead.appendChild(totalsHeaderRow);
  198. totalsTable.appendChild(totalsThead);
  199.  
  200. const totalsTbody = document.createElement('tbody');
  201. for (const [metric, total] of Object.entries(totals)) {
  202. const row = document.createElement('tr');
  203. const metricCell = document.createElement('td');
  204. metricCell.textContent = metric;
  205. const totalCell = document.createElement('td');
  206. totalCell.textContent = total;
  207. row.appendChild(metricCell);
  208. row.appendChild(totalCell);
  209. totalsTbody.appendChild(row);
  210. }
  211. totalsTable.appendChild(totalsTbody);
  212. container.appendChild(totalsTable);
  213.  
  214. // 创建按钮
  215. const toggleButton = document.createElement('button');
  216. toggleButton.id = 'metrics-extractor-toggle';
  217. toggleButton.textContent = '显示详细数据';
  218. container.appendChild(toggleButton);
  219.  
  220. // 创建详细数据部分
  221. const detailsSection = document.createElement('div');
  222. detailsSection.id = 'metrics-extractor-details';
  223.  
  224. const detailsTitle = document.createElement('h2');
  225. detailsTitle.textContent = '详细数据';
  226. detailsSection.appendChild(detailsTitle);
  227.  
  228. const detailsTable = document.createElement('table');
  229. const detailsThead = document.createElement('thead');
  230. const detailsHeaderRow = document.createElement('tr');
  231. const projectHeader = document.createElement('th');
  232. projectHeader.textContent = '项目';
  233. detailsHeaderRow.appendChild(projectHeader);
  234. metrics.forEach(metric => {
  235. const th = document.createElement('th');
  236. th.textContent = metric;
  237. detailsHeaderRow.appendChild(th);
  238. });
  239. detailsThead.appendChild(detailsHeaderRow);
  240. detailsTable.appendChild(detailsThead);
  241.  
  242. const detailsTbody = document.createElement('tbody');
  243. categorizedData.forEach((item, index) => {
  244. const row = document.createElement('tr');
  245. const projectCell = document.createElement('td');
  246. projectCell.textContent = index + 1;
  247. row.appendChild(projectCell);
  248. metrics.forEach(metric => {
  249. const cell = document.createElement('td');
  250. cell.textContent = item[metric] !== null ? item[metric] : '-';
  251. row.appendChild(cell);
  252. });
  253. detailsTbody.appendChild(row);
  254. });
  255. detailsTable.appendChild(detailsTbody);
  256. detailsSection.appendChild(detailsTable);
  257.  
  258. container.appendChild(detailsSection);
  259. document.body.appendChild(container);
  260.  
  261. // 添加按钮点击事件
  262. toggleButton.addEventListener('click', () => {
  263. if (detailsSection.style.display === 'none' || detailsSection.style.display === '') {
  264. detailsSection.style.display = 'block';
  265. toggleButton.textContent = '隐藏详细数据';
  266. } else {
  267. detailsSection.style.display = 'none';
  268. toggleButton.textContent = '显示详细数据';
  269. }
  270. });
  271.  
  272. console.log('指标面板已创建。');
  273. }
  274.  
  275. /**
  276. * 更新指标展示面板的数据
  277. */
  278. function updateMetricsPanel() {
  279. console.log('更新指标面板数据...');
  280. const totalsTableBody = document.querySelector('#metrics-extractor-container table tbody');
  281. const detailsTableBody = document.querySelector('#metrics-extractor-details table tbody');
  282.  
  283. // 更新总数表格
  284. totalsTableBody.innerHTML = '';
  285. for (const [metric, total] of Object.entries(totals)) {
  286. const row = document.createElement('tr');
  287. const metricCell = document.createElement('td');
  288. metricCell.textContent = metric;
  289. const totalCell = document.createElement('td');
  290. totalCell.textContent = total;
  291. row.appendChild(metricCell);
  292. row.appendChild(totalCell);
  293. totalsTableBody.appendChild(row);
  294. }
  295.  
  296. // 更新详细数据表格
  297. detailsTableBody.innerHTML = '';
  298. categorizedData.forEach((item, index) => {
  299. const row = document.createElement('tr');
  300. const projectCell = document.createElement('td');
  301. projectCell.textContent = index + 1;
  302. row.appendChild(projectCell);
  303. metrics.forEach(metric => {
  304. const cell = document.createElement('td');
  305. cell.textContent = item[metric] !== null ? item[metric] : '-';
  306. row.appendChild(cell);
  307. });
  308. detailsTableBody.appendChild(row);
  309. });
  310.  
  311. console.log('指标面板数据已更新。');
  312. }
  313.  
  314. /**
  315. * 等待指定的条件满足
  316. * @param {Function} conditionFunction - 返回布尔值的函数,表示条件是否满足
  317. * @param {number} timeout - 最大等待时间(毫秒)
  318. * @param {number} interval - 检查间隔时间(毫秒)
  319. * @returns {Promise<boolean>} 条件是否在超时前满足
  320. */
  321. function waitFor(conditionFunction, timeout = 15000, interval = 500) {
  322. return new Promise((resolve) => {
  323. const startTime = Date.now();
  324. const checkCondition = () => {
  325. if (conditionFunction()) {
  326. resolve(true);
  327. } else if (Date.now() - startTime >= timeout) {
  328. resolve(false);
  329. } else {
  330. setTimeout(checkCondition, interval);
  331. }
  332. };
  333. checkCondition();
  334. });
  335. }
  336.  
  337. /**
  338. * 初始化脚本
  339. */
  340. async function init() {
  341. console.log('Metrics Extractor 脚本初始化...');
  342. // 使用更通用的选择器,等待至少2个div.icon元素存在且其span内有内容
  343. const elementsLoaded = await waitFor(() => {
  344. const icons = document.querySelectorAll('div.icon, div[class*="icon"]');
  345. if (icons.length >= 2) {
  346. return Array.from(icons).every(div => {
  347. const span = div.querySelector('span');
  348. return span && span.textContent.trim() !== '';
  349. });
  350. }
  351. return false;
  352. }, 15000, 500);
  353. if (!elementsLoaded) {
  354. console.warn('等待目标元素超时,未找到足够的div.icon元素或span内无内容。');
  355. return;
  356. }
  357.  
  358. const extractionSuccess = extractMetrics();
  359. if (!extractionSuccess) {
  360. console.warn('提取指标数据失败。');
  361. return;
  362. }
  363.  
  364. console.log('Metrics Extractor 脚本运行完毕。');
  365. }
  366.  
  367. // 运行初始化函数
  368. init();
  369.  
  370. })();