Google Apps Script: Поиск текста в строке — подробное руководство

Google Apps Script предоставляет мощные возможности для автоматизации задач в экосистеме Google Workspace. Одной из фундаментальных операций при обработке данных является поиск текста в строках. Будь то анализ данных из Google Sheets, парсинг содержимого Google Docs или обработка данных из внешних API, умение эффективно находить нужные фрагменты текста критически важно.

Что такое Google Apps Script и где он используется?

Google Apps Script (GAS) — это облачная платформа для скриптов на основе JavaScript, позволяющая расширять функциональность Google Workspace (Sheets, Docs, Forms, Drive и др.) и создавать интеграции между ними и сторонними сервисами. GAS выполняется на серверах Google, что обеспечивает надежность и доступность без необходимости управления инфраструктурой.

Применения GAS включают автоматизацию рутинных задач (например, генерация отчетов в Sheets), создание кастомных функций, разработку веб-приложений и интеграцию с внешними API (например, для сбора данных из рекламных платформ или систем аналитики).

Основные методы работы со строками в Google Apps Script

GAS, базируясь на JavaScript, наследует его стандартные методы для работы со строками. Ключевыми для поиска текста являются:

  • indexOf(): Поиск первого вхождения подстроки.
  • lastIndexOf(): Поиск последнего вхождения подстроки.
  • includes(): Проверка наличия подстроки (возвращает true или false).
  • search(): Поиск с использованием регулярных выражений.
  • match(): Поиск всех совпадений с регулярным выражением.
  • matchAll(): Итератор для всех совпадений с регулярным выражением (ES2020+).

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

Обзор задач поиска текста: от простого к сложному

Задачи поиска варьируются по сложности:

  1. Проверка наличия: Есть ли в строке URL определенный UTM-параметр?
  2. Поиск первого вхождения: Найти позицию символа @ в email-адресе.
  3. Извлечение данных: Выделить все email-адреса или номера телефонов из большого текста.
  4. Структурированный поиск: Найти строки в лог-файле, соответствующие определенному шаблону ошибки.

В этой статье мы подробно рассмотрим основные методы поиска, начиная с простейшего indexOf() и заканчивая мощными возможностями регулярных выражений с match() и matchAll().

Простейший поиск подстроки: метод indexOf()

Метод indexOf() является базовым инструментом для определения позиции первого вхождения указанной подстроки в строке. Он чувствителен к регистру.

Синтаксис и параметры метода indexOf()

/**
 * Находит индекс первого вхождения подстроки в строке.
 * @param {string} searchValue Подстрока для поиска.
 * @param {number} [fromIndex=0] Индекс, с которого начинать поиск (необязательный).
 * @returns {number} Индекс первого вхождения searchValue или -1, если подстрока не найдена.
 */
string.indexOf(searchValue, fromIndex);
  • searchValue: Строка, которую ищем.
  • fromIndex (опционально): Позиция в исходной строке, с которой следует начать поиск. По умолчанию равен 0.

Примеры использования indexOf() для поиска одного вхождения

/**
 * Пример поиска первого вхождения UTM-метки в URL.
 */
function findFirstUtmSource() {
  const url: string = "https://example.com/?utm_source=google&utm_medium=cpc";
  const searchTerm: string = "utm_source=";

  const index: number = url.indexOf(searchTerm);

  if (index !== -1) {
    Logger.log(`Найден '${searchTerm}' на позиции: ${index}`); // Вывод: Найден 'utm_source=' на позиции: 20
  } else {
    Logger.log(`'${searchTerm}' не найден.`);
  }
}

/**
 * Пример поиска с указанием начальной позиции.
 */
function findSecondOccurrence() {
  const data: string = "campaign_id=123&campaign_id=456";
  const searchTerm: string = "campaign_id=";

  const firstIndex: number = data.indexOf(searchTerm); // Находит первое вхождение (0)
  let secondIndex: number = -1;

  if (firstIndex !== -1) {
    // Ищем второе вхождение, начиная поиск *после* первого
    secondIndex = data.indexOf(searchTerm, firstIndex + 1);
  }

  if (secondIndex !== -1) {
    Logger.log(`Второе вхождение '${searchTerm}' на позиции: ${secondIndex}`); // Вывод: Второе вхождение 'campaign_id=' на позиции: 18
  } else {
    Logger.log(`Второе вхождение '${searchTerm}' не найдено.`);
  }
}

Обработка ситуаций, когда подстрока не найдена (-1)

Ключевым аспектом работы с indexOf() является проверка возвращаемого значения. Если метод возвращает -1, это означает, что searchValue не был найден в строке. Игнорирование этой проверки может привести к ошибкам в логике, особенно если результат используется для дальнейших операций со строкой (например, substring()).

/**
 * Демонстрация обработки случая, когда подстрока не найдена.
 */
function handleNotFound() {
  const text: string = "Данные без ключевого слова";
  const keyword: string = "важно";

  const position: number = text.indexOf(keyword);

  if (position === -1) {
    Logger.log(`Ключевое слово '${keyword}' не найдено. Выполняем альтернативное действие.`);
    // Здесь может быть логика для случая отсутствия ключевого слова
  } else {
    Logger.log(`Ключевое слово '${keyword}' найдено на позиции ${position}.`);
  }
}

Рекомендации по оптимизации использования indexOf()

Хотя indexOf() достаточно быстр для большинства задач, при работе с очень большими строками или при выполнении поиска в циклах стоит учитывать производительность:

  • Предварительная проверка: Если возможно, используйте более дешевые проверки (например, string.length) перед вызовом indexOf(), чтобы избежать ненужного поиска.
  • Чувствительность к регистру: Если регистр не важен, приведите обе строки (исходную и искомую) к одному регистру (toLowerCase() или toUpperCase()) перед поиском. Помните, что это создает новые строки и может повлиять на производительность в очень интенсивных циклах.
  • Используйте includes(): Если вам нужно только проверить наличие подстроки, а не ее позицию, метод includes() может быть семантически более понятным и потенциально оптимизирован для этой задачи.

Метод search() ищет в строке совпадение с регулярным выражением и возвращает индекс первого найденного совпадения. Если совпадений нет, он возвращает -1. В отличие от indexOf(), search() работает с регулярными выражениями, но не поддерживает параметр fromIndex.

Основы регулярных выражений для поиска текста

Регулярные выражения (RegExp) — это мощный инструмент для описания шаблонов поиска в тексте. Основные элементы:

  • Литералы: Обычные символы (например, a, 1, _) соответствуют самим себе.
  • Метасимволы: Символы со специальным значением (например, . — любой символ, * — ноль или более повторений, + — одно или более повторений, ? — ноль или одно повторение, \d — цифра, \s — пробельный символ).
  • Классы символов: [abc] — любой из символов a, b или c. [^abc] — любой символ, кроме a, b, c. [0-9] — любая цифра.
  • Квантификаторы: {n}, {n,}, {n,m} — точное количество, не менее n, от n до m повторений.
  • Якоря: ^ — начало строки, $ — конец строки.
  • Группы: (...) — группировка и захват.
  • Флаги: i — игнорировать регистр, g — глобальный поиск (важно для match() и matchAll(), но не влияет на search()), m — многострочный режим.

Синтаксис и параметры метода search()

/**
 * Ищет первое совпадение с регулярным выражением в строке.
 * @param {RegExp | string} regexp Регулярное выражение (объект RegExp или строка, которая будет преобразована в RegExp).
 * @returns {number} Индекс первого совпадения или -1, если совпадений нет.
 */
string.search(regexp);

Если regexp передается как строка, она автоматически компилируется в регулярное выражение. Для использования флагов необходимо использовать конструктор RegExp или литеральный синтаксис /pattern/flags.

Примеры использования search() с простыми и сложными регулярными выражениями

/**
 * Пример поиска email-адреса с помощью search().
 */
function findEmailWithSearch() {
  const text: string = "Свяжитесь с нами по адресу support@example.com для помощи.";
  // Простое регулярное выражение для поиска email (может быть неполным для всех случаев)
  const emailRegex: RegExp = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/i;

  const index: number = text.search(emailRegex);

  if (index !== -1) {
    Logger.log(`Email найден начиная с позиции: ${index}`); // Вывод: Email найден начиная с позиции: 29
  } else {
    Logger.log("Email не найден.");
  }
}

/**
 * Пример поиска телефонного номера в определенном формате.
 */
function findPhoneNumberWithSearch() {
  const logLine: string = "ERROR: User ID 12345 failed to connect. Call +1-555-123-4567 for support.";
  // Ищем номер телефона в формате +1-XXX-XXX-XXXX
  const phoneRegex: RegExp = /\+1-\d{3}-\d{3}-\d{4}/;

  const index: number = logLine.search(phoneRegex);

  if (index !== -1) {
    Logger.log(`Номер телефона найден на позиции: ${index}`); // Вывод: Номер телефона найден на позиции: 53
  } else {
    Logger.log("Номер телефона не найден.");
  }
}
Реклама

Обработка ошибок и особых случаев при использовании search()

  • Возврат -1: Как и indexOf(), search() возвращает -1, если совпадение не найдено. Всегда проверяйте этот случай.
  • Сложность RegExp: Некорректные или слишком сложные регулярные выражения могут привести к ошибкам или низкой производительности. Тестируйте выражения на различных входных данных.
  • Только первый индекс: search() всегда возвращает только индекс первого совпадения, даже если регулярное выражение имеет флаг g (глобальный поиск).

Более сложные сценарии поиска: match() и matchAll()

Когда требуется не просто найти позицию первого совпадения, а извлечь сам текст совпадений или найти все вхождения, на помощь приходят методы match() и matchAll().

Метод match(): поиск всех вхождений подстроки

Метод match() ведет себя по-разному в зависимости от наличия флага g (глобальный поиск) у регулярного выражения:

  • Без флага g: Возвращает массив, где первый элемент — полное совпадение, последующие — совпадения для групп захвата ((...)). Также массив содержит свойства index (позиция совпадения) и input (исходная строка). Если совпадений нет, возвращает null.
  • С флагом g: Возвращает массив, содержащий все найденные совпадения (только полные совпадения, без групп захвата). Если совпадений нет, возвращает null.
/**
 * Синтаксис метода match().
 * @param {RegExp | string} regexp Регулярное выражение.
 * @returns {RegExpMatchArray | null} Массив совпадений или null.
 */
string.match(regexp);

Метод matchAll(): итерация по всем совпадениям (ES2020+)

Метод matchAll() (доступен в современных средах выполнения V8, используемых Apps Script) возвращает итератор всех совпадений с регулярным выражением в строке. Это особенно полезно при работе с группами захвата, так как каждый элемент итератора содержит полную информацию о совпадении, включая группы.

Регулярное выражение для matchAll() должно иметь флаг g.

/**
 * Синтаксис метода matchAll().
 * @param {RegExp} regexp Регулярное выражение с флагом 'g'.
 * @returns {IterableIterator<RegExpMatchArray>} Итератор совпадений.
 */
string.matchAll(regexp);

Использование групп захвата в регулярных выражениях с match() и matchAll()

Группы захвата (...) позволяют извлекать части найденного совпадения.

  • С match() (без флага g): Группы доступны как элементы массива, начиная со второго (result[1], result[2], …).
  • С matchAll(): Группы доступны в каждом элементе, возвращаемом итератором (match[1], match[2], …).

Примеры извлечения данных из строки с использованием match() и matchAll()

/**
 * Извлечение всех цен из текста с помощью match() и флага 'g'.
 */
function extractAllPrices() {
  const reportText: string = "Продажи: товар A - $19.99, товар B - $25.50, скидка - $5.00";
  const priceRegex: RegExp = /\$\d+\.\d{2}/g; // Ищем цены в формате $XX.XX

  const prices: RegExpMatchArray | null = reportText.match(priceRegex);

  if (prices) {
    Logger.log("Найденные цены:");
    prices.forEach((price: string) => Logger.log(price));
    // Вывод:
    // Найденные цены:
    // $19.99
    // $25.50
    // $5.00
  } else {
    Logger.log("Цены не найдены.");
  }
}

/**
 * Извлечение параметров UTM-меток из URL с помощью matchAll() и групп захвата.
 */
function extractUtmParameters() {
  const url: string = "https://example.com/?utm_source=google&utm_medium=cpc&utm_campaign=spring_sale";
  // Ищем пары ключ=значение для utm_ параметров
  const utmRegex: RegExp = /utm_([^=]+)=([^&]+)/g;

  const matches: IterableIterator<RegExpMatchArray> = url.matchAll(utmRegex);
  const utmParams: { [key: string]: string } = {};

  for (const match of matches) {
    // match[0] - полное совпадение (напр., 'utm_source=google')
    // match[1] - первая группа захвата (ключ, напр., 'source')
    // match[2] - вторая группа захвата (значение, напр., 'google')
    if (match.length === 3) { // Убедимся, что обе группы захвачены
        utmParams[match[1]] = match[2];
    }
  }

  if (Object.keys(utmParams).length > 0) {
    Logger.log("Извлеченные UTM-параметры:");
    Logger.log(JSON.stringify(utmParams, null, 2));
    /* Вывод:
    Извлеченные UTM-параметры:
    {
      "source": "google",
      "medium": "cpc",
      "campaign": "spring_sale"
    }
    */
  } else {
    Logger.log("UTM-параметры не найдены.");
  }
}

Практические примеры и лучшие практики

Рассмотрим применение методов поиска в реальных задачах Google Apps Script.

Поиск и замена текста в Google Sheets с использованием Apps Script

Частая задача — найти ячейки с определенным текстом и заменить его.

/**
 * Находит и заменяет текст во всех ячейках указанного листа Google Sheets.
 *
 * @param {string} sheetName Имя листа.
 * @param {string | RegExp} searchText Текст или RegExp для поиска.
 * @param {string} replaceText Текст для замены.
 */
function findAndReplaceInSheet(sheetName: string, searchText: string | RegExp, replaceText: string): void {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);

  if (!sheet) {
    Logger.log(`Лист '${sheetName}' не найден.`);
    return;
  }

  const dataRange = sheet.getDataRange();
  // Используем TextFinder для эффективного поиска и замены
  const finder = dataRange.createTextFinder(searchText instanceof RegExp ? searchText.source : searchText);

  // Дополнительные опции для TextFinder (опционально)
  // finder.matchCase(true); // Учитывать регистр
  // finder.matchEntireCell(true); // Искать только полное совпадение содержимого ячейки
  if (searchText instanceof RegExp) {
      finder.useRegularExpression(true);
  }

  const count: number = finder.replaceAllWith(replaceText);
  Logger.log(`Произведено замен на листе '${sheetName}': ${count}`);
}

// Пример вызова:
// findAndReplaceInSheet("Лист1", "старый_текст", "новый_текст");
// findAndReplaceInSheet("Данные", /ID-\d+/g, "REPLACED_ID"); // Замена с RegExp

Примечание: Для задач поиска и замены в Google Sheets предпочтительнее использовать встроенный TextFinder, так как он оптимизирован для работы с диапазонами ячеек.

Поиск определенных слов в тексте документа Google Docs

Поиск ключевых слов или фраз в документе.

/**
 * Находит все вхождения ключевого слова в тексте документа Google Docs.
 *
 * @param {string} docId ID документа Google Docs.
 * @param {string} keyword Ключевое слово для поиска (чувствительно к регистру).
 * @returns {number} Количество найденных вхождений.
 */
function findKeywordInDoc(docId: string, keyword: string): number {
  try {
    const doc = DocumentApp.openById(docId);
    const body = doc.getBody();
    const text = body.getText();

    let count = 0;
    let index = text.indexOf(keyword);

    while (index !== -1) {
      count++;
      // Ищем следующее вхождение, начиная после текущего
      index = text.indexOf(keyword, index + 1);
    }

    Logger.log(`Ключевое слово '${keyword}' найдено ${count} раз в документе ID: ${docId}`);
    return count;

  } catch (e) {
    Logger.log(`Ошибка при доступе к документу ${docId} или поиске: ${e}`);
    return 0;
  }
}

// Пример вызова:
// const keywordCount = findKeywordInDoc("YOUR_DOCUMENT_ID", "конфиденциально");

Рекомендации по повышению производительности поиска в больших объемах текста

  • Избегайте поиска в циклах: По возможности, выполняйте поиск один раз над всем текстом, а не итеративно над его частями.
  • indexOf() vs RegExp: Для простого поиска подстроки indexOf() обычно быстрее, чем search() или match() с простым выражением. Регулярные выражения становятся эффективнее для сложных шаблонов.
  • Компиляция RegExp: Если одно и то же регулярное выражение используется многократно (например, в цикле), создайте объект RegExp один раз вне цикла, а не используйте строковый литерал внутри.
  • Ограничивайте объем данных: Загружайте и обрабатывайте только необходимые данные. Вместо getDataRange().getValues() используйте диапазоны поменьше, если это возможно.
  • Используйте matchAll(): Для извлечения множества совпадений с группами matchAll() может быть эффективнее и удобнее, чем многократные вызовы match() или exec().

Обработка краевых случаев и возможных ошибок

  • Пустые строки: Убедитесь, что ваш код корректно обрабатывает пустые строки как на входе, так и в качестве искомых значений.
  • Специальные символы RegExp: Если вы ищете строку, которая может содержать специальные символы регулярных выражений (например, . * ? [ ]), их необходимо экранировать при использовании search(), match() или matchAll(). Создайте вспомогательную функцию для экранирования.
  • Регистр символов: Четко определите, важен ли регистр при поиске, и используйте соответствующие методы (toLowerCase(), toUpperCase()) или флаг i в RegExp.
  • null результаты: Помните, что match() может вернуть null. Всегда проверяйте результат перед обращением к его элементам или свойствам.
  • Ограничения GAS: Помните об ограничениях Google Apps Script на время выполнения скрипта (6 минут для большинства пользователей, 30 минут для Workspace). Длительные операции поиска в очень больших данных могут превысить эти лимиты.

Эффективный поиск текста — ключевой навык при работе с данными в Google Apps Script. Выбор правильного метода и понимание его особенностей позволяют создавать надежные и производительные скрипты для решения широкого круга задач автоматизации.


Добавить комментарий