What's new
Runion

This is a sample guest message. Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

Парсинг Google: получаем 100 000 целей за 20 руб. с помощью Google Sheets

petrinh1988

Light Weight
Депозит
$0
Автор petrinh1988
Источник https://xss.is


Для атак на веб-приложения, нужна база сайтов, вот мой вариант массового сбора по доркам. Кроме того, считаю, что Google Apps Script сильно недооценен в сообществе, поэтому кроме основной темы, разберу пару интересных примеров.

Есть куча инструментов, которые позволяют собирать сайты, но не всегда ими удобно пользоваться. Ну или не выгодно. Тот же A-Parser или Zenno стоит денег. Плюс нагрузка на комп и сеть. GAS же позволяет парсить параллельно другим процессам, не требуя дополнительных ресурсов. Поэтому, я решил использовать возможности Google Sheets и Google Apps Script.

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

Что такое GAS?

Начнем с базы. Google Apps Script - это, как понятно из названия, скриптовый язык Google. Как Visual Basic for Application в продуктах MS Office. Он также охватывает большую часть продуктов и сервисов Google.

Sheets, Docs, Drive, Gmail, Calendar — этим всем можно спокойно оперировать при помощи скриптов. Сам по себе язык, это одна из реализаций Javascript. Поэтому, если есть базовые знания JS, никаких проблем не возникнет. Чтобы писать полноценные решения, нужно будет просто посмотреть, какие объекты (интерфейсы) представляет GAS. Ну и разобраться с некоторым несложным устройством, а также с замороченными правами доступа.

Сами проекты, которые используются в ваших Google-аккаунтах, можно посмотреть по адресу https://script.google.com/home Скрипт может быть привязан к той же таблице (создаваться из нее), тогда при копировании таблицы будет копироваться и скрипт.

AD_4nXdNcOOkwZ-qDoPTNkQytMYq-KFvnZv9EBZ4gs3r-bzvWivZZke-RUB0me3rifGNrbdaao9hL38Vb0_CqKmUTqbfkYwHFJPiOxc29bwvL3ngnvBeJSbB0NAlfpLGTMBrvm1T0Ko8dDM6i03w9gzOgb8Zc8w



Важные детали перед началом

GAS имеет ограничения на количество исходящих запросов. Раньше было 100 000 запросов в сутки с аккаунта, сейчас 20 000. Т.е., если потребуется большое количество парсингов, потребуется большое количество аккаунтов. Повторюсь — ограничение для аккаунта, а не таблицы или чего-то другого. Суммируются все исходящие запросы, запросы к опубликованному приложению не считаются. По крайней мере я не видел таких квот.

Для парсинга использую сервис. Почему? Потому что так отпадает множество вопросов. Не нужно париться по поводу распарсивания самой страницы. Подобные сервисы берут данные из Google через XML API и нет возни с подозрениями Google, гаданием каптчей и т.п. Просто сделали запрос и получили результат от 0 до 100 записей. Если пихать дорки в поиск Google, он быстро задастся вопросом - а чего это ты так активно пользуешься дорками? Очередной плюс парсинга через XML API Google в том, что прокси не нужны.

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

В моем случае, стоимость 1000 запросов 20 руб. Т.е. за 20 рублей можно получить до 100 000 сайтов. Хотя на практике, будут дубли, как ты с ними не борись Будут “пролазить” крупные порталы разработчиков и всякие вопросы-ответы.. Ну и не всегда можно получить сотню сайтов… бывает и ноль. Виноват, заголовок кликбейтный... Не лишним, перед запуском парсинга, глазками пробежаться по базе дорков, пройтись руками и посмотреть, какие сайты заминусить. Типа github, youtube, stackoverflow и т.п. Для каждого отдельного дорка сайты будут разные.

Можно заморочиться и написать свой код, который будет точно так же парсить Google используя XML API. Но я в этом моменте не разбирался. Единственное, нашел справку и попробовал выполнить запрос из примера, результат работы:
AD_4nXcM4tt3oGsuUnrq3jgLJ9uPQlV2trXqZubU_n85h4q3QoWHPL-syRovwFtAYFVz31DNA660MJadjgDqHPfcOjR_MUbFALgBipyIvJMM54QoY88x0OVO3Q0cQnRE0FI6LFLheai7y1TpgSMe8swT7txNWmE


Прямой парсинг Google через GAS не получится. Парсер сразу отлетает на сообщение о роботизированном трафике. Проблем приводящих к этому несколько:
1. IP давно известны "шалостями", т.к. запросы идут с определенных серверов Гугла, которыми пользовались другие люди...
2. На текущий момент, нет способа прикрутить прокси к Google. Только костыли, а в этом случае нет смысла в представленной схеме... тогда уж проще взять любую связьку, где будет использоваться какой-то вебдрайвер
3. Даже если бы прокси проходили, в официальной документации нет ничего про юзер-агенты. Народ пытается пихать, но насколько в этом есть смысл, не проверял.
Нажмите, чтобы раскрыть...

Пошаговая инструкция

Как ни странно, начинаю с создания таблицы. Вбиваю в браузере sheets.new и получаю готовую табличку. Да, если кто не в курсе, Google купил домены sheets.new для создания таблиц и doc.new для быстрого создания документов. Документ назову “Parser”

Назову лист “dorks” для дорков и создам еще один с именем “results”. Вам захотеться добавить дополнительные листы для сращивания. Например, лист содержащий регионы. Таким образом, можно было бы обойти все дорки для разных регионов поиска. Но тогда нужно дописывать кучу циклов, обходящих дополнительные листы и код становится неудобным для поддержки и оптимизации. Да и время парсинга увеличится, так как будет запущена одна очередь. Все же, рекомендую разделять и властвовать. Только для примера приведу кусок кода со сращиванием.

AD_4nXca5lBbAeyKrp7R_FI0UrEuyxPuQww_ITeBOqtMwQ3STu1yqQ5CC_rUxwhhINwdP9E6JVJaHAzfiFoXyvMMmG7cUXDDYJyTdTDy58RRL-Z5E3DupQgqkvuMMuYijb473xLX6HUAK1DbTvjc_IBzRLkYiKw5



Иду в верхнее меню Extensions -> Apps Script и попадаю в проект GAS Переименовываю, кликнув по названию, чтобы было понятно к какой таблице относится скрипт. Когда их становится под сотню, названия очень помогают.
GAS-проект, созданный таким способом, будет привязан к самой таблице, а значит будет вместе с ней копироваться!
AD_4nXd32jzKAgtl7vEN3SMfc7KwsBj03j39iqDuTRXzXskYHvqTSAuAtUQPgrznCmkSpNN5eGADGeK4VVvNU4zntz5zSQbOHOqC9Pk0C6_bG6JtMAmTWjTlMN1FRZNku0PapNd2pHIvIDx4-1SS9kd-tleFjc8



Прежде, чем идти дальше, обращу внимание на еще одно важное ограничение Google: время выполнения скрипта ограничено шестью минутами. Парсинг большого количества запросов явно превысит предел в 360 секунд. Особенно, учитывая неторопливое добавление данных в таблицу. Я выработал следующую стратегию:
  1. Скрипт запускается по триггеру. Триггер основан на времени, запуск каждые 5 минут. Триггер запускает Head версию, хотя можно заморочиться с версионностью, но у нас скрипт на 10 строчек…
  2. Скрипт будет обрабатывать лист с дорками, проходя по каждому.Номер последней строки по которой были получены данные, надо где-то хранить. Иначе будем ходить по кругу по первым строчкам. Для хранения таких вещей отлично подходят параметры скрипта.
  3. Так как триггер запускает код каждые 5 минут, чтобы один и тот же скрипт не запускался в параллели, добавляю контроль времени выполнения. Можно, конечно, повесить распараллеливание на Google, запуская скрипт на выполнение хоть каждую минуту и перед каждой итерацией запрашивать последнюю взятую в работу строку. Но может начаться хаос.

Мне удобнее, когда есть хоть какое-то разделение кода. Жму плюсик вверху слева, выбираю “Script” и переименовываю gs-файл в const. Здесь будут лежать все необходимые глобальные константы.
AD_4nXfoSD1fGKk54Nzf3z7LgNe_su9LagejfaPHNSXDQQCwr8Ap-nX-2w0Fsu1gZ8qIrgdG23a6XHlnO04Kjd4i0di_B3ZjWhQNwFckGtUrbdvARN5gQkIWVCABkSwdLYlz_-Bboj2CqJQma6wqLuW0ArI_GpM




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

1718136100457.png


Если будете пользоваться тем же сервисом, потребуются константы, которые я тщательно замазал… тщательно, т.к. Местные по обрубкам пикселей цифр смогут user id восстановить))))

JavaScript: Скопировать в буфер обмена
Code:
const API_URL = `https://xmlstock.com/google/xml/?`;
const API_KEY = `PUT_YOUR_API_KEY_HERE`;
const API_USER = 00000;

const SHEET_DORKS = `dorks`;
const SHEET_RESULTS = `results`;

const MAX_TIME_SEC = 280;

SHEET_DORKS и SHEET_RESULT сюда же, чтобы дальше было удобнее и управляемее.

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

Создаю запускающую функцию startParsing:

JavaScript: Скопировать в буфер обмена
Code:
let startTime = new Date()

function startParsing() {
  let currentDork = parseInt(ScriptProperties.getProperty('currentDork'));
  if (!currentDork) {
    ScriptProperties.setProperty('currentDork', 1);
    currentDork = 1;
  }

  startParsinп(currentDork)
}

Скрипт получает параметр из ScriptProperties, если он пустой, считает это первым сканированием и инициализирует переменные. После переходит к самому парсингу.

parseInt() используется потому как параметры текстовые, более того, при сохранении приводятся к виду “1.0”. По идее, JS должен прекрасно пониматЬ, что речь идет о единице, но в данном случае нет.
AD_4nXdGldY3w6208YCxxtmXGqGgKprI75d1vsxbxgpveYrtsl0DjualYJXgwBapsUUssgNxNfYMiCJ3PxJcixIpu4TFv55d2LcNYCVoEBE5_vYH6u4v7CIIpKavf__ousZDOGHjn7fXzHbSzVucfnJ5rinAV7Qs



Обращаю внимание на то, что первая строка задается как 1. Дело в том, что мы будем работать с таблицей гугла, а там нумерация начинается с 1, не с 0! Видмо, гугл сделал для удобства, чтобы проблемную строку таблицы было удобно искать.

Переменная startTime нужна для отслеживания времени выполнения и инициализируется при запуске скрипта.

Основная функция всего скрипта

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

JavaScript: Скопировать в буфер обмена
const xss = SpreadsheetApp.getActiveSpreadsheet();

Следующим шагом, получаю интересующие листы по их названию. Как писал вначале, это

JavaScript: Скопировать в буфер обмена
Code:
const sheetDorks = xss.getSheetByName(SHEET_DORKS);
const sheetResults = xss.getSheetByName(SHEET_RESULTS);

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

Для запуска главного цикла не хватает номера последней заполненной строки. Получить ее можно используя метод lastRow(). Но чтобы цикл прошел до конца, прибавлю единичку.

JavaScript: Скопировать в буфер обмена
const lastDork = sheetDorks.getLastRow() + 1;

Внутри цикла, первым делом, скрипт проверяет текущее время выполнения. Если мы близки к порогу, прекращаем выполнение. Далее скрипт берет нужный ключ с листа дорков. Для этого надо получить нужный диапазон, указав строку и ячейку (getRange). После вытащить из него значение через getValue().

JavaScript: Скопировать в буфер обмена
Code:
for(let i = currentDork; i < lastDork; i++) {
    let currentTime = new Date().getTime()
    let seconds = (currentTime - startTime) / 1000
 
    if (seconds > MAX_TIME_SEC) {
      console.log('Time end');
      return;
    }

    const dorkValue = sheetDorks.getRange(i, 1).getValue()
    // ...
}

Запрос к API и сохранение результатов реализую отдельными методами. Чуть позже пригодится такой подход. Да и как-то профессиональнее что ли…

JavaScript: Скопировать в буфер обмена
Code:
// ...
const result = getDataFromAPI(dorkValue);
parseJSONToSheet_(result , sheetResults, dorkValue);

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

JavaScript: Скопировать в буфер обмена
Code:
currentDork++;
ScriptProperties.setProperty('currentDork', currentDork);

Итоговая главная функция выглядит так:

JavaScript: Скопировать в буфер обмена
Code:
function startParsing(currentDork) {
  const xss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetDorks = xss.getSheetByName(SHEET_DORKS);
  const sheetResults = xss.getSheetByName(SHEET_RESULTS);
  const lastDork = sheetDorks.getLastRow() + 1;
  for(let i = currentDork; i < lastDork; i++) {
    let currentTime = new Date().getTime();
    let seconds = (currentTime - startTime) / 1000;
 
    if (seconds > MAX_TIME_SEC) {
      console.log('Time end');
      return;
    }
    const dorkValue = sheetDorks.getRange(i, 1).getValue();
    const result = getDataFromAPI(dorkValue);
    parseJSONToSheet_(result , sheetResults, dorkValue);

    currentDork++;
    ScriptProperties.setProperty('currentDork', currentDork);
  }
}

Функция сохранения:
Для сохранения информации предпочитаю appendRow(). Есть другой вариант, получать последний range и писать данные в него через setValues(). Но тогда придется самому контролировать с каким диапазоном работать, не перезаписываю ли какие-то данные и не надо ли в лист добавить строк. Второй подход, по ощущениям, работает чуть быстрее, но как-то лениво его использовать…

JavaScript: Скопировать в буфер обмена
Code:
function parseJSONToSheet_(json, sheet, dork) {
  const {results} = json
  for(let i = json.first; i <= json.last; i++) {
    sheet.appendRow(['', '', results[i].url, results[i].title, results[i].passage, ,dork]);
  }
}

Функция проста, как банка огурцов. Из всего json забираем только results После проходим циклом, выгружая значения объекта в табличку. Первые две ячейки оставляю пустыми. К ним вернемся позже, при нормализации данных.

Последней реализую функцию запроса к API. В ней нет ничего сложного. Для выполнения запросов в GAS есть объект URLFetch. Просто вызываю его метод fetch() с нужными параметрами. Из функции возвращаю тело, преобразованное в JSON.
JavaScript: Скопировать в буфер обмена
Code:
function getDataFromAPI(dork) {
  const url = `${API_URL}?user=${API_USER}&key=${API_KEY}&groupby=100&domain=37&device=desktop&hl=en&lr=2840&filter=1&query=${encodeURIComponent(dork)}`;
 
  const response = UrlFetchApp.fetch(url);
  const content = response.getContentText();
  const json = JSON.parse(content);
  return json;
}

Для тех, кто не знаком или плохо знаком с JS — в url помещается строка, которая зажата в литеральные кавычки (буква ё с английской раскладкой). Эти кавычки позволяют использовать подстановки ${...} прям как в BASH. Ну или как f”{...}” в Python. Внутри может быть переменная, вызов функции и т.п. Все значения, которые могут подставляться параметрам, взяты из сервиса. В данном случае мы получаем максимум 100 значений (максимум сервиса), 37 это домен google.com, lr — регион США.
AD_4nXcwtAMf83DpPmrXqvbXpp02IMLvb72F1l4Kn4gmJh2poFoz13W4Agbtg04sjt_KvKksc9am8C3DP2spEMrV2sl-05cqC17kN7E37atZ4nkrOBGRTnQ9vW4xmgdIaNBaIzlZoBv2t9RAFYn1bKuSF5IeDQYi


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

Первый запуск


Итак, у нас получилось четыре функции, которые полностью реализу нужный нам парсинг. Можно жать на кнопу “Run” и…. не спешим радоваться и не отчаиваемся. Google хочет убедиться, что мы действительно понимаем, что запускаем скрипт и для этого просит подтверждения.

AD_4nXeQ1EqdBkoc4BOA1cN4NtFFWjg3YU2j2SSc2ZvWzDe03LuIZID9dSyTXpehcI3GvO_2FqoL8nN_80vRs6omPXbGiSmhqC1-7Dz8D09UJnQd_ChBE6_acUya6xuzVKzcyT8-sWQyyLFALnColzUavB_42jc



Жмем “Review permissions” и выбираем нужный аккаунт для авторизации. После жмем на слабо заметную ссылку Advanced.

1718136563919.png



Да, Google постарался максимально усложнить процесс запуска скрипта, чтобы усложнить жизнь честным хацкерам и скамерам. Жмем на Go … (unsafe)

AD_4nXfQH9qllGGEMoSgf1v-9YJO-0AiS4IaXGKjxAiqRG_Qzsu9QjRVLHcYyMswsXFK75wBsu-ZjIXGmVxrpbMyy31P8o4MnAM6ayJCCwxzsnmLHnchGjypPT8SfmF96Ojyhv5ld18eN4RFf9YDOutd9ggSNqWl


После нажатия Allow, на почту прилетит письмо о предоставленном доступе и скрипт, наконец-то, выполнится. Должен выполниться без ошибок, если все написано правильно и есть баланс. Если что-то пошло не так, внизу появится ошибка.

Что делать в случае ошибки?
  1. Самый действенный совет олдовых админов —перезагрузи. Да, бывает такая фигня, что Google тупит и не может запустить скрипт. Закрываем скрипт, закрываем табличку, после открываем снова.
  2. Что-то с вашим сервисом парсинга. Проверьте, что все параметры прописаны верно. Добавьте в функцию запроса, сразу после инициализации url строчку console.log(url)и посмотрите верна ли ссылка. Протестируйте полученную ссылку руками.
  3. Если проблемы на этапе сохранения, проверьте правильно ли скрипт обрабатывает ответ вашего сервиса.
  4. Ничего не помогло? Пишите здесь, по возможности отвечу.

Триггер для автозапуска

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

AD_4nXdAffE88hZ1iWPETrgl7xLDwuw44pzFi6BRrwYgbPwcAcZkQnyFzpUd_kuneZC5WpcgJXKA7NUX8PdtpzPOa5tlJmf9Os5ljGHG6-uh3SzxVzQHwNHiqM_NDOvmiuCxYg_QNMlhIzGbgFi54uhq5xqrnVY



Все, каждые 5 минут будет запускаться функция startParsing. Версия Head - это исходники. Time-driven, соответственно, запуск по времени. Запуск по минутам, каждые пять минут. Отчет о запусках ежедневно.

К слову об отчетах, в левой панели, прямо по часиками есть пункт “Execute” (запуски). Это полноценный лог всех запусков проекта. Там есть время запуска, время выполнения, тип запуска и куча полезной информации. Чтобы посмотреть ошибки, жмем на нужный запуск. Но главное, что все консоль логи попадают сюда...

1718137149214.png



Логирование проекта

В большинстве случаев, достаточно выводить данные в консоль, через console.log() или Logger.log(). Но бывают ситуации, когда таким образом данные не удастся получить, а распечатка данных нужна. Или, например, нужен быстрый доступ к списку запросов полученных через doGet() или doPost(). В этом случае, можно сделать отдельный лист “log” и добавлять на него данные через appendRow[]
JavaScript: Скопировать в буфер обмена
Code:
const xss = SpreadsheetApp.getActiveSpreadsheet();
const sheetLog = xss.getSheetByName(`log`);
sheetLog.appendRow([new Date(),’logLevel’ , ‘Data to log’]);

Ускоряем парсинг

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

AD_4nXeHBC7CK7SMDNoI7hWpSnxPXGU2cT8_eqDwSYmEjhcxatNr7WAl-qo6oKfKMyWpZWvL3l01UWql080MxbYd13aeWMzsSvHIk6lwBPIujbqjjYlB4WwozwSsaiPDeJsLvUiNv2FAewlm7QuKhisaRr-FPxw6



AD_4nXeb3iDJMVRzp6QN7AbGYvooJEzuLU2l5BGQD3AdF4K5N5PJyGvb4JmY2TlJNyZZGXNKOzzldae_a_BDSN5RA9vLCe3jMVVzoVqzHaSS602b0yYea4OzPlO2mi3rERuToZjh3wM-fiYglwN5aGiNhY1ML1p0



Получение данных из таблицы GET-запросом без заморочек

Отлично, сайты парсятся и можно руками что-то с ними делать. Но разве ради этого мы всю эту историю с автоматизацией придумали? Чтобы парсить, а потом руками куда-то переносить? Нет, поэтому напишем простой код для получения данных. В этом нам помогут быстрые триггеры doGet() или doPost(). Чем они занимаются, понятно из названий — обрабатывают GET и POST запросы к нашему веб-приложению.

Вэб приложени? Да! Фишка скриптов Google в том, что их можно опубликовать, как полноценное приложение. Более того, можно даже веб-морду прикрутить, но это не является темой нашего урока, поэтому не отвлекаемся.

Для начала напишем простую функцию получения данных:
JavaScript: Скопировать в буфер обмена
Code:
function doGet(e) {
  return ContentService.createTextOutput('XSS.is').setMimeType(ContentService.MimeType.TEXT)
}

Чтобы приложение GAS дало нам ответ, нам нужно сделать правильные return из doGet(). В этом нам поможет интерфейс ContentService. Функция createTextOutput сформирует правильный HTTP-ответ. Не важно, возвращаем мы просто текст, CSV или JSON, нужна именно текстовая функция. Ну и, как видно из кода, чтобы задать конкретный Content-Type, добавляем его через setMimeType используя константы хранящиеся в ContentService.MimeType.

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

Справа вверху жмем Deploy > New Deployment и видим такое окошко.

AD_4nXfkI-oZXipMB0PiN2v8lKK_ajoXm70I_M8LvTBSf2jLjbR8KJb8RnfCRRENKDWS-GpcRoFp_j9bHN-1AWlTwA2ibR01gt3lGEelsy0zfwjojEL5Dxgju8vXVl-k5x7HAf3zJK01SVKE6Wry8fiV4g-K1I9z



Кликаем на шестеренку слева, выбираем “Web app”. Указываем от чьего имени будет выполняться приложение. В нашем случае выбираем Me. В “Who has access” указываем “Anyone”. Именно такие параметры, так как нам нужен простой прямой доступ к данным. В ином случае, надо заморачиваться с авторизациями и правами. А так, делаем из Python обычный get и как хотим оперируем данными.

AD_4nXems1nUxXOIdNShMa0ubchmVBXU4SBlAYW-0RGyyi6TkEk1q4E7mSTUXXaqSJfTv9pcPs2mWmwSz3127psg-V-E8dW1ei6JXCBxy8LtH7dAtVufVEIkyr6gLjgbMYnDe0L7FzoopBsCymqHPKCHah2y_SGD



На последнем шаге, Google дает нам идентификатор веб-приложения и ссылку для доступа. Копируем ссылку и жмем Done. Переходим по ссылке и видим надпись “XSS.is”. Ура, веб-приложение как-то но работает.

AD_4nXdB0rz_lpnxhgNA27LYTtejU-tQ9ny0eG8jA_Z30T4f8qqVww5p8cjZ_2zwx4Tm5Rnb5WAnEN6EcbSq8qTagTa8ZcVhuG6ODb_wsF8ubu_2LysbzufVJB2gZxvnrpWkWoKBp4A-9dwpgbx9QwKAJahlcymU



Заставим функцию делать то, что нужно нам:
  1. Получать get-параметры offset и count, чтобы можно было получать данные по частям. Все же, перекидывать десятки тысяч строк — это моветон.
  2. Формировать осмысленный JSON, с которым потом будет удобно работать в других скриптах
  3. Возвращать осмысленные данные из таблицы

Параметры получаю следующим образом:

JavaScript: Скопировать в буфер обмена
let {offset,count} = e.parameters;


Объект, который получаем на входе содержит в себе ряд важных свойств. При работе с GET-параметрами, чаще всего нужен parameters. Из него, методом десириализации, получаю две нужных переменных. Если бы мы работали с doPost() и входными данными POST-запроса, мы бы брали данные из e.postData.contents. Эта информация для тех, кто хочет углубиться, добавив функционала.

Следующим шагом, получаю ссылку на таблицу уже известным способом:
JavaScript: Скопировать в буфер обмена
Code:
const xss = SpreadsheetApp.getActiveSpreadsheet();
const sheetResults = xss.getSheetByName(SHEET_RESULTS);

Далее делаю пару проверок. Во-первых, если у нас offset больше или равен количеству строк, можно сразу вернуть пустой объект. Во-вторых, проверю, чтобы значение офсета было больше 0 (помните, что в таблицах данные с единички?). Ну и ограничу максимальное количество в ответе тысячей строк и сделаю проверку на забывчивость:
JavaScript: Скопировать в буфер обмена
Code:
if (offset >= sheetResults.getLastRow()) {
    return ContentService.createTextOutput({success:true, count: 0, results:[]}).setMimeType(ContentService.MimeType.JSON)
}

if (!offset || offset < 1) offset = 1
if (!count || count > 1000) count = 1000;

Остается только получить данные из таблицы и вернуть их:
JavaScript: Скопировать в буфер обмена
Code:
const results = sheetResults.getRange(offset, 1, count).getValues().map(el => el[0]).filter(Boolean)
return ContentService.createTextOutput(JSON.stringify({success:true, count: results.length, results})).setMimeType(ContentService.MimeType.TEXT);

Как и раньше, используем getRange(). Отличие только в третьем параметре, им мы указываем количество нужных строк. Если бы и ячеек надо было несколько, дописали бы четвертый параметр. Дальше работает функция getValues(), которая возвращает массив массивов. В нашем случае это выглядит так:

[[‘https://google.com’], [‘https://yandex.ru’’]]
Нажмите, чтобы раскрыть...

Соответственно, нам нужно сделать массив плоским, для чего и нужна функция map(el => el[0]), которая в сущности просто возвращает, вместо массива значений, одно значение, которые упаковываются в обычный массив строк.

Возврат ContentService уже знаком, разве что передаем объект, который конвертируется в строку через JSON.stringify(). Сам объект выглядит так:

JSON: Скопировать в буфер обмена
Code:
{
    success: true,
    count: results.length,
    results
}

Можно не париться и возвращать просто строками. Все зависит от ваших задач и предпочтений. Мне удобнее получить полноценный объект, который можно удобно построчно обрабатывать. Но если, например, предполагается дальнейшая загрузка в тот же Acunetix через CSV-файлы можно сделать так:

JavaScript: Скопировать в буфер обмена
return ContentService.createTextOutput(results.join(‘,\n’)).setMimeType(ContentService.MimeType.CSV);

А в принимающем скрипте на Python просто напрямую писать в нужный файл. Разве что, ограничить количество строк в 500, т.к. из csv окунь больше не принимает.

Скрипт готов, осталось только сделать новый деплой: Deploy > Manage Deployments, в появившемся окне жмем на карандашик, в версиях выбираем New Version и жмем Deploy. После этого, скрипт будет полноценно работать.

Спойлер: Вся функция doGet(e)
JavaScript: Скопировать в буфер обмена
Code:
function doGet(e) {
  let {offset,count} = e.parameters;
  const xss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetResults = xss.getSheetByName(SHEET_RESULTS);
  if (!offset || offset < 1) offset = 1
  if (!count || count > 1000) count = 1000;
  if (offset >= sheetResults.getLastRow()) {
    return ContentService.createTextOutput({success:true, count: 0, results:[]}).setMimeType(ContentService.MimeType.JSON)
  }
 
  const results = sheetResults.getRange(offset, 1, count).getValues().map(el => el[0]).filter(Boolean)
  return ContentService.createTextOutput(JSON.stringify({success:true, count: results.length, results})).setMimeType(ContentService.MimeType.TEXT);
}

Пример результата:
JSON: Скопировать в буфер обмена
Code:
{
    "success": true,
    "count": 3,
    "results": [
        "wipach.si","flutacious.com","naveenautomationlabs.com"
    ]
}

Останется дописать скрипт, например, на Python, который будет перегружать данные из таблицы в тот же Acunetix. Подробнее о том, как создавать таргеты в окуне и генерить новые сканирования, читайте в этой статье. Здесь просто приведу короткий скрипт на питоне, без детальных пояснений, так как они излишни. Единственное, обращу внимание на то, где взять ID приложения. Помните мы делали деплой? Там в окне был нужный нам ID. Для его получения можно зайти в Deploy -> Manage Deployment
1718138373225.png


Python: Скопировать в буфер обмена
Code:
import requests
deployment_id = 'your_deployment_id'
offset = 0
count = 100
url = f'https://script.google.com/macros/s/{deployment_id}/exec?offset={offset}&count={count}'
response = requests.get(url=url)
if response.status_code ==200:
    print(response.text)

Нормализация данных

Парсить научились, отдавать данные тоже. Но есть нюанс — URL’ы являются полноценными ссылками, с указанием путей и GET-параметров. Много где это может мешать. Например, sqlmap полезно дать полный урл, Acunetix надо бы домен, а какому-нибудь DNS-дамперу хост. Нужно все это дело нормализовать и привести к удобному виду.

В оригинале, предпочитаю чтобы у меня были следующие данные:
  1. Хост
  2. Полный домен
  3. Полный URL страницы
  4. Title
  5. Description
  6. Другие полезные данные, например, статистика посещаемости.
Поправлю, слегка, код парсера. Добавлю функцию, которая вытащит нужные данные из url страницы и заменю пустые значения appendRow[] подстановками.

JavaScript: Скопировать в буфер обмена
Code:
function getClearURLData(url) {
  const [protocol, tail] = url.split(':');
  const host = tail.replace('//','').split('/')[0];
  return {
    protocol, host, domain: protocol.concat('://', host)
  }
}

function parseJSONToSheet_(json, sheet, dork) {
  const {results} = json;
  for(let i = json.first; i <= json.last; i++) {
    const clearURLData = getClearURLData(results[i].url)
    console.log('Append data: ', [results[i].url, results[i].title, results[i].passage, ,dork])
    sheet.appendRow([clearURLData.host, clearURLData.domain, results[i].url, results[i].title, results[i].passage, ,dork]);
  }
}

Все, что делает getClearURLData():
1. Выделяет из урла протокол
2. Разбивает оставшийся после первой операции хвост, и берет оттуда первый элемент - это хост
3. Собирает все обратно в удобный объект.

Парсинг без использования сервисов

Вероятно, у вас возникнет желание парсить что либо еще, без использования сервисов и API, а в лоб через DOM. Тогда на помощь нам придет возможность подключать сторонние библиотеки, а именно Cheerio. Вот ссылка на проект Cheerio для GAS https://github.com/tani/cheeriogs

Чтобы подключить его, в проекте GAS слева жмем плюсик возле надписи Libraries. В появившееся окно вбиваем идентификатор библиотеки 1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0 Это точно такой же ID, который нам выдает Deploy приложения.

AD_4nXfQp7mixHcl5ixIPnoMsXhuPL5t90eNEqK1Zd4eCcevP2AC9kzzhZjBEoKyHpS3Wco5M4k-zQEdoEByVoJCF0U_oNPoQawtK8z9k0AWFjgGVEwwvofskq6oqATYGPO4Ls5QViGFdlg_emuzAQquPWILUn-z



После нажатия на Look up, окно приобретает такой вид. Оставляем последнюю версию и жмем Add. Если потребуется, всегда можно будет кликнуть на библиотеку и заменить версию.

Теперь доступен объект Cheerio со всем его функционалом. Вот пример использования из справки:

JavaScript: Скопировать в буфер обмена
Code:
const content = UrlFetchApp.fetch('https://en.wikipedia.org').getContentText()
const $ = Cheerio.load(content);
Logger.log($('p').first().text());

После обработки контента через Cheerio, становится доступна работа с контентом, практически как с обычным DOM через jQuery. Для примера приведу парсер прокси с одного из тонны сайтов-листингов бесплатных прокси. Пример намеренно выстроен таким образом, чтобы максимально просто показать работу с библиотекой:

JavaScript: Скопировать в буфер обмена
Code:
function parseProxy() {
  const url = `https://freeproxyupdate.com/fast-response-proxy`;
  const html = UrlFetchApp.fetch(url).getContentText();
  console.log(html)
  const $ = Cheerio.load(html);
  const table = $('.list-proxy > tbody').first();
  const rows = $(table).find('tr').toArray();
  const proxyData = rows.map(el => $(el).find('td').toArray())
                        .map(cells => [$(cells[0]).text(), $(cells[1]).text()])
  console.log(proxyData)
}

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

[ [ '167.114.222.149', '27182' ], [ '167.114.222.144', '27182' ], [ '\n\n\n\n', '' ], [ '64.201.163.133', '80' ], [ '138.199.48.1', '8443' ], [ '138.199.48.4', '8443' ], [ '51.124.209.11', '80' ], [ '201.174.239.31', '4153' ], [ '195.189.62.5', '80' ]]
Нажмите, чтобы раскрыть...

Итоги

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

Можно прикрутить не только парсинг сайтов, но и наполнение базы нужными данными: статистика посещаемости, ДНС-реверс, фаззинг и т.д. Можно прикрутить различные сервисы для сбора данных так же по API или варварски через Cheerio, Можно внешними скриптами наполнять данные по результатам работы инструментов (например, все тот же окунь). У вас есть механизм, который может полноценно работать сам по себе, не требуя ваших ресурсов и вмешательства.

Никто не мешает в контент сервисе указать MIME-type “JAVASCRIPT” и через вебприложение гугла отдавать полноценный скрипт. Да, видимо в борясь с хацкерами, которые использовали подобное для XSS атак для обхода политик безопасности, Google перенес приложения на домен script.googleusercontent.com но в любом случае, подобное хранилище скриптов может оказаться полезным. Как минимум, не нужны сервера, не нужен отдельный домен.

Те же телеграм-боты спокойно цепляются к GAS вебхуком. А все остальная инфраструктура Google? Мы ведь даже не коснулись ее. Между тем, мне в смартфон до сих пор ежедневно сыплются десятками уведомления от календаря по типу “Аня отправила вам видео” или “Сбербанк: поступил перевод”. Не лазил в этим темы, но скорее всего, реализованы они именно через GAS.

Я постарался максимально понятно и подробно донести свои мысли. Если интересно развитие темы применения GAS в нашей сфере, дайте знать любым удобным способом и я обязательно выдам что-то очень интересное.

Спойлер: Полный код code.gs
JavaScript: Скопировать в буфер обмена
Code:
let startTime = new Date().getTime();

function doGet(e) {
  let {offset,count} = e.parameters;
  const xss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetResults = xss.getSheetByName(SHEET_RESULTS);

  if (!offset || offset < 1) offset = 1
  if (!count || count > 1000) count = 1000;

  if (offset >= sheetResults.getLastRow()) {
    return ContentService.createTextOutput({success:true, count: 0, results:[]}).setMimeType(ContentService.MimeType.JSON)
  }
  
  const results = sheetResults.getRange(offset, 1, count).getValues().map(el => el[0]).filter(Boolean)
  return ContentService.createTextOutput(JSON.stringify({success:true, count: results.length, results})).setMimeType(ContentService.MimeType.TEXT);
}

function resetDorkRow() {
  ScriptProperties.setProperty('currentDork', 1);
}

function startParsing() {
  let currentDork = parseInt(ScriptProperties.getProperty('currentDork'));
  if (!currentDork) {
    currentDork = 1;
    ScriptProperties.setProperty('currentDork', currentDork);
  }

  startParsing(currentDork);
}

function getDataFromAPI(dork) {
  const url = `${API_URL}?user=${API_USER}&key=${API_KEY}&groupby=100&domain=37&device=desktop&hl=en&lr=${API_REGION}&filter=1&query=${encodeURIComponent(dork)}`;
  console.log('Start fetching by url: ', url);
  const response = UrlFetchApp.fetch(url);
  const content = response.getContentText();
  console.log('Response:');
  console.log(content);
  const json = JSON.parse(content);
  return json;
}

function getClearURLData(url) {
  const [protocol, tail] = url.split(':');
  const host = tail.replace('//','').split('/')[0];
  return {
    protocol, host, domain: protocol.concat('://', host)
  }
}

function parseJSONToSheet_(json, sheet, dork) {
  const {results} = json;
  for(let i = json.first; i <= json.last; i++) { 
    const clearURLData = getClearURLData(results[i].url)
    console.log('Append data: ', [results[i].url, results[i].title, results[i].passage, ,dork])
    sheet.appendRow([clearURLData.host, clearURLData.domain, results[i].url, results[i].title, results[i].passage, ,dork]);
  }
}

function startParsing(currentDork) {
  const xss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetDorks = xss.getSheetByName(SHEET_DORKS);
  const sheetResults = xss.getSheetByName(SHEET_RESULTS);
  const lastDork = sheetDorks.getLastRow() + 1;

  for(let i = currentDork; i < lastDork; i++) {
    let currentTime = new Date().getTime();
    let seconds = (currentTime - startTime) / 1000;
  
    if (seconds > MAX_TIME_SEC) {
      console.log('Time end');
      return;
    }

    const dorkValue = sheetDorks.getRange(i, 1).getValue();
    const result = getDataFromAPI(dorkValue);
    parseJSONToSheet_(result , sheetResults, dorkValue);

    currentDork++;
    ScriptProperties.setProperty('currentDork', currentDork);
  }
}

Спойлер: Код const.gs
JavaScript: Скопировать в буфер обмена
Code:
const API_URL = `h_ttps://xmlstock.com/google/json/`;
const API_KEY = `your_api_key`;
const API_USER = your_user_id;
const API_REGION = 2840;

const SHEET_DORKS = `dorks`;
const SHEET_RESULTS = `results`;

const MAX_TIME_SEC = 280;
 
Бомба! То-что искал!) С без пяти минут приобретённым A-парсером) Буду тестить
Супер статьи 👍
 
Vandalism сказал(а):
Бомба! То-что искал!) С без пяти минут приобретённым A-парсером) Буду тестить
Супер статьи 👍
Нажмите, чтобы раскрыть...
Рад, что статьи были полезными. A-Parser, вероятно, есть смысл покупать. Если, конечно, речь не только о парсинге Google. У него куча полезных возможностей, если ими пользоваться))) У меня просто тонна всякого софта накуплена была, но лежит без дела.
 
Top