• XSS.stack #1 – первый литературный журнал от юзеров форума

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

petrinh1988

X-pert
Эксперт
Регистрация
27.02.2024
Сообщения
243
Реакции
493
Автор petrinh1988
Источник https://xss.pro


Для атак на веб-приложения, нужна база сайтов, вот мой вариант массового сбора по доркам. Кроме того, считаю, что 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 Скрипт может быть привязан к той же таблице (создаваться из нее), тогда при копировании таблицы будет копироваться и скрипт.
1724257258965.png



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

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. Но я в этом моменте не разбирался. Единственное, нашел справку и попробовал выполнить запрос из примера, результат работы:
1724257277026.png

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

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

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

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

1724257298875.png


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

1724257316791.png


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

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

1724257332843.png


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

1718136100457.png

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

JavaScript:
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 сюда же, чтобы дальше было удобнее и управляемее.

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

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

JavaScript:
let startTime = new Date()

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

  startParsinп(currentDork)
}

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

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

1724257364101.png



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

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

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

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

JavaScript:
const xss = SpreadsheetApp.getActiveSpreadsheet();

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

JavaScript:
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:
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:
// ...
const result = getDataFromAPI(dorkValue);
parseJSONToSheet_(result , sheetResults, dorkValue);

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

JavaScript:
currentDork++;
ScriptProperties.setProperty('currentDork', currentDork);

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

JavaScript:
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:
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:
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 — регион США.

1724257626599.png


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

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


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

1724257613007.png


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

1718136563919.png


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

1724257599311.png

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

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

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

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


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

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

1718137149214.png


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

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

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

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

1724257579131.png


1724257566382.png


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

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

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

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

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

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

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

1724257550877.png


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

1724257534431.png


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

1724257515508.png


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

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

JavaScript:
let {offset,count} = e.parameters;


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

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

Далее делаю пару проверок. Во-первых, если у нас offset больше или равен количеству строк, можно сразу вернуть пустой объект. Во-вторых, проверю, чтобы значение офсета было больше 0 (помните, что в таблицах данные с единички?). Ну и ограничу максимальное количество в ответе тысячей строк и сделаю проверку на забывчивость:
JavaScript:
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:
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:
{
    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. После этого, скрипт будет полноценно работать.

JavaScript:
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:
{
    "success": true,
    "count": 3,
    "results": [
        "wipach.si","flutacious.com","naveenautomationlabs.com"
    ]
}

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

Python:
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:
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 приложения.

1724257464191.png


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

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

JavaScript:
const content = UrlFetchApp.fetch('https://en.wikipedia.org').getContentText()
const $ = Cheerio.load(content);
Logger.log($('p').first().text());

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

JavaScript:
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 в нашей сфере, дайте знать любым удобным способом и я обязательно выдам что-то очень интересное.

JavaScript:
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 initParsing() {
  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);
  }
}

JavaScript:
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;
 

Вложения

  • 1724257487887.png
    1724257487887.png
    44.8 КБ · Просмотры: 58
Последнее редактирование:
Бомба! То-что искал!) С без пяти минут приобретённым A-парсером) Буду тестить
Супер статьи 👍
 
Бомба! То-что искал!) С без пяти минут приобретённым A-парсером) Буду тестить
Супер статьи 👍
Рад, что статьи были полезными. A-Parser, вероятно, есть смысл покупать. Если, конечно, речь не только о парсинге Google. У него куча полезных возможностей, если ими пользоваться))) У меня просто тонна всякого софта накуплена была, но лежит без дела.
 
Очень интересно, но ничего не понятно.. запустить так и не вышло)
)))) Это вывод какой конкретно функции? Как пытались запустить? Нужны подробности.
 
А для чего парсите? Какие варианты использования
Первое предожение статьи -"Для атак на веб-приложения" :)
 
Для парсинга использую сервис. Почему? Потому что так отпадает множество вопросов. Не нужно париться по поводу распарсивания самой страницы. Подобные сервисы берут данные из Google через XML API и нет возни с подозрениями Google, гаданием каптчей и т.п. Просто сделали запрос и получили результат от 0 до 100 записей. Если пихать дорки в поиск Google, он быстро задастся вопросом - а чего это ты так активно пользуешься дорками? Очередной плюс парсинга через XML API Google в том, что прокси не нужны.
Можете подсказать в лс сервис?
 
Можете подсказать в лс сервис?
В коде есть ссылка:
JavaScript:
const API_URL = `https://xmlstock.com/google/xml/?`;

Хотя по хорошему, лучше бы заморочиться и самому организовать подобную историю. Правда руки никак не доходят.
 
Прошу прощения, пытаюсь запустить, но ничего не происходит. Обращения к xmlstock тоже. Если очень коротко, всё как в статье, есть таблица парсер с 2 листами dorks и result. 2 gs code and const. Данные в конст прописаны. Запукаю startparsing, получаю такой ответ -
Примечание
Выполнение начато и Выполнение завершено. Но ничего в листе result нету...
 
Прошу прощения, пытаюсь запустить, но ничего не происходит. Обращения к xmlstock тоже. Если очень коротко, всё как в статье, есть таблица парсер с 2 листами dorks и result. 2 gs code and const. Данные в конст прописаны. Запукаю startparsing, получаю такой ответ -
Примечание
Выполнение начато и Выполнение завершено. Но ничего в листе result нету...
Я о чем-то замечтался, когда писал код и сделал две функции startParsing(). Запускать нужно ту, которая в модуле http:

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

Она получает начальную позицию на листе дорков, если нет, то берет 1 и запускает вторую startParsing(). Такая коллизия произошла, сам не заметил, а из-за особенностей GAS, он и не ругается вовсе. По хорошему, эту функцию, которая фактически инициализирует, стоит назвать initParsing() или как-то так.

99%, что проблема в этом. В ином случае нужно дебажить. Там есть встроенный дебагер, но если лень разбираться, можно просто консольлогать любые значения (console.log())

P.S.
Внес правки
 
Последнее редактирование:
Я о чем-то замечтался, когда писал код и сделал две функции startParsing(). Запускать нужно ту, которая в модуле http:

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

Она получает начальную позицию на листе дорков, если нет, то берет 1 и запускает вторую startParsing(). Такая коллизия произошла, сам не заметил, а из-за особенностей GAS, он и не ругается вовсе. По хорошему, эту функцию, которая фактически инициализирует, стоит назвать initParsing() или как-то так.

99%, что проблема в этом. В ином случае нужно дебажить. Там есть встроенный дебагер, но если лень разбираться, можно просто консольлогать любые значения (console.log())

P.S.
Внес правки
Попробовал, теперь такая ошибка и это кстати тоже непонятно в тексте. "Теперь, если надо поменять регион парсинга, можно это сделать в полтора клика, заменив константу, а не копать код в поисках нужной строчки." Где поменять?
ReferenceError: API_REGION is not defined
getDataFromAPI
@ code.gs:36
startParsing
@ code.gs:79
initParsing
@ code.gs:32
 
Завелось, добавил пропущенную константу
const API_REGION = 'de';

+ в константах изменил строчку
const API_URL = `https://xmlstock.com/google/json/`;

иначе там два знака вопроса и xml у вас прописан. Вроде завелось, спасибо за статью!
 
Запустил скрипт, спарсил данным способом ради интереса. Слишком гемморно как по мне. Непонятно для чего такие извращения?))
Простенький php скрипт накидал с парсингом через апи xml сервисы и записью в sqlite. Любой дешманский vps за пару баксов для этих целей подойдет. Даже на "пирожке" можно запустить свой скрипт парсинга. Будет работать нонстоп круглосуточно пока все не спарсится))
 
Запустил скрипт, спарсил данным способом ради интереса. Слишком гемморно как по мне. Непонятно для чего такие извращения?))
Простенький php скрипт накидал с парсингом через апи xml сервисы и записью в sqlite. Любой дешманский vps за пару баксов для этих целей подойдет. Даже на "пирожке" можно запустить свой скрипт парсинга. Будет работать нонстоп круглосуточно пока все не спарсится))
Пока все не спрасится, либо пока все не накроется и надо будет снова парсить.

Не особо понял смысл сообщения. Он в том, что существует огромное количество вариантов реализации? Так это и без того понятно. Точно так же можно сказать: "-Нафига писать на php, когда можно использовать Bash, в том числе с записью в sqlite?". Статья про конкретное решение, которое имеет свои плюсы и минусы. Не более того. Ровно как в варианте с php-скриптом.
 
Пока все не спрасится, либо пока все не накроется и надо будет снова парсить.

Не особо понял смысл сообщения. Он в том, что существует огромное количество вариантов реализации? Так это и без того понятно. Точно так же можно сказать: "-Нафига писать на php, когда можно использовать Bash, в том числе с записью в sqlite?". Статья про конкретное решение, которое имеет свои плюсы и минусы. Не более того. Ровно как в варианте с php-скриптом.
Смысл в том, что быстрее и проще применить, то и использовать для парсинга дорков. В моем случае это быстро накидать php скрипт с записью в sqlite или в текстовый файл закинуть. В основном дорки парсят под конкретные задачи и особо много дорков не будет. Максимум тыс. 10 дорков под определенные движки к примеру cms. Это выйдет в рублей 80-150 за парсинг через xml сервис.
надо будет снова парсить.
Не надо заново парсить. Спаршенные строки с дорками удаляются из файла, начнется оттуда где был прерван парсинг.
 
Хороший метод, спасибо👍

Гугл успел убрать параметр кол-ва результатов на странице (num=100), приходится по страницам собирать отдельными запросами по 10 штук с каждой
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх