Русский
Русский
English
Статистика
Реклама

Google apps script

Google документы станут полновесными с 1 июня. Пишем скрипт для обхода этого ограничения

27.05.2021 18:04:31 | Автор: admin

Предыстория

Google изменяет политику хранения данных с 1 июня 2021 года. Вкратце: документы и фото теперь станут полновесными и будут учитываться в общей квоте 15Гб. К тому же, при неактивности аккаунта более двух лет, Google может удалить ваши данные.

Я часто работаю с Google документами, и при активном использовании дисковая квота закончится довольно быстро. Но есть и хорошая новость: документы, созданные до 1 июня 2021 года так и останутся невесомыми, поэтому вы не получите превышение квоты в одночасье.

У меня сразу возникла мысль сделать документов "в запас". Ниже я расскажу, как это можно осуществить, не тратя много времени и сил.

Пишем скрипт, создающий документы

Скрипт буду писать с помощью Google Apps Script.

Создаём новую таблицу Google, заходим в редактор скриптов (Инструменты - Редактор скриптов).

Создаём три файла

  • main.gs - основной код для создания файлов

  • menu.gs - код для создания пользовательского меню при открытии таблицы

  • index.html - шаблон страницы для отображения информации

Сначала в файле menu.gs создаём функцию onOpen(). Это простой триггер, который выполняется при открытии таблицы. Его задача - создать пользовательское меню, из которого можно запустить функцию.

function onOpen(e) { // Выполняется при открытии таблицы  SpreadsheetApp.getUi() // Получаем интерфейс пользователя      .createMenu('Меню')// Создаём меню      .addItem('Создать документы', 'main') // Создаём команду меню      .addToUi();// Добавляем меню в интерфейс}

В файле main.gs создадим функцию main(), которая и будет запускаться с кнопки в меню.

function main() { // Меню - Создать документы  // Создаём HTML документ из шаблона  let template = HtmlService.createTemplateFromFile(`index`);  // Показываем модальное окно с HTML   SpreadsheetApp.getUi()    .showModelessDialog(template.evaluate(),`Создаю документы...`);}

Пора разобраться с index.html. это обычный HTML файл, который отобразится в модальном окне. там можно использовать стили, скрипты и т.п.

index.html
<!doctype html><html lang="en">  <head>    <!-- Required meta tags -->    <meta charset="utf-8">    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">    <!-- Bootstrap CSS -->    <link rel="stylesheet" href="http://personeltest.ru/aways/maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">    <script>      let timer = function(){ // Обслуживает таймер        // Получаем текущее время        let now = (new Date()).getTime();                // Задаём режим обновления таймера - 1 раз в секунду        setInterval(function(){          // Получаем прошедшее время. now будет доступно из-за замыкания.          let time = (new Date()).getTime() - now;          // Вычисляем минуты          let minutes = Math.floor(time/60000);          // И секунды          let seconds = Math.floor(time%60000/1000);          // Обновляем данные в поле lifeTime          updateData("lifeTime", `${minutes<10?"0"+minutes:minutes}:${seconds<10?"0"+seconds:seconds}`);        },1000);      };            updateData = function(id, value){ // Обновляет информацию в одном элементе по id        let element = document.getElementById(id);        if (element) {          element.innerHTML = value;        };      };            refreshData = function(){ // Обновляем все данные        google.script.run.withSuccessHandler(function(data){        updateData("createTables", data.createTables); // Таблиц создано:        updateData("createDocs", data.createDocs);// Документов создано:        updateData("createForms", data.createForms);// Форм создано        updateData("createSlides", data.createSlides);// Презентаций создано        updateData("log", data.log);// Лог      }).getData(); // Получаем данные     };      setTimeout(setInterval(refreshData, 2000),1000); // Данные обновляем раз в 2 секунды      timer();// Запуск таймера      google.script.run.doMagic();// Запуск функции для создания документов    </script>  </head>  <body>    <div class=".container bg-dark text-white text-center">      <div class="row">        <div class="col">          Таблиц        </div>                <div class="col">          Документов        </div>                <div class="col">          Форм        </div>                <div class="col">          Презентаций        </div>      </div>      <div class="row">        <div class="col" id="createTables">          0        </div>        <div class="col" id="createDocs">          0        </div>                <div class="col" id="createForms">          0        </div>                <div class="col" id="createSlides">          0        </div>      </div>    </div>         <div class=".container bg-dark text-white">      <div class="row">        <div class="col text-right" id="label_lifeTime">          Время работы:          </div>                <div class="col  text-left" id="lifeTime">          00:00        </div>              </div>    </div>    <div bg-dark text-white id="label_log">Лог: </div>    <ul class="list-group" id="log">    </ul>  </body></html>

В ней подключаем стили, создаём скрипт, в котором три функции:

  • updateData(id, value) - ищет на странице элемент по id и обновляет содержимое

  • refreshData() - обновляет все данные на странице, кроме таймера

  • timer() - обслуживает таймер

Для создания документов в файле main.gs создаём универсальную функцию.

function create()
const FILES_TO_CREATE = 50;function create(filesToCreate = FILES_TO_CREATE, folderId, prefix="file_", app, key) {  // Получаем словарь ключ-значение для текущего скрипта.  let props = PropertiesService.getScriptProperties();  // Получаем значение для ключа data. Преобразуем в объект  let data = JSON.parse(props.getProperty(`data`));  try{    // Получаем директорию по id    let folder = DriveApp.getFolderById(folderId);    for(var i=0; i<filesToCreate; i++){ // Создаём filesToCreate документов      // Создаём документ, получаем его id      let ssId = app.create(`${prefix}${(new Date()).getTime()}`).getId();      // Получаем файл по его id      let ss = DriveApp.getFileById(ssId);      // Копируем файл в папку      folder.addFile(ss);      // Удаляем файл из корневой папки      DriveApp.getRootFolder().removeFile(ss);      // Увеличиваем счётчик созданных файлов      data[key]=1+data[key];      // Сохраняем новые данные      props.setProperty(`data`, JSON.stringify(data));    };  }catch(err){    // В случае ошибки - пишем её в лог    logToHtml(`Error: ${err}`, LOG_TYPES.danger);  };  // Возвращаем из функции количество созданных файлов  return +i;};

И конкретные реализации - для таблиц, документов, форм и презентаций.

Обёртки для функции create()
function createSheets(key) {  // Получаем id папки для таблиц  let folderId =  PropertiesService.getScriptProperties().getProperty(`sheetsFolder`);  // Создаём нужное количество таблиц  let count = create(FILES_TO_CREATE, folderId, `sheet_`, SpreadsheetApp, key);  // Сохраняем в лог  logToHtml(`${count} sheets were created`, LOG_TYPES.success);}function createDocs(key) {  let folderId =  PropertiesService.getScriptProperties().getProperty(`docsFolder`);  let count = create(FILES_TO_CREATE, folderId, `doc_`, DocumentApp, key);  logToHtml(`${count} docs were created`, LOG_TYPES.success);}function createForms(key) {  let folderId =  PropertiesService.getScriptProperties().getProperty(`formsFolder`);  let count = create(FILES_TO_CREATE, folderId, `form_`, FormApp, key);  logToHtml(`${count} forms were created`, LOG_TYPES.success);}function createSlides(key) {  let folderId =  PropertiesService.getScriptProperties().getProperty(`slidesFolder`);  let count = create(FILES_TO_CREATE, folderId, `slide_`, SlidesApp, key);  logToHtml(`${count} slides were created`, LOG_TYPES.success);}

Далее делаем функцию для создания папок.

function createFolders(){  // Получаем словарь ключ-значение для текущего скрипта  let props = PropertiesService.getScriptProperties();  // Задаём структуру папок  let folders = [    {key:`rootFolder`,   name:`Прозапас`                         },    {key:`sheetsFolder`, name:`Sheets`, parentFolder:`rootFolder`},    {key:`docsFolder`,   name:`Docs`,   parentFolder:`rootFolder`},    {key:`formsFolder`,  name:`Forms`,  parentFolder:`rootFolder`},    {key:`slidesFolder`, name:`Slides`, parentFolder:`rootFolder`},    ];  // Проходим по структуре и создаём папки    folders.forEach(folder=>{      if (!props.getProperty(folder.key)){ // Если папка ещё не создана        // Если есть параметр rootFolder, то используем его, иначе выбираем корневую папку        let parentFolder = folder.parentFolder?DriveApp.getFolderById(props.getProperty(folder.parentFolder)):DriveApp.getRootFolder();        // Создаём папку        let folderId = parentFolder.createFolder(folder.name).getId();        // Сохраняем информацию о ней        props.setProperty(folder.key, folderId);      };    });}

И функцию для создания всех типов документов:

Основная функция, которая запускает создание папок и документов
function doMagic(){  let props = PropertiesService.getScriptProperties();  // Структура данных  let data = {    createTables:0,     createDocs:0,     createForms:0,     createSlides:0,     startTime:new Date(),    log:``,  };  // Сохраняем данные в словарь скрипта  props.setProperty(`data`, JSON.stringify(data));    try{    createFolders(); // Создаём папки    createSheets(`createTables`);// Создаём таблицы    createDocs(`createDocs`);// Создаём документы    createForms(`createForms`);// Создаём формы    createSlides(`createSlides`);// Создаём презентации        // Сообщаем, что всё готово    SpreadsheetApp.getUi().alert(`Готово!`);  }catch(err){    // При ошибке сообщаем об этом    SpreadsheetApp.getUi().alert(`Ошибка! ${err}`);  };};

Остаётся создать функцию для получения данных - так модальное окно может получить актуальные данные для отображения.

function getData(){ // Получает данные из словаря  // Получаем словарь ключ-значение  let props = PropertiesService.getScriptProperties();  // Получаем нужные данные и преобразуем в объект  let data = JSON.parse(props.getProperty(`data`));  return data; //Возвращаем данные};

И функция для записи строки в лог:

const LOG_TYPES = { // Типы сообщений. Взято из bootstrap  primary:   "primary",  secondary: "secondary",  success:   "success",  danger:    "danger",  warning:   "warning",  info:      "info",  };function logToHtml(log, type = LOG_TYPES.primary){  // Получаем данные  let data = getData();  // Добавляем li тег (лог отображается в виде списка) с данными  data.log+=`<li class="list-group-item text-${type}">${log}</li>\n`;  // Сохраняем данные  let props = PropertiesService.getScriptProperties();  props.setProperty(`data`, JSON.stringify(data));};

Что получилось

Остаётся только запустить скрипт из меню. При первом запуске Google запросит права - это нормально. После этого откроется окно, в котором можно наблюдать за прогрессом.

Модальное окно для визуализации прогрессаМодальное окно для визуализации прогресса

Ограничения скрипта

  • Время жизни скрипта ограничено 6 минутами. За это время он успеет создать несколько сотен документов. Можно обойти это ограничение, закинув все функции непосредственно в HTML код модального окна и обращаться к диску по API, но об этом в следующий раз

  • Есть ограничение на количество ежедневно созданных документов. Рано или поздно будет появляться ошибка Exception: Служба была вызвана слишком много раз за день: docs create. Тогда скрипт можно запустить на следующий день


TL;DR

Всё вышеописанное я собрал в таблицу, которую можно скопировать себе(Файл - Создать копию), запустить(Меню - Создать файлы) и получить к себе на диск несколько сотен файлов. При необходимости процедуру повторить.

Спасибо за внимание. Буду рад получить фидбэк по коду. Удачи!

Подробнее..

Экстренная психологическая помощь Prototyping Weekend

05.09.2020 16:05:22 | Автор: admin

#openDevelopment #codeSaveLives
Привет Хабр! Я завершил работу над прототипом платформы, которая объединяет психологов-добровольцев и людей, нуждающихся в экстренной помощи. Это инициатива в ответ на насилие, происходящее в настоящее время в Беларуси и Ливане:
https://brmlab.cz/project/belhack/start

Я попытался запрототипировать веб-приложение для удаленной неотложной психологической помощи. Меня мотивировало к этому проекту то, что сегодня многие тысячи людей оказались в безвыходном положении и подверглись насилию, по моему мнению они нуждаются в поддержке.

Это может быть не только психологическая помощь, но и онлайн-инструкции о том, как поступить с пострадавшим. Пользователь этого приложения - человек, которому немедленно нужна неотложная помощь, и мед-волонтер, кто готов помощь оказать. Это расширение концепции Online Doctor на экстренную помощь.

Презентация механики:

*** Технические подробности ***

Я решил использовать максимально доступные инструменты, которые позволили бы мне создать прототип приложения с минимальными ресурсами.

#googleMeet #googleSpreadSheet #googleAppsScript #googleChromeExtension

Для создания прототипа я использовал технологии и сервисы Google: электронную таблицу, встречу, расширение, apps script.

На странице приложения отображаются психологи, которые готовы оказать помощь прямо сейчас.

Как пациент: Нажав на кнопку, я присоединюсь к встрече, после чего смогу получить поддержку от свободного в данную минуту специалиста - волонтера.

Как психолог: Я готов начать прием людей, нажимаю кнопку Ready to help. Мне нужно нажать кнопку Help in Progress (используя расширения), когда кто-то присоединяется, и статус в таблице этого доктора меняется на 1. Затем строка с врачом исчезает со страницы.

Общение происходит в среде Google Meet.

Буду рад новым людям, которые готовы помочь провести тестирования, отловить жуков и помочь спуску в продакшен.

Подробнее..

Конструктор плейлистов для Spotify

17.01.2021 16:22:22 | Автор: admin

Статья о том, как Spotify Web API (SWA) и платформа Google Apps Script (GAS) позволили превратить библиотеку в гибкий конструктор плейлистов с бесплатным исполнением по расписанию.

Под конструктором плейлистов понимаем механизм сбора, фильтрации, сортировки и добавления треков.

Ссылки на исходный код и документацию в конце статьи.

Проблема

Наличие официального API породило для Spotify множество интересных инструментов. В нашем контексте рассмотрим Smarter Playlists. Графический интерфейс в виде графов, множество управляющих блоков делают его довольно дружелюбным для обычного пользователя.

Плотное использование сервиса выявило ряд недостатков: отсутствие истории прослушиваний, лимитированное добавление треков, ограниченное количество запусков по расписанию, некорректная работа с кириллицей. С течением времени сюда добавляется желание новых источников треков, более сложных алгоритмов фильтрации. Сервис перестал развиваться с точки зрения новых функций.

В связи с этим появилось желание альтернативы, устраняющей данные недостатки.

Решение

В результате была разработана библиотека для GAS на JavaScript - Goofy. Конструктором её делает возможность запуска на платформе в редакторе или по расписанию (триггерам) без стороннего ПО.

Естественно GAS накладывает свои ограничения, но другого порядка. Например, время исполнения не более 6 минут; количество запросов к внешним API не более 20 тысяч в день. С другой стороны, круг возможностей существенно расширился. Реализованы все функции Smarter Playlists, устранены его недостатки. Появились новые функции.

Алгоритм

Несколько простых примеров, которые можно реализовать с помощью Goofy

  • Объединение личных миксов с удалением ранее прослушанных треков

  • Выявление давно непрослушиваемых треков

  • Сбор новинок отслеживаемых исполнителей

  • Накопление треков обновляемого источника в плейлисте с удалением уже прослушанных

  • Рекомендации Last.fm любого пользователя, топ за период и прочее

Новые возможности

Особое внимание уделю истории прослушиваний. Частое прослушивание музыки без особо активных действий приводят стриминг платформы к повторам. Одни и те же треки приходят изо дня в день. Goofy позволяет удалить ранее прослушанные треки. Однако это сложнее, чем казалось на первых взгляд.

  • SWA дает возможность получить только 50 последних прослушанных треков. Появление 51-ого делает недоступным 1-й.

    GAS имеет выход к Google Drive. Библиотека создает повторяющийся триггер и опрашивает SWA на предмет новых прослушиваний. Если они есть, сохраняет в файл. Таким образом, появляется локальная полная история прослушиваний.

  • Есть другая проблема. Трек считается прослушанным только после 30 секунд. Причем зачастую недавние треки обновляются не сразу или вовсе теряются. Списываю это на "качество" SWA.

    Решение есть и здесь - использовать Last.fm. Реализовано два подхода. Во первых, можно просто запросить недавние прослушивания и удалять совпадения по названию трека. Во вторых, можно выполнить поиск трека в Spotify и сохранить его в файл на Google Drive. Тем самым получая историю прослушиваний от Last.fm готовую к работе со Spotify.

Кроме того, Last.fm API используется и для получения других источников. Например, рекомендации, треки соседей по музыкальным вкусам, любимые треки, топ за разный период.

Пример

Нужно выполнить установку (копирование в GAS) и настройку (выдача разрешений, ключи доступа). Все подробно есть в документации.

1) Простой пример: получить любимые треки, взять пять случайных и добавить в плейлист.

Далее графическими средствами GAS создаем триггер. В результате, например, раз в минуту плейлист будет обновляться пятью случайными треками.

function createHelloPlaylist() {    let tracks = Source.getSavedTracks();    Selector.keepRandom(tracks, 5);    Playlist.saveWithReplace({        name: 'Hello, playlist',        tracks: tracks,    });}

2) Выявить любимые треки, которые не были прослушаны более чем месяц.

function templateSavedAndForgot(){    let recentTracks = Source.getRecentTracks(2500);    let savedTracks = Source.getSavedTracks();     Filter.removeTracks(savedTracks, recentTracks);    let startDate = new Date('2006-01-01');    let endDate = Filter.getDateRel(30, 'endDay');    Filter.rangeDateAbs(savedTracks, startDate, endDate);    Selector.keepRandom(savedTracks, 20);        Order.sort(savedTracks, 'meta.added_at', 'asc');    Playlist.saveWithReplace({        name: 'Любимо и забыто',        tracks: savedTracks,        randomCover: 'update', // установить случайную обложку плейлиста    });}

3) Получить треки альбомов и синглов отслеживаемых исполнителей за неделю.

function createNewRelease() {    const playlistId = 'abc';    let tracks = Source.getArtistsTracks({        artist: {           followed_include: true         },        album: {           groups: 'album,single',           release_date: { sinceDays: 7, beforeDays: 0 }         },    });    Order.shuffle(tracks);    Combiner.push(tracks, Source.getPlaylistTracks('name', playlistId));    Filter.removeTracks(tracks, RecentTracks.get(3000));    Filter.matchOriginalOnly(tracks);        Playlist.saveWithReplace({        id: playlistId,        name: 'Новые релизы',        tracks: tracks,        randomCover: 'update',    });}

Ссылки

Документация писалась с ориентиром на пользователя, который не знаком с программированием. Чтобы дать возможность использовать Goofy через копипаст шаблонов и примеров.

Исходный код на GitHub, включая форум.

Подробнее..
Категории: Javascript , Api , Музыка , Spotify , Google apps script , Last.fm , Gas

Игровая статика, или как я перестал бояться и полюбил Google Apps Script

07.08.2020 14:13:35 | Автор: admin


Приветствую! Сегодня хотелось бы поговорить на одну тему, с которой так или иначе сталкивается любой гейм-дизайнер. И эта тема боль и страдания работа со статикой. Что такое статика? Если вкратце это все постоянные данные, с которыми взаимодействует игрок, будь то характеристики его оружия или параметры подземелья и его обитателей.

Представьте, у вас в игре есть 100500 видов различных мечей и им всем внезапно потребовалось немного поднять базовый урон. Обычно, в таком случае запрягается старый добрый эксель, а результаты затем вставляются в JSON/XML руками или при помощи регулярок, но это долго, хлопотно и чревато ошибками валидации.

Давайте посмотрим, как для таких целей может подходить Google Spreadsheets и встроенный в него Google Apps Script и можно ли на этом сэкономить время.

Заранее оговорюсь, что речь идет о статике для f2p-игр или игр-сервисов, которым свойственны регулярные обновления механик и пополнение контента, т.е. указанный выше процесс постоянен.

Итак, для редактирования тех же мечей вам потребуется выполнить три операции:
  1. извлечь текущие показатели урона (если у вас нет готовых расчетных таблиц);
  2. рассчитать обновленные значения в старом добром экселе;
  3. перенести новые значения в игровые JSON-ы.

До тех пор, пока у вас есть готовый инструмент и он вас устраивает, все хорошо и можно редактировать так, как привыкли. Но что, если инструмента нет? Или еще хуже, нет самой игры, т.к. она еще только в разработке? В таком случае, помимо редактирования существующих данных вам также требуется решить, где их хранить и какая у них будет структура.

С хранением все еще более-менее понятно и стандартизировано: в большинстве случаев статика это просто набор отдельных JSON-ов, лежащий где-то в VCS. Бывают, конечно, более экзотические случаи, когда все хранится в реляционной (или не очень) базе, или, что самое страшное, в XML. Но, если вы выбрали их, а не обычный JSON, то скорее всего у вас уже есть весомые на то основания, т.к. производительность и удобство использования этих вариантов весьма сомнительны.

А вот что касается структуры статики и ее редактирования изменения будут зачастую радикальные и ежедневные. Конечно, в некоторых ситуациях ничто не заменит по эффективности обычный Notepad++ вкупе с регулярками, но нам все же хочется иметь инструмент с более низким порогом входа и удобством для редактирования командой.

В качестве подобного инструмента лично мне подошел банальный и многим известный Google Spreadsheets. Как и у любого инструмента, у него есть свои плюсы и минусы. Попробую рассмотреть их с точки зрения ГД.

Плюсы Минусы
  • Совместное редактирование
  • Удобно переносить расчеты из других спредшитов
  • Макросы (Google Apps Script)
  • Есть история редактирования (вплоть до ячейки)
  • Родная интеграция с Google Drive и прочими сервисами

  • Лагает при большом количестве формул
  • Нельзя создавать отдельные ветки изменений
  • Лимит времени отработки скриптов (6 минут)
  • Сложность в отображении nested JSON-ов


Для меня плюсы значительно перевешивали минусы, и в связи с этим было решено попробовать найти обходной путь по каждому из представленных минусов.

Что получилось в итоге?


В Google Spreadsheets сделан отдельный документ, в котором есть лист Main, где мы управляем выгрузкой, и остальные листы, по одному на каждый игровой объект.
При этом, чтобы привычный nested JSON уложить в плоскую таблицу, пришлось немного переизобрести велосипед. Допустим, мы имели следующий JSON:

{  "test_craft_01": {    "id": "test_craft_01",    "tags": [ "base" ],"price": [ {"ident": "wood", "count":100}, {"ident": "iron", "count":30} ],"result": {"type": "item","id": "sword","rarity_wgt": { "common": 100, "uncommon": 300 }}  },  "test_craft_02": {    "id": "test_craft_02","price": [ {"ident": "sword", "rarity": "uncommon", "count":1} ],"result": {"type": "item","id": "shield","rarity_wgt": { "common": 100 }}  }}


В таблицах такую структуру можно представить как пару значений полный путь значение. Отсюда родился самопальный язык разметки пути, в котором:

  • text это поле или объект
  • / разделитель иерархии
  • text[] массив
  • #number индекс элемента в массиве


Таким образом, в таблицу JSON будет записан следующим образом:


Соответственно, добавление нового объекта такого типа это еще один столбец в таблице и, если у объекта были какие-то особые поля, то расширение списка строк с ключами в keypath.

Разделение на root и остальные уровни это дополнительное удобство в целях использования фильтров в таблице. В остальном работает простое правило: если значение в объекте не пустое, то мы его добавим в JSON и выгрузим.
На случай, если же в JSON будут добавляться новые поля и кто-то ошибется в пути он проверяется следующей регуляркой на уровне условного форматирования:
=if( LEN( REGEXREPLACE(your_cell_name, "^[a-zA_Z0-9_]+(\[\])*(\/[a-zA_Z0-9_]+(\[\])*|\/\#*[0-9]+(\[\])*)*", ""))>0, true, false)

А теперь о том, как происходит выгрузка. Для этого необходимо перейти на лист Main, выбрать желаемые объекты для выгрузки в столбце #ACTION и
нажать на Палпатина ( )


В результате будет запущен скрипт, который возьмет данные с листов, указанных в поле #OBJECT, и выгрузит их в JSON. Путь для выгрузки указан в поле #PATH, а место, куда будет выгружен файл, это ваш личный Google Drive, привязанный к учетной записи Google, под которой вы просматриваете документ.

Поле #METHOD позволяет настроить, как именно требуется выгрузить JSON:
  • Если single выгружается один файл с названием, равным названию объекта (без эмодзи, конечно же, они тут только для читаемости)
  • Если separate каждый объект с листа будет выгружен в отдельный JSON.

Оставшиеся поля носят больше информационный характер и позволяют понять, сколько сейчас объектов готово к выгрузке и кто их выгружал последним.

При попытке реализовать честный вызов метода экспорта я столкнулся с интересной особенностью спредшитов: повесить вызов функции на картинку можно, а указать в вызове этой функции аргументы нельзя. После небольшого периода фрустрации было принято решение продолжить эксперимент с велосипедом и родилась идея разметки самих листов с данными.
Так, например, в таблицах появились якоря ###data### и ###end_data### на листах с данными, по которым определяются области атрибутов для выгрузки.

Исходники


Соответственно, как выглядит сбор JSON-a на уровне кода:
  1. Берем поле #OBJECT и ищем все данные листа с таким именем
    var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(name)
    
  2. Ищем координаты основных якорей, по которым будем фильтровать данные листа (идем по рэнжу как по двумерному массиву, пока не найдем ячейку со значением == тексту якоря)
    function GetAnchorCoordsByName(anchor, data){  var coords = { x: 0, y: 0 }    for(var row=0; row<data.length; row++){    for(var column=0; column<data[row].length; column++){      if(data[row][column] == anchor){        coords.x = column;        coords.y = row;        }    }  }  return coords;}
    
  3. Отрезаем столбцы объектов, которые выгружать не потребуется (для них в строке с якорем ###enable### можно изначально выставить true|false)
    function FilterActiveData(data, enabled){    for(var column=enabled.x+1; column<data[enabled.y].length; column++){    if(!data[enabled.y][column]){      for(var row=0; row<data.length; row++){        data[row].splice(column, 1);      }      column--;    }  }  return data}
    
  4. Отрезаем строки за пределами якорей ###data### и ###end_data###
    function FilterDataByAnchors(data, start, end){  data.splice(end.y)  data.splice(0, start.y+1);    for(var row=0; row<data.length; row++){    data[row].splice(0,start.x);  }  return data;}
    
  5. Забираем данные первого столбца в качестве ключей наших атрибутов
    function GetJsonKeys(data){  var keys = [];    for(var i=1; i<data.length; i++){    keys.push(data[i][0])  }  return keys;}
    
  6. Пробегаемся по каждому столбцу и создаем на каждый из них по объекту
    //На вход получаем отфильтрованные значения. //В случае, если экспорт идет как single-file, - сюда приходят все столбцы с листа. //Иначе - метод вызывается столько раз, сколько будет создано separate JSON-овfunction PrepareJsonData(filteredData){  var keys = GetJsonKeys(filteredData)    var jsonData = [];  for(var i=1; i<filteredData[0].length; i++){    var objValues = GetObjectValues(filteredData, i);       var jsonObject = {      "objName": filteredData[0][i],      "jsonBody": ParseToJson(keys, objValues)    }    jsonData.push(jsonObject)  }    return jsonData;}//Упаковываем в JSON конкретный столбец (пары ключ-значение)function ParseToJson(fields, values){  var outputJson = {};  for(var field in fields){    if( IsEmpty(fields[field]) || IsEmpty(values[field]) ){       continue;     }    var key = fields[field];    var value = values[field];        var jsonObject = AddJsonValueByPath(outputJson, key, value);  }  return outputJson;}//Добавляем конкретный атрибут в JSON по его полному путиfunction AddJsonValueByPath(jsonObject, path, value){  if(IsEmpty(value)) return jsonObject;    var nodes = PathToArray(path);  AddJsonValueRecursive(jsonObject, nodes, value);    return jsonObject;}//Разбиваем string с адресом поля на сегментыfunction PathToArray(path){  if(IsEmpty(path)) return [];  return path.split("/");}//Рекурсивно проверяем, существует ли нода адреса, и если нет - добавляемfunction AddJsonValueRecursive(jsonObject, nodes, value){  var node = nodes[0];    if(nodes.length > 1){    AddJsonNode(jsonObject, node);    var cleanNode = GetCleanNodeName(node);    nodes.shift();    AddJsonValueRecursive(jsonObject[cleanNode], nodes, value)  }  else {    var cleanNode = GetCleanNodeName(node);    AddJsonValue(jsonObject, node, value);  }  return jsonObject;}//Добавляем ранее не существовавшую ноду в JSON. Индексы массивов обрабатываются отдельно.function AddJsonNode(jsonObject, node){  if(jsonObject[node] != undefined) return jsonObject;  var type = GetNodeType(node);  var cleanNode = GetCleanNodeName(node);    switch (type){    case "array":      if(jsonObject[cleanNode] == undefined) {        jsonObject[cleanNode] = []      }      break;    case "nameless":       AddToArrayByIndex(jsonObject, cleanNode);      break;    default:        jsonObject[cleanNode] = {}  }  return jsonObject;}//Добавляем новый объект в массив по указанному индексуfunction AddToArrayByIndex(array, index){  if(array[index] != undefined) return array;    for(var i=array.length; i<=index; i++){    array.push({});  }  return array;}//Заполняем конечный атрибут значением (после того, как проверен полный путь до атрибута)function AddJsonValue(jsonObject, node, value){  var type = GetNodeType(node);  var cleanNode = GetCleanNodeName(node);  switch (type){    case "array":      if(jsonObject[cleanNode] == undefined){        jsonObject[cleanNode] = [];      }      jsonObject[cleanNode].push(value);      break;    default:      jsonObject[cleanNode] = value;  }  return jsonObject}//Узнаем тип ноды.//Если object - будем добавлять вложенные ключи по дефолту//Если array - проверяем его наличие и создаем, если его нет//Если nameless - проверяем в массиве выше наличие объекта с соответствующим индексом, и если такого нет - создаемfunction GetNodeType(key){  var reArray       = /\[\]/  var reNameless    = /#/;    if(key.match(reArray) != null) return "array";  if(key.match(reNameless) != null) return "nameless";    return "object";}//Вычищаем из имени ноды псевдоразметку для указания конечного значения уже в JSONfunction GetCleanNodeName(node){  var reArray       = /\[\]/;  var reNameless    = /#/;    node = node.replace(reArray,"");    if(node.match(reNameless) != null){    node = node.replace(reNameless, "");    node = GetNodeValueIndex(node);  }  return node}//Извлекаем индекс объекта массива из nameless-объектаfunction GetNodeValueIndex(node){  var re = /[^0-9]/  if(node.match(re) != undefined){    throw new Error("Nameless value key must be: '#[0-9]+'")  }  return parseInt(node-1)}
    
  7. Полученный JSON передаем для создания соответствующего файла в Google Drive
    //Основной метод, в который необходимо передать: путь, имя файла (с расширением) и string с данными.function CreateFile(path, filename, data){  var folder = GetFolderByPath(path)     var isDuplicateClear = DeleteDuplicates(folder, filename)  folder.createFile(filename, data, "application/json")  return true;}//Ищем конкретную папку в GoogleDrive по полному путиfunction GetFolderByPath(path){  var parsedPath = ParsePath(path);  var rootFolder = DriveApp.getRootFolder()  return RecursiveSearchAndAddFolder(parsedPath, rootFolder);}//Разбиваем полный путь к папке на сегментыfunction ParsePath(path){  while ( CheckPath(path) ){    var pathArray = path.match(/\w+/g);    return pathArray;  }  return undefined;}//Проверяем валидность переданного на вход путиfunction CheckPath(path){  var re = /\/\/(\w+\/)+/;  if(path.match(re)==null){    throw new Error("File path "+path+" is invalid, it must be: '//.../'");  }  return true;}//Если вдруг в папке уже есть файл с таким именем, с которым мы хотим создать файл, - маркируем старый на удаление. //Иначе - получим дублирование файлов, т.к. старый сам не удалитсяfunction DeleteDuplicates(folder, filename){  var duplicates = folder.getFilesByName(filename);    while ( duplicates.hasNext() ){    duplicates.next().setTrashed(true);  }}//Штатной возможности поиска по пути нет, поэтому мы идем от корневого раздела вниз до конечного, ища каждый сегмент пути по имениfunction RecursiveSearchAndAddFolder(parsedPath, parentFolder){  if(parsedPath.length == 0) return parentFolder;     var pathSegment = parsedPath.splice(0,1).toString();  var folder = SearchOrCreateChildByName(parentFolder, pathSegment);    return RecursiveSearchAndAddFolder(parsedPath, folder);}//Ищем в parent папку name, и если нет - создаемfunction SearchOrCreateChildByName(parent, name){  var childFolder = SearchFolderChildByName(parent, name);     if(childFolder==undefined){    childFolder = parent.createFolder(name);  }  return childFolder}//Перебираем итератор файлов в parent на предмет соответствия name на входеfunction SearchFolderChildByName(parent, name){  var folderIterator = parent.getFolders();    while (folderIterator.hasNext()){    var child = folderIterator.next();    if(child.getName() == name){       return child;    }  }  return undefined;}
    


Готово! Теперь идем в Google Drive и забираем там свой файлик.

Для чего была нужна возня с файлами в Google Drive, и почему не постить сразу в гит? В основном только для того, чтобы можно было проверять файлы до того, как они улетели на сервер и совершили непоправимое. В будущем быстрее будет пушить файлы напрямую.
Чего нормально решить не удалось: при проведении различных A/B-тестов всегда возникает необходимость создавать отдельные ветки статики, в которых меняется часть данных. Но так как по сути это еще одна копия дикта, мы можем для A/B-теста копировать сам спредшит, поменять данные в нем и уже оттуда выгружать данные для теста.

Заключение.


Как в итоге справляется подобное решение? На удивление быстро. При условии того, что большая часть таких работ и так выполняется в спредшитах, использовать подручный инструмент оказалось наилучшим способом уменьшить время на разработку.

За счет того, что в документе почти не используются формулы, приводящие к каскадным обновлениям, там практически нечему тормозить. Перенос балансовых расчетов из других таблиц теперь вообще занимает минимум времени, т.к. требуется только перейти на нужный лист, выставить фильтры и копировать значения.

Основным бутылочным горлышком производительности становится API Google Drive: поиск и удаление/создание файлов занимает максимальное время, здесь помогает только выгрузка не всех файлов сразу или выгрузка листа не отдельными файлами, а единым JSON-ом.

Надеюсь, подобный клубок извращений будет полезным для тех, кто все еще редактирует JSON-ы руками и регулярками, а также выполняет балансные расчеты статики в Excel вместо Google Spreadsheets.

Ссылки:


Пример спредшита-экспортера
Ссылка на проект в Google Apps Script
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru