Блог

Мониторинг доступности сайта

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

Есть огромное число платных и условно бесплатных сервисов, которые готовы оказать услугу по круглосуточному мониторингу веб ресурсов. И, в случае их недоступности, немедленно проинформировать об этом заинтересованную сторону.

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

Идея проста: используя Google Apps Script мы отправляем запрос на указанный url и анализируем код ответа. Если код ответа равен 200 - ничего не предпринимает. Ну, а если нет - отправляем на свой емейл сообщение об ошибке.

Скрипт, реализующий эту задачу, находится ниже:

function locator() {
  let sites = ['https://it4each.com/', 
               ];

  let myEmail = YourEmail;
  let subject = "Site not working!!!";
  let errors = [];
  
  // request sending and processing loop
  for (const site of sites) {
    try {
      let response = UrlFetchApp.fetch(site);
      if (response.getResponseCode() != 200 ) errors.push(site);
    } catch (e) {
      let error_messege = e.name + ': for website ' + site + '\n';
      console.error(error_messege);
      errors.push(site)
    };
  };

  // send email
  if (errors.length > 0) {
    let message = "";
    for (let error of errors) {
      message += 'Website ' + error + " doesn't working!\n";
    };
    message += '\n' + 'Remaining Daily Quota: ' + MailApp.getRemainingDailyQuota();

    MailApp.sendEmail(myEmail, subject, message)
  };
}

За работу сайтов наблюдает функция locator(). Предварительно, в эту функцию должны быть переданы следующие исходные данные:

  • Список сайтов sites;
  • Адрес почты, куда следует отправлять сообщение об ошибке myEmail;
  • Тему (заголовок) электронного сообщения subject.

Далее идёт цикл отправки и обработки запросов. Это делается с помощью стандартного метода fetch(url) класса UrlFetchApp.

Если ресурс в принципе доступен, но его код ответа изменился и более не равен 200, то имя проблемного ресурса добавляется в список ошибок errors в той же строке.

Но если ресурс вообще недоступен, то UrlFetchApp.fetch(site) даст ошибки, которая может привести к остановке программы. Чтобы этого не произошло, вариант подобной ошибки мы обработаем через try - catch(e). И добавление "битого" сайта произойдёт на этот раз в блоке catch.

Обработка результата будет проиcходить ниже, в блоке send email.

Если список ошибок не пустой, то в цикле будет сформировано сообщение message, где будут перечислены все неработающие сайты. Дополнительно будет добавлена информация о том, сколько подобных сообщений ещё можно создать на сегодня, чтобы не превысить квоту: MailApp.getRemainingDailyQuota().

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

Узнать о том, как создать и настроить триггер, а также получить дополнительную информация о работе данного скрипта, вы сможете из этого видео:

Читать дальше >>

Бесконечные Связанные Списки: Личный Финансовый План (пример #1)

Данная статья является продолжением статьи Бесконечные Зависимые Выпадающие Списки в Google Sheets (часть 3).

Здесь рассмотрен пример практического применения Бесконечных связанных списков при составлении Личного Финансового Плана.

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

// global variable
const excludedSheets = ['Summary', 'Category'];
const dataSheet = 'Category';

var colShift = 3;
var rowMin = 4;

function onEdit(e) {

  let row = e.range.getRow();
  let col = e.range.getColumn();
  let sheetName = e.source.getActiveSheet().getName();
  if (excludedSheets.includes(sheetName)) return;
  let name = e.value;
  let oldName = e.oldValue;
  let sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  let mask = JSON.stringify(sh.getRange(row, colShift+1, 1, col-colShift).getValues()[0]);
  let colMax = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dataSheet).getLastColumn();
  
  if(name !== oldName && row >= rowMin && col < colMax+colShift) {
    insertDateFormulasAndDataValidation(row, col, sheetName)
    fillColumn(row, col, mask, sheetName);
  }
}

function insertDateFormulasAndDataValidation(row, col, sheetName) {

  if (col == colShift+1) {
    let sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    let dateValue = sh.getRange(row, 2).getValue();
    if (dateValue == '') {
      let today = new Date();
      let timeZone = Session.getScriptTimeZone();
      let todayStr = Utilities.formatDate(today, timeZone, 'dd.MM.yyyy');
      sh.getRange(row, 2).setValue(todayStr);
    };
    let formulaStr = '=I' + (row-1) + '+H' + row + '-C' + row; 
    sh.getRange(row, 9).setFormula(formulaStr);
    let conN = String.fromCharCode(65+colShift) 
    let adress = sheetName + '!' + conN + (row+1) + ':' + conN + (row+3);
    sh.getRange(adress).setDataValidation(SpreadsheetApp.newDataValidation()
      .setAllowInvalid(false)
      .requireValueInRange(sh.getRange(dataSheet+'!$A$2:$A'), true)
      .build());    
  }
}

function tmp() {
  insertDateFormulasAndDataValidation(7, 4, 'Transactions');
}

function onOpen() {
  let ui = SpreadsheetApp.getUi();
  // Or DocumentApp or FormApp.
  ui.createMenu('Custom Menu')
      .addItem('Create sheets', 'createSheets')
      .addToUi();
}

function fillColumn(row, col, mask, sheetName) {

  let col_data = col - colShift;

// clear dataVal and Value
  let sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  let colMax = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dataSheet).getLastColumn();
  sh.getRange(row, col + 1, 1, colMax-col_data).clearDataValidations().clearContent();
 
// find date
  let sd = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dataSheet);
  let arrData = sd.getDataRange().getValues();
  let arrData_2 = [];
  
  let iMax = arrData.length - 1;
  for(let i=1; i<=iMax; i++) {
    if(JSON.stringify(arrData[i].slice(0, col_data)) == mask) {
      arrData_2.push(arrData[i].slice(0, col_data+1));
    }
  }
  arrData_2 = arrData_2.map(item => item.pop())
  let uniqArrDate_2 = arrData_2.filter(uniqValues); 

// add dataVal
  col++;
  sh.getRange(row, col).setDataValidation(SpreadsheetApp.newDataValidation()
  .setAllowInvalid(false)
  .requireValueInList(uniqArrDate_2, true)
  .build());
}

function uniqValues(item, index, arr) {
  return arr.indexOf(item) === index;
}

Дополнительную информацию вы можете получить из этого видео:

Читать дальше >>

Бесконечные Зависимые Выпадающие Списки в Google Sheets (часть 3)

Данная статья является продолжением статьи Бесконечные Зависимые Выпадающие Списки в Google Sheets (часть 2).

Здесь описаны изменения изменения и дополнения кода, а именно:

  • Автоматическое создание листа Номе ;
  • Автоматическое добавление всех формул на листе Номе;
  • При редактировании созданной ранее строки зависимых (связанных списков), автоматически удаляются значения и формулы справа от редактируемой ячейки.
  • Возможность сдвига таблицы на листе Номе вправо на произвольное число столбцов
  • 01.10.2021 Добавлена проверка наличия листов Home и Data в рабочем файле (книге)

var colShift = 0;

function onEdit(e) {

  let row = e.range.getRow();
  let col = e.range.getColumn();
  let sheetName = e.source.getActiveSheet().getName();
  let name = e.value;
  let oldName = e.oldValue;
  let sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  let mask = JSON.stringify(sh.getRange(row, colShift+1, 1, col-colShift).getValues()[0]);
  if (!doesSheetExist('Home', true)) return;
  if (!doesSheetExist('Data', true)) return;

  let colMax = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Data").getLastColumn();
  
  if(sheetName === "Home" && name !== oldName && col < colMax+colShift) {
    fillColumn(row, col, mask);
  }
}

function onOpen() {
  let ui = SpreadsheetApp.getUi();
  // Or DocumentApp or FormApp.
  ui.createMenu('Custom Menu')
      .addItem('Create sheets', 'createSheets')
      .addToUi();
}

function fillColumn(row, col, mask) {

  let col_data = col - colShift;

// clear dataVal and Value
  let sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Home');
  let colMax = sh.getLastColumn();
  sh.getRange(row, col + 1, 1, colMax).clearDataValidations().clearContent();
 
// find date
  let sd = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Data");
  let arrData = sd.getDataRange().getValues();
  let arrData_2 = [];
  
  let iMax = arrData.length - 1;
  for(let i=1; i<=iMax; i++) {
    if(JSON.stringify(arrData[i].slice(0, col_data)) == mask) {
      arrData_2.push(arrData[i].slice(0, col_data+1));
    }
  }
  arrData_2 = arrData_2.map(item => item.pop())
  let uniqArrDate_2 = arrData_2.filter(uniqValues); 

// add dataVal
  col++;
  sh.getRange(row, col).setDataValidation(SpreadsheetApp.newDataValidation()
  .setAllowInvalid(false)
  .requireValueInList(uniqArrDate_2, true)
  .build());
}

function createSheets() {

// is exist Home?
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sd;
  if (doesSheetExist('Data', true)) {
    sd = ss.getSheetByName('Data');
  } else {
    return;
  }
  
  
// create if not exist
  if(!ss.getSheetByName('Home')) {
    ss.insertSheet('Home', 0);
// create Data Val
    var sh = ss.getSheetByName('Home');
    sh.getRange('Home!A2:A20').setDataValidation(SpreadsheetApp.newDataValidation()
      .setAllowInvalid(false)
      .requireValueInRange(sh.getRange('Data!$A$2:$A'), true)
      .build());
    sh.getRange(1, 1, 1, 10).setValues(sd.getRange(1, 1, 1, 10).getValues()).setFontWeight('bold');

  };

}

function uniqValues(item, index, arr) {
  return arr.indexOf(item) === index;
}


function doesSheetExist(sheetName, needAlert) {
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let allSheets = ss.getSheets().map(sheet => sheet.getName())
  if (allSheets.includes(sheetName)) return true;
  if (needAlert) Browser.msgBox(`Error! Could not find sheet "${sheetName}"!`);
  
  return false;
}

В этой версии программы сохранена возможность по нажатию всего лишь одной(!) кнопки из пользовательского меню автоматически создавать все листы файла, необходимые для работы скрипта (включая форматирование и валидацию данных).

Всё что для этого нужно:

  1. cоздать лист с именем "Data"
  2. скопировать в него данные, необходимые для связанных (зависимых) списков
  3. скопировать и встваить в файл этот скрипт
  4. обновить страницу
  5. в появившемся пользовательском меню выбрать: Create sheets

Дополнительную информацию вы можете получить из этого видео:

ВНИМАНИЕ!
У этой статьи есть продолжение: Бесконечные Связанные Списки: Личный Финансовый План (пример #1)

Читать дальше >>

Google API Sheets in Python Чтение и Запись Данных с Помощью Сервисного Аккаунта

Скрипт, представленный ниже, позовляет осуществлять достуа к файлам Google Spreadsheets для извлечения данных с листов электронной таблицы и записи новой информации.

Для того, чтобы скримт мог работать необхомо выполнить следующие условия:

  • Создать сервисный аккаунт: https://console.cloud.google.com/home/dashboard
  • Создать новый проект, установить виртуальное окружение и установить программый пакет Google API (см. видео)
  • В сервисном аккаунте создать файл credentials.json
  • Выгрузить файл credentials.json в корневой каталог проекта
  • Создать файл Google Spreadsheet и дать доступ боту к этому файлу (см. видео)
  • Скопировать файлы main.py и a1range.py и скопировать в них код, представленный ниже


Код для файла main.py:

from a1range import A1Range
import os.path
from googleapiclient.discovery import build
from google.oauth2 import service_account

SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SERVICE_ACCOUNT_FILE = os.path.join(BASE_DIR, 'credentials.json')

credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE, scopes=SCOPES)

# The ID and range of a sample spreadsheet.
SAMPLE_SPREADSHEET_ID = '1KKOM9tpdCKtQmxyKkP_mcEMj1zreHyZ72cLL0ENXSHw'
SAMPLE_RANGE_NAME = 'Sheet1'


service = build('sheets', 'v4', credentials=credentials).spreadsheets().values()

# Call the Sheets API
# result = service.get(spreadsheetId=SAMPLE_SPREADSHEET_ID,
#                      range=SAMPLE_RANGE_NAME).execute()

# data_from_sheet = result.get('values', [])

array = {'values': [[5, 6, None, 100], ['=SUM(A1:A4)', '=SUM(B1:B4)']]}
range_ = A1Range.create_a1range_from_list('Sheet1', 4, 1, array['values']).format()
response = service.update(spreadsheetId=SAMPLE_SPREADSHEET_ID,
                          range=range_,
                          valueInputOption='USER_ENTERED',
                          body=array).execute()

Код для файла a1range.py:

from dataclasses import dataclass


@dataclass
class A1Range:
    sheet_name: str
    start_row: int
    start_col: int
    end_row: int
    end_col: int

    def format(self) -> str:
        start = f"{self.col_number_to_letter(self.start_col)}{self.start_row}"
        end = f"{self.col_number_to_letter(self.end_col)}{self.end_row}"
        return f"{self.sheet_name}!{start}:{end}"

    def iter_rows(self):
        return range(self.start_row, self.end_row + 1)

    def iter_cols(self):
        return range(self.start_col, self.end_col + 1)

    @staticmethod
    def col_number_to_letter(j: int) -> str:
        if 1 <= j <= 26:
            return chr(ord('A') + j - 1)
        elif 27 <= j <= 26 * 26:
            first_letter = chr(ord('A') - 1 + (j - 1) // 26)
            second_letter = chr(ord('A') + (j - 1) % 26)
            return first_letter + second_letter
        else:
            raise ValueError(f"Col number is out of range: {j!r}")

    @staticmethod
    def col_letter_to_number(letters: str) -> int:
        letters = letters.upper()
        if len(letters) == 1 and (ord(letters) < ord('A') or ord(letters) > ord('Z')):
            raise ValueError(f"Col letter is out of range: {letters!r}")

        if len(letters) == 2 and (ord(letters[1]) < ord('A') or ord(letters[1]) > ord('Z')):
            raise ValueError(f"The second Col letter is out of range: {letters!r}")

        if len(letters) == 1:
            return ord(letters) - ord('A') + 1
        elif len(letters) == 2:
            return (ord(letters[0]) - ord('A') + 1) * 26 + ord(letters[1]) - ord('A') + 1

    @staticmethod
    def extract_letters(text) -> str:
        only_letters = ''
        for t in text:
            if t.isalpha():
                only_letters += t
        return only_letters

    @staticmethod
    def extract_digits(text) -> str:
        only_digits = ''
        for t in text:
            if t.isdigit():
                only_digits += t
        return only_digits

    @classmethod
    def parse_a1_range(cls, a1: str):
        if "!" in a1 and ":" in a1:
            sheet_name, cell_range = a1.split('!')
            range_start, range_end = cell_range.split(':')
            start_col, start_row = cls.extract_letters(range_start), cls.extract_digits(range_start)
            end_col, end_row = cls.extract_letters(range_end), cls.extract_digits(range_end)
            return cls(
                sheet_name=sheet_name,
                start_col=cls.col_letter_to_number(start_col),
                start_row=int(start_row),
                end_col=cls.col_letter_to_number(end_col),
                end_row=int(end_row),
            )
        else:
            raise ValueError(f'Error! For method "parse_a1_range()" must be full address!')

    @classmethod
    def create_a1range_from_list(cls, sheet_name, from_row, from_col, array):
        """
        The method find coordinates in format A1Notation for a list with data for inserting into a Google Sheet
        :param sheet_name: sheet name in Google Sheet
        :param from_col: the column coordinate of the upper left corner
        :param from_row: the row coordinate of the upper left corner
        :param array: a list with data for inserting in Google Sheet
        :return: coordinates for Google Sheet in format A1Notation
        """
        count_rows = len(array)
        count_cols = 0
        for row in array:
            if len(row) > count_cols:
                count_cols = len(row)
        return cls(
            sheet_name=sheet_name,
            start_col=from_col,
            start_row=from_row,
            end_col=from_col+count_cols-1,
            end_row=from_row+count_rows-1,
        )

Более подробно о работе скрипта рассказано в этом видео:

Читать дальше >>

Не работает парсинг сайта с помощью Apps Script. Почему?

Способ парсинга (скрейпинга), рассмотренный в статье Парсинг (Скрапинг) с помощью Google Apps Script, невероятно прост и удобен. Здесь не нужно ни сложных программ, ни высокой квалификации или специальных знаний, ни большого опыта. Весь код пишется прямо в редакторе Google-таблиц, и затем там же размещается и запускается.

Плюс ко всему этому, скрипт можно запускать по расписанию в любое время в режиме 24 / 7, а полученный результат - прямо тут же обработать и, если надо, отправить на электронную почту или создать на Гугл-диске новый файл с полученными и обработанными данными.

Однако, и здесь могут быть свои проблемы. Прежде всего, этот скрипт не работает с сайтами (точнее, со страницами), где необходима предварительная регистрация. Поскольку, для регистрации на сайте необходимо создать сессию и разместиь её код в специальных файлах - cookies. Однако, стандартная функция UrlFetchApp, используемая в Apps Script для доступа к веб-сайту, не позволяет работать c cookies.

Есть и другая проблема, которая в последее время проявляется всё чаще и чаще. Это - нежелание владельцев сайтов делиться информацией с ботами. Поэтому, нередко можно видеть картину, когда html-код, выдаваемый сервером по запросу скрипта, существенно отличается от кода, который выдаётся браузеру.

Собственно, именно для того, чтобы быстро узнать, можно ли парсить (скрейпить) нужный нам сайт или нет, и была написана эта программа:

function onOpen() {
  var ui = SpreadsheetApp.getUi();
  // Or DocumentApp or FormApp.
  ui.createMenu('Custom Menu')
      .addItem('Check', 'fastAnalyzer')
      .addSeparator()
      .addItem('Create Sheeet CHECK', 'createSheet')
      .addToUi();
}


function fastAnalyzer() {
  const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Check');
  const urlDomain = ss.getRange(2, 1).getValue();
  
  let url = urlDomain;
  let count = 0;
  let response = UrlFetchApp.fetch(url);
  let lll = response.getContentText().length;
  ss.getRange(2, 2).setValue(response.getResponseCode());
  ss.getRange(2, 3).setValue(lll);
  let html = response.getContentText();
  let whatLookingFor = ss.getRange(3, 2).getValue();
  let pos = html.indexOf(whatLookingFor);
  ss.getRange(3, 3).setValue(pos);

  ss.getRange(4, 1, ss.getLastRow(), 2).clearContent();
  let totalNumber = html.length
  let i = 0;

  // Check defoult value of delta - number of symbols in 1 row
  // If the cell is empty, print the defole value = 5000
  let delta = Number.parseInt(ss.getRange(2, 4).getValue());
  if (isNaN(delta)) {
    delta = 5000
    // Print defoult value of number of symbols in 1 row
    ss.getRange(2, 4).setValue(delta);
  };

  let iStart = 0;
  while (true) {
    let iEnd = iStart + delta;
    if (iEnd > totalNumber) iEnd = totalNumber; 
    ss.getRange(4 + i, 1).setValue("Символы от " + iStart + " до " + iEnd);
    ss.getRange(4 + i, 2).setValue(html.slice(iStart,iEnd));
    // let currentRow = (4+i) + ":" + (4+i)
    // ss.getRange(3, 3).setValue(currentRow);
    // ss.getRange(currentRow).activate();
    // ss.setRowHeight(4+i, 200);
    i++;
    if (iEnd - iStart < delta) break;
    iStart = iEnd;

  };
}

function createSheet() {

// is exist Check sheet?
  let s = SpreadsheetApp.getActiveSpreadsheet();
  
// create if not exist
  if(!s.getSheetByName('Check')) {
    s.insertSheet('Check', 0);
    ssc = s.getSheetByName('Check');

    ssc.getRange(1, 1).setValue("Url сайта");
    ssc.getRange(1, 2).setValue("Код ответа сайта");
    ssc.getRange(1, 3).setValue("Число символов на выбранной странице");
    ssc.getRange(1, 4).setValue("Изменить вывод числа знаков в однй строке");
    ssc.getRange(2, 1).setBackground('ACCENT3');  // yellow background cell
    ssc.getRange(3, 1).setValue("Какой элемент кода ищем?");
    ssc.getRange(3, 2).setBackground('ACCENT3');  // yellow background cell
  };
}

Итак, всё, что вам нужна для работы с этой программой - это

  • создать новый файл Google Spreadsheet
  • открыть в этом файле редактор кода
  • полностью скопировать код из этой статьи в редактора кода только что созданного файла (и не забыть его сохранить!)
  • обновить страницу электронной таблицы и выбрать пункт пользовательского меню: Create Sheet CHECK (Создать Листь ПРОВЕРКИ)

Программа готова к работе! Всё, что теперь нужно, это - добавить на страницу Гугл-таблицы url сайта, элемент кода, который вы хотите найти на этой странице, и запустить скрипт. Ответ будет готов через секунду!

Более подробно о работе скрипта рассказано в этом видео:

Читать дальше >>

Парсинг (Скрапинг) с помощью Google Apps Script

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

Имя домена берётся со страницы Google Spread Sheets. Туда же выгружаются результаты поиска.

В качестве управляющего скрипта используется функция scraper(), которая содержит два цикла. В первом цикле (for-цикл) идёт обращение к веб-страницам сайта, и их сохранение в переменной html. Во втором цикле (while-цикл) идёт последовательная обработка этой переменной с помощью трёх вспомогательных функций:

  • Функция getBlock находит часть html-кода (блок кода) внутри тега (обычно по уникальному значению атрибутов этого тега), и возвращает этот блок в виде строкового значения (без самого тега!);
  • Функция deleteBlock наоборот, удаляет найденный фрагмент html-кода внутри блока и также возвращает оставшуюся часть этого блока в виде строкового значения.
  • В отличии от первых двух функций, функция getOpenTag не удаляет найденный тег, а возвращает его в виде строкового знечения. Правде, не весь тег, а только первую (открывающую часть) этого тега.

function scraper() {
  
  const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
  const urlDomain = ss.getRange(1, 1).getValue();
  
  let url = urlDomain;
  let count = 0;
  for (let page = 1; page < 5; page++) {
    url = urlDomain + page + '/';
    if (page == 1) url = urlDomain;
    let response = UrlFetchApp.fetch(url);
    ss.getRange(2, 1).setValue(response.getResponseCode());
    let html = response.getContentText();
    let p = 0;
    while (true) {
    
      let out = getBlock(html, 'div', html.indexOf('class="JobSearchCard-primary"', p));
      let block = out[0];
      p = out[1] + 1;
      if (p == 0) break;
      
      let title1 = getBlock(block, 'div', 0)[0];
      let title = getBlock(title1, 'a', 0)[0];
      
      let link = getOpenTag(title1, 'a', 0);
      link = getAttrName(link, 'href', 0)
      let formula = '=HYPERLINK("https://www.freelancer.com' +link + '", "' + title + '")';
      ss.getRange(3 + 3 * count, 2).setValue(formula);
      
      let price = getBlock(block, 'div', block.indexOf('class="JobSearchCard-primary-price'))[0];
      if (price.includes('span')) price =  deleteBlock(price, 'span', price.indexOf('span'));
      ss.getRange(3 + 3 * count + 1, 2).setValue(price).setHorizontalAlignment('right');
      
      let description = getBlock(block, 'p', block.indexOf('class="JobSearchCard-primary-description"'))[0];
      ss.getRange(3 + 3 * count, 1, 3).mergeVertically().setValue(description)
          .setBorder(true, true, true, true, null, null, '#000000', SpreadsheetApp.BorderStyle.SOLID)
          .setVerticalAlignment('middle')
          .setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP);
  
      ss.getRange(3 + 3 * count, 2, 3).setBorder(true, true, true, true, null, null, '#000000', SpreadsheetApp.BorderStyle.SOLID);
      
      let cat = getBlock(block, 'div', block.indexOf('class="JobSearchCard-primary-tags"'))[0];
      cat = cat.split('</a>').map(item => item.split('>')[1]);
      cat.pop();
      cat = cat.join(', ');
      ss.getRange(3 + 3 * count + 2, 2).setValue(cat);
    
      count++;
    
    }; 
  };
}

function getAttrName(html, attr, i) {
  let idxStart = html.indexOf(attr +'=' , i);
  if (idxStart == -1) return "Can't to find attr " + attr + ' !';
  idxStart = html.indexOf('"' , idxStart) + 1;
  let idxEnd = html.indexOf('"' , idxStart);
  return html.slice(idxStart,idxEnd).trim();
}

function getOpenTag(html, tag, idxStart) {
  let openTag = '<' + tag;
  let lenOpenTag = openTag.length;
  // where we are?
  if (html.slice(idxStart, idxStart + lenOpenTag) != openTag) {
    idxStart = html.lastIndexOf(openTag, idxStart);
    if (idxStart == -1) return "Can't to find openTag " + openTag + ' !';
  };
  // begin loop after openTag
  let idxEnd = html.indexOf('>', idxStart) + 1;
  if (idxStart == -1) return "Can't to find closing bracket '>' for openTag!";
  return html.slice(idxStart,idxEnd).trim();
}

function deleteBlock(html, tag, idxStart) { // delete opening & closing tag and info between them
  let openTag = '<' + tag;
  let lenOpenTag = openTag.length;
  let closeTag = '</' + tag + '>';
  let lenCloseTag = closeTag.length;
  let countCloseTags = 0;
  let iMax = html.length;
  let idxEnd = 0;
  // where we are?
  if (html.slice(idxStart, idxStart + lenOpenTag) != openTag) {
    idxStart = html.lastIndexOf(openTag, idxStart);
    if (idxStart == -1) return ["Can't to find openTag " + openTag + ' !', -1];
  };
  // begin loop after openTag
  let i = html.indexOf('>') + 1;
  
  while (i <= iMax) {
    i++;
    if (i === iMax) {
      return ['Could not find closing tag for ' + tag, -1];
    };
    let carrentValue = html[i];
    if (html[i] === '<'){
      let closingTag = html.slice(i, i + lenCloseTag);
      let openingTag = html.slice(i, i + lenOpenTag);
      if (html.slice(i, i + lenCloseTag) === closeTag) {
        if (countCloseTags === 0) {
          idxEnd = i + lenCloseTag;
          break;
        } else {
          countCloseTags -= 1;
        };
      } else if (html.slice(i, i + lenOpenTag) === openTag) {
        countCloseTags += 1;
      };
    };
  };
  return (html.slice(0, idxStart) + html.slice(idxEnd, iMax)).trim();
}

function getBlock(html, tag, idxStart) {  // <tag .... > Block </tag>
  let openTag = '<' + tag;
  let lenOpenTag = openTag.length;
  let closeTag = '</' + tag + '>';
  let lenCloseTag = closeTag.length;
  let countCloseTags = 0;
  let iMax = html.length;
  let idxEnd = 0;
  // where we are?
  if (html.slice(idxStart, idxStart + lenOpenTag) != openTag) {
    idxStart = html.lastIndexOf(openTag, idxStart);
    if (idxStart == -1) return ["Can't to find openTag " + openTag + ' !', -1];
  };
  // change start - will start after openTag!
  idxStart = html.indexOf('>', idxStart) + 1;
  let i = idxStart;
  
  while (i <= iMax) {
    i++;
    if (i === iMax) {
      return ['Could not find closing tag for ' + tag, -1];
    };
    let carrentValue = html[i];
    if (html[i] === '<'){
      let closingTag = html.slice(i, i + lenCloseTag);
      let openingTag = html.slice(i, i + lenOpenTag);
      if (html.slice(i, i + lenCloseTag) === closeTag) {
        if (countCloseTags === 0) {
          idxEnd = i - 1;
          break;
        } else {
          countCloseTags -= 1;
        };
      } else if (html.slice(i, i + lenOpenTag) === openTag) {
        countCloseTags += 1;
      };
    };
  };
  return [html.slice(idxStart,idxEnd + 1).trim(), idxEnd];
}

Более продробную информацию вы сможете найти в этом видео:

Читать дальше >>

Массивы Google Apps Script и JavaScript. Методы поиска

В примерах, приведённых ниже, подробно рассматриваются JavaScript и Apps Script методы поиска элементов массивов : find(), findIndex(), indexOf(), lastIndexOf(), includes(), every() и some().

Методы findIndex(), indexOf() и lastIndexOf() позволяют найти индекс массива, удовлетворяющий заданному условию, а метод find() позволяет найти его значение.

Метод includes() удобен для использования в операторе if , поскольку возвращает true, если элемент найден, и false, если нет.

Метод every() проверяет соответствие ВСЕХ элементов массива указанному в callback функции условию, а метод some() проверяет, удовлетворяет ли этому условию ХОТЯ БЫ ОДИН элемент массива.

const arr = [1, 2, 3, 4, 5, 3];
  
// find() returns the VALUE of the FIRST item satisfies condition of the callback function
// OR return <underfined>

  let valueGteater3 = arr.find(item => item > 3);
  console.log(valueGteater3); // 4
  
  let valueGteater5 = arr.find(item => item > 5);
  console.log(valueGteater5); // undefined

// findIndex() returns the INDEX of the FIRST item satisfies condition of the callback function
// OR return <-1>

  let indexItemGteater3 = arr.findIndex(item => item > 3);
  console.log(valueGteater3); // 3
  
  let indexItemGteater5 = arr.findIndex(item => item > 5);
  console.log(valueGteater5); // -1
  
// variant for not first item  
  
  let notFirstItem = arr.find((item, index) => {
    if (item > 3 && index > 3) return true;
  });
  console.log(notFirstItem); // 5
  
// indexOf() returns the INDEX of the FIRST item is equal the specified value
// OR return <-1>

  let valueEqual3 = arr.indexOf(3);
  console.log(valueEqual3); // 2
  
  let valueEqual3Next = arr.indexOf(3, 3);
  console.log(valueEqual3Next); // 5
 
  let valueEqual1 = arr.indexOf(1, 3);
  console.log(valueEqual1); // -1
  
// lastIndexOf() returns the INDEX of the LAST item is equal the specified value
// OR return <-1>

  let valueEqual3Last = arr.lastIndexOf(3);
  console.log(valueEqual3Last); // 5
  
  let valueEqual3NextLast = arr.lastIndexOf(3, 4);
  console.log(valueEqual3NextLast); // 2
 
  let valueEqual5Last = arr.lastIndexOf(5, 3);
  console.log(valueEqual5Last); // -1
  
// const arr = [1, 2, 3, 4, 5, 3];
// includes() returns TRUE is element is icluded in the array, and FALSE is not

  let isIncludes3 = arr.includes(3);
  console.log(isIncludes3); // true
  
  let isIncludes6 = arr.includes(6);
  console.log(isIncludes6); // false
  
// every() returns TRUE if ALL elements satisfy the condition, and FALSE is not

  let allPositive = arr.every(item => item > 0);
  console.log(allPositive); // true
  
  let moreThen1 = arr.every(item => item > 1);
  console.log(moreThen1); // false

// some() returns TRUE if AT LEAST ONE element satisfy the condition, and FALSE is not

  let moreThen4 = arr.some(item => item > 4);
  console.log(moreThen4); // true
  
  let moreThen5 = arr.some(item => item > 5);
  console.log(moreThen5); // false

Более продробную информацию вы сможете найти в этом видео:

Читать дальше >>

Массивы Google Apps Script и JavaScript. Метод .reduce()

Краткое описание метода

Метод .reduce() как и большинство других методов, использующих callback функцию, представляет собой цикл for, внутри которого callback функция последовательно обрабатывает все элементы массива. Однако, следует добавить, что что это единственный метод, который используе ещё один параметр - accumulator (аккумулятор), блягодаря чему метод .reduce() часто используется для вычислений суммы массива.

Метод .reduce() может передавать в callback функцию 4 параметра:

  1. текщее значение суммы элемента массива accumulator
  2. текщее значение элемента массива item
  3. необязательный параметр index - индекс текущего элемента
  4. необязательный и редко используемый параметр array

Этот метод может быть использован и для более сложных вычислений. Например, ниже приводится скрипт, который создаёт массив индексов элементов массива arr, представлящих собой нечётные числа.

const arr = [1, 2, 3, 4, 5];
  
// sum with for loop
  let sumArr = 0;
  for (let i = 0; i < arr.length; i++) {
    sumArr += arr[i];
  };
  console.log(sumArr);
  
// sum with forEach method
  let sumArr_1 = 0;
  arr.forEach(item => sumArr_1 += item);
  console.log(sumArr_1);
  
// sum with reduce method
  let sumArr_2 = arr.reduce((acc, item) => acc + item, 0);
  console.log(sumArr_2);
  
// with initial value
  sumArr_2 = arr.reduce((acc, item, index, array) => {
    console.log(`acc = ${acc}, item = ${item}, index = ${index}`);
    return acc + item;
  }, 0);
  console.log(sumArr_2);
  
// without initial value
  sumArr_2 = arr.reduce((acc, item, index, array) => {
    console.log(`acc = ${acc}, item = ${item}, index = ${index}`);
    return acc + item;
  });
  console.log(sumArr_2);
  
// find indexes items that are odd numbers  
  let newArr = arr.reduce((acc, item, index) => {
    if (item % 2 !== 0) acc.push(index);
    return acc;
  }, []);
  console.log(newArr); // [ 0, 2, 4 ]
  
 // reduceRight
  sumArr_2 = arr.reduceRight((acc, item, index, array) => {
    console.log(`acc = ${acc}, item = ${item}, index = ${index}`);
    return acc + item;
  });
  console.log(sumArr_2);

Дополнительную информацию вы можете найти в этом видео:

Читать дальше >>

Массивы Google Apps Script и JavaScript. Метод .forEach()

Краткое описание метода

Метод .forEach() , по сути, представляет собой цикл for, внутри которого callback функция последовательно обрабатывает все элементы массива.

Метод .forEach() может передавать в callback функцию три параметра:

  1. текщее значение элемента массива item
  2. необязательный параметр index - индекс текущего элемента
  3. необязательный и редко используемый параметр array

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

В частности, в последнем примере с помощью методов .forEach() и .includes() создаётся новый массив result, который содержит в себе общие элементы массивов arr1 и arr2, то есть является результатом пересечения этих массивов.

const arr = [1, 2, 3, 4, 5];
  
  for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
  };

// variant #1
  arr.forEach(function(item) {
    console.log(item);
  });

// variant #2
  arr.forEach(item => console.log(item));
  
  console.log('------------------------------------');
  
// callback function parameters
  arr.forEach((item, idx, array) => console.log(item, idx, array));

  console.log('------------------------------------');

// get intersection of arrays
  let arr1 = [1, 3, 5];
  let arr2 = [1, 2, 3, 4, 5];
  let result = [];

  let res = arr1.forEach(item => {
    if (arr2.includes(item)) return result.push(item);
  });

  console.log(result); // [ 1, 3, 5 ]
  console.log(res); // undefined

Дополнительную информацию вы можете найти в этом видео:

Читать дальше >>

Массивы Google Apps Script и JavaScript. Метод .filter()

Краткое описание метода

Метод .filter() последовательно сравнивает все элементы массива с условием фильтра, указанным в callback функции (функции обратного вызова). То есть метод, по сути, представляет собой цикл for, внутри которого callback функция последовательно обрабатывает все элементы массива.

Метод .filter() может передавать в callback функцию три параметра:

  1. текщее значение элемента массива item
  2. необязательный параметр index - индекс текущего элемента
  3. необязательный и редко используемый параметр array

Для того, чтобы элемент, удовлетворяющий условию фильтра, был выбран, callback функция должна вернуть значение true.

Все элементы, для которых callback функция вернула знечение true, собираются в отдельный массив, который может быть сохранён в новом массива (в примерах ниже это массив result2).

В трёх вариантов примера, представленных ниже (examples #1, #2, #3) рассмотрено одно и то же условие фильтра - отобрать все элементы массива со значением > 3.

В последнем примере (наиболее оптимальным с точки зрения краткости кода) логическое выражение, вместо оператора if , поставлено непосредственно в return . Поскольку, как известно, логическое выражение может принимать только 2 значения: true или false.

const arr = [1, 2, 9, 4, 1, 6, 5];
    
    let result = arr.filter((item, idx, arr) => console.log(item, idx, arr));
//  [20-06-15 14:53:21:082 EEST] 1 0 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:084 EEST] 2 1 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:085 EEST] 9 2 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:086 EEST] 4 3 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:087 EEST] 1 4 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:089 EEST] 6 5 [ 1, 2, 9, 4, 1, 6, 5 ]
//  [20-06-15 14:53:21:090 EEST] 5 6 [ 1, 2, 9, 4, 1, 6, 5 ]
    
    console.log(result);
//  [20-06-15 14:53:21:091 EEST] []  

// variant #1
  let result2 = arr.filter(item => {
      if (item > 3) {
        return true;
      } else {
        return false;
      };
    });
    
// variant #2
    result2 = arr.filter(item => {
      return item > 3;
    });


// variant #3
    result2 = arr.filter(item => item > 3);
    
    console.log(result2); // [ 9, 4, 6, 5 ]
    
    var sf = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
    let data = sf.getDataRange().getValues().slice(1);
    
    console.log(data);
//  [ [ '', false, 'apples', 5 ],
//    [ '', true, 'carrots', 12 ],
//    [ '', false, 'grapes', 4 ],
//    [ '', false, 'plums', 3 ],
//    [ '', true, 'strawberry', 9 ],
//    [ '', false, 'perches', 4 ],
//    [ '', false, 'bananas', 1 ] ]   

    let newData = data.filter(item => item[1]);

    console.log(newData); 
// [ [ '', true, 'carrots', 12 ], 
//   [ '', true, 'strawberry', 9 ] ]

И ещё одни полезный скрипт, который не вошёл в это видео - скрипт, который фильтрует ТОЛЬКО уникальные значения.

Идея очень проста: для каждого элемента массива находится его индекс и сравнивается с текущим индексом значения этого элемента в массиве. Если элемент не встретился ранее, то значения индексов будут равны, и функция uniqValue вернёт true .

В первом примере, для лучшего понимания алгоритма, функция uniqValue написана отдельно. Во втором - непосредственно "встроена" в метод .filter

let arrayNotUniq = [1, 5, 9, 5];

//variant #1

function uniqValues(item, index, arr) {
  return arr.indexOf(item) === index;
}

let arrayUniq = arrayNotUniq.filter(uniqValues)                        // [1, 5, 9]



//variant #2

  let arrayUniq = arrayNotUniq.filter((item, index, arr) => {
    return arr.indexOf(item) === index;
  });                                                                                                     // [1, 5, 9]

Дополнительную информацию вы можете найти в этом видео:

Читать дальше >>

Список тэгов

    Apps Script      Arrays Java Script      asynchronous code      asyncio      coroutine      Django      Dropdown List      Drop Shipping      Exceptions      GitHub      Google API      Google Apps Script      Google Docs      Google Drive      Google Sheets      multiprocessing      Parsing      Python      regex      Scraping      ssh      Test Driven Development (TDD)      threading      website monitoring      zip