VK Chat Parser with Google Sheets Integration

Parse lists from all messages (both types in one message) and update Google Sheets

  1. // ==UserScript==
  2. // @name VK Chat Parser with Google Sheets Integration
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.36
  5. // @description Parse lists from all messages (both types in one message) and update Google Sheets
  6. // @author Grok
  7. // @match https://web.vk.me/convo/*
  8. // @grant GM_xmlhttpRequest
  9. // @connect script.google.com
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const originalList = [
  17. "Асеева Алиса", "Базарсадаева Айлана", "Бальжинимаев Лубсан", "Беина Маргарита",
  18. "Белоусова Виктория", "Белоусова Ольга", "Буянтуева Номин", "Воробьев Алексей",
  19. "Воробьева Светлана", "Воронов Артур", "Вторушин Константин", "Геращенко Ангелина",
  20. "Дашадоржиев Сандан", "Жамбалов Доржо", "Жигжитова Виктория", "Зиновьев Геннадий",
  21. "Иванов Роман", "Константинова Татьяна", "Лаухин Сергей", "Попов Роман",
  22. "Раднаева Ольга", "Распопова Мирослава", "Самбуева Сарана", "Сергеев Марк",
  23. "Славко Валентина", "Степанова Ангелина", "Цыжипова Баярма"
  24. ];
  25.  
  26. const monthNames = [
  27. "Сентябрь", "Октябрь", "Ноябрь", "Декабрь", "Январь",
  28. "Февраль", "Март", "Апрель", "Май"
  29. ];
  30.  
  31. function initializeButton() {
  32. if (document.querySelector('button#parseButton') || !document.body) return;
  33.  
  34. const button = document.createElement('button');
  35. button.id = 'parseButton';
  36. button.textContent = 'Отправить';
  37. button.style.position = 'fixed';
  38. button.style.top = '10px';
  39. button.style.right = '150px';
  40. button.style.zIndex = '9999';
  41. button.style.padding = '10px';
  42. button.style.backgroundColor = '#4CAF50';
  43. button.style.color = 'white';
  44. button.style.border = 'none';
  45. button.style.borderRadius = '5px';
  46. button.style.cursor = 'pointer';
  47. button.style.transition = 'background-color 0.3s';
  48. document.body.appendChild(button);
  49.  
  50. button.addEventListener('click', () => {
  51. button.disabled = true;
  52. button.style.backgroundColor = '#f44336';
  53. button.textContent = 'Найдено 0';
  54. parseAndUpdate(button).finally(() => {
  55. button.disabled = false;
  56. button.style.backgroundColor = '#4CAF50';
  57. button.textContent = 'Отправить';
  58. });
  59. });
  60. console.log('Кнопка инициализирована');
  61. }
  62.  
  63. document.addEventListener('DOMContentLoaded', initializeButton);
  64. setInterval(() => {
  65. if (!document.querySelector('button#parseButton') && document.body) {
  66. console.log('Периодическая инициализация кнопки');
  67. initializeButton();
  68. }
  69. }, 1000);
  70.  
  71. function findClosestMatch(inputName) {
  72. const inputLower = inputName.toLowerCase().replace(/\.\s*/g, ' ').trim();
  73. const inputParts = inputLower.split(' ');
  74. const inputSurname = inputParts[0];
  75. const inputInitial = inputParts[1] ? inputParts[1][0] : '';
  76.  
  77. return originalList.find(original => {
  78. const originalLower = original.toLowerCase().replace(/\s+/g, ' ');
  79. const originalParts = originalLower.split(' ');
  80. const originalSurname = originalParts[0];
  81. const originalInitial = originalParts[1] ? originalParts[1][0] : '';
  82.  
  83. if (originalSurname === 'воробьев' || originalSurname === 'воробьева' ||
  84. originalSurname === 'белоусова') {
  85. return originalSurname === inputSurname && originalInitial === inputInitial;
  86. } else {
  87. return originalSurname === inputSurname;
  88. }
  89. }) || inputName;
  90. }
  91.  
  92. function parseMessageText(htmlContent) {
  93. const text = htmlContent.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '').trim();
  94. const lines = text.split('\n').map(line => line.trim()).filter(line => line);
  95. if (lines.length < 2) return { absent: [], notEating: [] };
  96.  
  97. let absentLines = [];
  98. let notEatingLines = [];
  99. let currentList = null;
  100.  
  101. lines.forEach((line, index) => {
  102. const lowerLine = line.toLowerCase();
  103.  
  104. if (lowerLine.match(/^\d+\/\d+$/) && index === 0) {
  105. return;
  106. } else if (lowerLine.includes('нет')) {
  107. currentList = 'absent';
  108. } else if (lowerLine.includes('не кушают')) {
  109. currentList = 'notEating';
  110. } else if (currentList && line.trim()) {
  111. const matchedName = findClosestMatch(line);
  112. if (currentList === 'absent') {
  113. absentLines.push(matchedName);
  114. } else if (currentList === 'notEating') {
  115. notEatingLines.push(matchedName);
  116. }
  117. }
  118. });
  119.  
  120. return {
  121. absent: absentLines,
  122. notEating: notEatingLines,
  123. absentList: absentLines.length ? '\n' + absentLines.join('\n') : '',
  124. notEatingList: notEatingLines.length ? '\n' + notEatingLines.join('\n') : ''
  125. };
  126. }
  127.  
  128. function getDateFromLabel(label, currentDate) {
  129. const normalizedLabel = label.toLowerCase().trim();
  130. const date = new Date(currentDate);
  131.  
  132. if (normalizedLabel === 'сегодня') {
  133. // Используем текущую дату
  134. } else if (normalizedLabel === 'вчера') {
  135. date.setDate(date.getDate() - 1);
  136. } else {
  137. const [dayStr, monthStr] = normalizedLabel.split(' ');
  138. const day = parseInt(dayStr, 10);
  139. const monthMap = {
  140. 'января': 0, 'февраля': 1, 'марта': 2, 'апреля': 3,
  141. 'мая': 4, 'июня': 5, 'июля': 6, 'августа': 7,
  142. 'сентября': 8, 'октября': 9, 'ноября': 10, 'декабря': 11
  143. };
  144. const month = monthMap[monthStr];
  145. if (day && month !== undefined) {
  146. date.setDate(day);
  147. date.setMonth(month);
  148. if (month > date.getMonth()) {
  149. date.setFullYear(date.getFullYear() - 1);
  150. }
  151. }
  152. }
  153.  
  154. const dayStr = String(date.getDate()).padStart(2, '0');
  155. const monthStr = String(date.getMonth() + 1).padStart(2, '0');
  156. const year = date.getFullYear();
  157. return `${dayStr}.${monthStr}.${year}`;
  158. }
  159.  
  160. function updateGoogleSheets(absentList, notEatingList, dateString, sheetName) {
  161. return new Promise((resolve, reject) => {
  162. const payload = JSON.stringify({
  163. sheetName: sheetName,
  164. absent: absentList,
  165. notEating: notEatingList,
  166. date: dateString
  167. });
  168.  
  169. console.log(`Отправляемые данные для ${dateString}:`, payload);
  170.  
  171. const apiUrl = 'https://script.google.com/macros/s/AKfycbzSjufiH1WUjKPSLq_k3Un6Q-FB_n6R_dVCrzDlz4Hxg2HOkVg4llng_c-cP7gOmPeO/exec';
  172. if (typeof GM_xmlhttpRequest !== 'undefined') {
  173. GM_xmlhttpRequest({
  174. method: 'POST',
  175. url: apiUrl,
  176. headers: { 'Content-Type': 'application/json' },
  177. data: payload,
  178. onload: response => {
  179. console.log(`Ответ от API для ${dateString}:`, response.responseText);
  180. resolve();
  181. },
  182. onerror: error => {
  183. console.error(`Ошибка GM_xmlhttpRequest для ${dateString}:`, error);
  184. reject(error);
  185. }
  186. });
  187. } else {
  188. console.warn('GM_xmlhttpRequest не доступен, используем fetch');
  189. fetch(apiUrl, {
  190. method: 'POST',
  191. headers: { 'Content-Type': 'application/json' },
  192. body: payload,
  193. mode: 'no-cors'
  194. })
  195. .then(() => {
  196. console.log(`Данные отправлены через fetch для ${dateString} (no-cors)`);
  197. resolve();
  198. })
  199. .catch(error => {
  200. console.error(`Ошибка fetch для ${dateString}:`, error);
  201. reject(error);
  202. });
  203. }
  204. });
  205. }
  206.  
  207. async function parseAndUpdate(button) {
  208. const currentDate = new Date(); // Фиксируем дату для примера
  209. const dateSeparators = document.querySelectorAll('.DateSeparator');
  210. let hasLists = false;
  211.  
  212. if (!dateSeparators.length) {
  213. console.log('Блоки дат не найдены');
  214. button.textContent = 'Ошибка';
  215. button.style.backgroundColor = '#f44336';
  216. await new Promise(resolve => setTimeout(resolve, 3000)); // Ждём 3 секунды
  217. return;
  218. }
  219.  
  220. const dataByDate = {};
  221.  
  222. dateSeparators.forEach(separator => {
  223. const label = separator.textContent.trim();
  224. const dateString = getDateFromLabel(label, currentDate);
  225. const dateStack = separator.closest('.ConvoHistory__dateStack');
  226. if (!dateStack) return;
  227.  
  228. const convoStacks = dateStack.querySelectorAll('.ConvoStack');
  229. let absentList = [];
  230. let notEatingList = [];
  231.  
  232. convoStacks.forEach(stack => {
  233. const messages = stack.querySelectorAll('.ConvoMessage');
  234. messages.forEach(message => {
  235. const content = message.querySelector('.ConvoMessage__text');
  236. if (content) {
  237. const messageHtml = content.innerHTML;
  238. const result = parseMessageText(messageHtml);
  239.  
  240. if (result.absent.length && absentList.length === 0) {
  241. absentList = result.absent;
  242. console.log(`Отсутствующие (${label}):`, result.absentList);
  243. }
  244. if (result.notEating.length && notEatingList.length === 0) {
  245. notEatingList = result.notEating;
  246. console.log(`Не кушают (${label}):`, result.notEatingList);
  247. }
  248. }
  249.  
  250. const forwardedContainer = message.querySelector('.ConvoMessage__forwardedMessages');
  251. if (forwardedContainer) {
  252. const forwardedMessages = forwardedContainer.querySelectorAll('.ForwardedMessageNew__text');
  253. forwardedMessages.forEach(fwdText => {
  254. const fwdHtml = fwdText.innerHTML;
  255. const fwdResult = parseMessageText(fwdHtml);
  256.  
  257. if (fwdResult.absent.length && absentList.length === 0) {
  258. absentList = fwdResult.absent;
  259. console.log(`Отсутствующие (пересылаемое, ${label}):`, fwdResult.absentList);
  260. }
  261. if (fwdResult.notEating.length && notEatingList.length === 0) {
  262. notEatingList = fwdResult.notEating;
  263. console.log(`Не кушают (пересылаемое, ${label}):`, fwdResult.notEatingList);
  264. }
  265. });
  266. }
  267. });
  268. });
  269.  
  270. if (absentList.length || notEatingList.length) {
  271. dataByDate[dateString] = { absentList, notEatingList };
  272. hasLists = true;
  273. }
  274. });
  275.  
  276. const listCount = Object.keys(dataByDate).length; // Количество дней с данными
  277.  
  278. if (!hasLists) {
  279. console.log('Списки не найдены');
  280. button.textContent = 'Ошибка';
  281. button.style.backgroundColor = '#f44336';
  282. await new Promise(resolve => setTimeout(resolve, 3000)); // Ждём 3 секунды
  283. } else {
  284. button.textContent = `Найдено ${listCount}`;
  285. const promises = Object.entries(dataByDate).map(([dateString, { absentList, notEatingList }]) => {
  286. const date = new Date(dateString.split('.').reverse().join('-'));
  287. const monthIndex = date.getMonth();
  288. const adjustedIndex = (monthIndex + 4) % 12; // Сдвиг для учебного года
  289. const sheetName = monthNames[adjustedIndex];
  290.  
  291. console.log(`=== Результаты для ${dateString} ===`);
  292. console.log('Отсутствующие:', absentList.length ? '\n' + absentList.join('\n') : 'Список не найден');
  293. console.log('Не кушают:', notEatingList.length ? '\n' + notEatingList.join('\n') : 'Список не найден');
  294.  
  295. return updateGoogleSheets(absentList, notEatingList, dateString, sheetName);
  296. });
  297.  
  298. await Promise.all(promises);
  299. }
  300. }
  301.  
  302. initializeButton();
  303. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址