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+).
Выбор метода зависит от конкретной задачи: требуется ли просто проверить наличие, найти позицию первого вхождения или извлечь все совпадения по сложному шаблону.
Обзор задач поиска текста: от простого к сложному
Задачи поиска варьируются по сложности:
- Проверка наличия: Есть ли в строке URL определенный UTM-параметр?
- Поиск первого вхождения: Найти позицию символа
@в email-адресе. - Извлечение данных: Выделить все email-адреса или номера телефонов из большого текста.
- Структурированный поиск: Найти строки в лог-файле, соответствующие определенному шаблону ошибки.
В этой статье мы подробно рассмотрим основные методы поиска, начиная с простейшего 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()
Метод 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()vsRegExp: Для простого поиска подстроки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. Выбор правильного метода и понимание его особенностей позволяют создавать надежные и производительные скрипты для решения широкого круга задач автоматизации.