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

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

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

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

Что такое ГАЗ?

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

Таблицы, Документы, Диск, Gmail, Календарь — этим всем можно спокойно оперировать с помощью скриптов. Сам по себе язык, это одна из реализаций Javascript. Поэтому, если есть базовые знания JS, никаких проблем не возникает. Чтобы написать полноценные решения, нужно будет просто посмотреть, какие объекты (интерфейсы) представляет собой ГАЗ. Ну и разобраться с некоторым переносным устройством, а также с заморочными правами доступа.

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



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

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

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

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

Как ни странно, начинаю создавать таблицы. Вбиваю в браузер листы.Новые и получаю готовую табличку. Да, если кто не в курсе, Google купил домены Sheets.new для создания таблиц и doc.new для быстрого создания документов. Документ назову «Парсер»

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

View attachment 79858


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

View attachment 79859


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

Я стабилизировался, когда есть какой-нибудь отдельный код. Жму плюсик вверх слева, выбираю «Скрипт» и переименовываю gs-файл в const. Здесь будут оставлены все необходимые глобальные константы.



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


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

JavaScript:Скопировать в буфер обмена
<span>const</span> <span>API_URL</span> <span>=</span> <span><span>`</span><span>https://xmlstock.com/google/xml/?</span><span>`</span></span><span>;</span><br><span>const</span> <span>API_KEY</span> <span>=</span> <span><span>`</span><span>ВСТАВЬТЕ_СЮДА_ВАШ_API_KEY</span><span>`</span></span><span>;</span><br><span>const</span> <span>API_USER</span> <span>=</span> <span>00000</span><span>;</span><br><br><span>const</span> <span>SHEET_DORKS</span> <span>=</span> <span><span>`</span><span>dorks</span><span>`</span></span><span>;</span><br><span>const</span> <span>SHEET_RESULTS</span> <span>=</span> <span><span>`</span><span>results</span><span>`</span></span><span>;</span><br><br><span>const</span> <span>MAX_TIME_SEC</span> <span>=</span> <span>280</span><span>;</span>

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

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

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

JavaScript:Скопировать в буфер обмена
<span>let</span> startTime <span>=</span> <span>new</span> <span>Date</span><span>(</span><span>)</span><br><br><span>function</span> <span>initParsing</span><span>(</span><span>)</span> <span>{</span><br> <span>let</span> currentDork <span>=</span> <span>parseInt</span><span>(</span>ScriptProperties<span>.</span><span>getProperty</span><span>(</span><span>'currentDork'</span><span>)</span><span>)</span><span>;</span><br> <span>if</span> <span>(</span><span>!</span>currentDork<span>)</span> <span>{</span><br> ScriptProperties<span>.</span><span>setProperty</span><span>(</span><span>'currentDork'</span><span>,</span> <span>1</span><span>)</span><span>;</span><br> currentDork <span>=</span> <span>1</span><span>;</span><br> <span>}</span><br><br> <span>startParsinп</span><span>(</span>currentDork<span>)</span><br><span>}</span>

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

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

View attachment 79860



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

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

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

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

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

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

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

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

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

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

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

JavaScript:Скопировать в буфер обмена
<span>for</span><span>(</span><span>let</span> i <span>=</span> currentDork<span>;</span> i <span><</span> lastDork<span>;</span> i<span>++</span><span>)</span> <span>{</span><br> <span>let</span> currentTime <span>=</span> <span>new</span> <span>Date</span><span>(</span><span>)</span><span>.</span><span>getTime</span><span>(</span><span>)</span><br> <span>let</span> seconds <span>=</span> <span>(</span>currentTime <span>-</span> startTime<span>)</span> <span>/</span> <span>1000</span><br> <br> <span>if</span> <span>(</span>секунд <span>></span> <span>MAX_TIME_SEC</span><span>)</span> <span>{</span><br> console<span>.</span><span>log</span><span>(</span><span>'Время окончания'</span><span>)</span><span>;</span><br> <span>return</span><span>;</span><br> <span>}</span><br><br> <span>const</span> dorkValue <span>=</span> sheetDorks<span>.</span><span>getRange</span><span>(</span>i<span>,</span> <span>1</span><span>)</span><span>.</span><span>getValue</span><span>(</span><span>)</span><br> <span>// ...</span><br><span>}</span>

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

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

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

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

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

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

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

JavaScript:Скопировать в буфер обмена
<span>function</span> <span>parseJSONToSheet_</span><span>(</span><span>json<span>,</span> sheet<span>,</span> dork</span><span>)</span> <span>{</span><br> <span>const</span> <span>{</span>results<span>}</span> <span>=</span> json<br> <span>for</span><span>(</span><span>let</span> i <span>=</span> json<span>.</span>first<span>;</span> i <span><=</span> json<span>.</span>last<span>;</span> i<span>++</span><span>)</span> <span>{</span><br> лист<span>.</span><span>appendRow</span><span>(</span><span>[</span><span>''</span><span>,</span> <span>''</span><span>,</span> результаты<span>[</span>i<span>]</span><span>.</span>url<span>,</span> результаты<span>[</span>i<span>]</span><span>.</span>заголовок<span>,</span> результаты<span>[</span>i<span>]</span><span>.</span>проход<span>,</span> <span>,</span>придурок<span>]</span><span>)</span><span>;</span><br> <span>}</span><br><span>}</span>

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

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

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


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

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


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



Жмем «Просмотр разрешений» и выбираем нужный аккаунт для авторизации. После жмем на слабо заметную ссылку Advanced.



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


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

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

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

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


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

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



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

В большинстве случаев достаточно выводить данные в консоль через console.log() или Logger.log(). Но бывают ситуации, когда таким образом данные не удается получить, нужна распечатка данных. Или, например, нужен быстрый доступ к списку запросов, полученных через doGet() или doPost(). В этом случае можно сделать отдельный лист «log» и добавить его в данные через AppendRow[]
JavaScript:Скопировать в буфер обмена
<span>const</span> xss <span>=</span> SpreadsheetApp<span>.</span><span>getActiveSpreadsheet</span><span>(</span><span>)</span><span>;</span><br><span>const</span> sheetLog <span>=</span> xss<span>.</span><span>getSheetByName</span><span>(</span><span>`</span><span>log</span><span>`</span></span><span>)</span><span>;</span><br>sheetLog<span>.</span><span>appendRow</span><span>(</span><span>[</span><span>новый</span> <span>Дата</span><span>(</span><span>)</span><span>,</span>'logLevel' <span>,</span> 'Данные для регистрации'<span>]</span><span>)</span><span>;</span>

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

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





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

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

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

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

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

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

Справа вверху жмем Развертывание > Новое развертывание и появляется такое окошко.

View attachment 79861


Кликаем на шестеренку слева, выбираем «Веб-приложение». Указание от чьего имени будет выполнено в приложении. В нашем случае выбираем Меня. В «Кто имеет доступ» указываем «Любой». Именно такие параметры, так как нам нужен простой прямой доступ к данным. В этом случае необходимо заморачиваться с авторизациями и правами. А так, используя Python, получайте обычные данные и оперируйте их, как хотите.



На последнем этапе Google предоставляет нам идентификатор веб-приложения и ссылку для доступа. Копируем ссылку и жмем Готово. Переходим по ссылке и появляется надпись «XSS.is». Ура, веб-приложение как-то но работает.

View attachment 79863


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

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

JavaScript:Скопировать в буфер обмена
<span>пусть</span> <span>{</span>смещение<span>,</span>количество<span>}</span> <span>=</span> e<span>.</span>параметры<span>;</span>


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

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

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

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

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


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

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

JSON:Скопировать в буфер обмена
<span>{</span><br> успех<span>:</span> <span>истина</span><span>,</span><br> количество<span>:</span> результаты.длина<span>,</span><br> результаты<br><span>}</span>

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

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

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

Скрипт готов, осталось только сделать новое развертывание: Развертывание > Управление развертываниями, в появившемся окне жмем на карандашике, в версиях выбираем Новую версию и жмем Развернуть. После этого скрипт будет полноценно работать.

Спойлер: Вся функция doGet(e)

Пример результата:
JSON:Скопировать в буфер обмена
<span>{</span><br> <span>"успех"</span><span>:</span> <span>истина</span><span>,</span><br> <span>"количество"</span><span>:</span> <span>3</span><span>,</span><br> <span>"результаты"</span><span>:</span> <span>[</span><br> <span>"wipach.si"</span><span>,</span><span>"flutacious.com"</span><span>,</span><span>"naveenautomationlabs.com"</span><br> <span>]</span><br><span>}</span>

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


Python:Скопировать в буфер обмена
<span>импорт</span> запросов<br>deployment_id <span>=</span> <span>'your_deployment_id'</span><br>смещение <span>=</span> <span>0</span><br>количество <span>=</span> <span>100</span><br>url <span>=</span> <span><span>f'https://script.google.com/macros/s/</span><span><span>{</span>deployment_id<span>}</span></span><span>/exec?offset=</span><span><span>{</span>offset<span>}</span></span><span>&count=</span><span><span>{</span>count<span>}</span></span><span>'</span></span><br>response <span>=</span> requests<span>.</span>get<span>(</span>url<span>=</span>url<span>)</span><br><span>if</span> response<span>.</span>status_code <span>==</span><span>200</span><span>:</span><br> <span>печать</span><span>(</span>ответ<span>.</span>текст<span>)</span>

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

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

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

JavaScript:Скопировать в буфер обмена
<span>function</span> <span>getClearURLData</span><span>(</span><span>url</span><span>)</span> <span>{</span><br> <span>const</span> <span>[</span>protocol<span>,</span> tail<span>]</span> <span>=</span> url<span>.</span><span>split</span><span>(</span><span>':'</span><span>)</span><span>;</span><br> <span>const</span> host <span>=</span> хвост<span>.</span><span>заменить</span><span>(</span><span>'//'</span><span>,</span><span>''</span><span>)</span><span>.</span><span>разделить</span><span>(</span><span>'/'</span><span>)</span><span>[</span><span>0</span><span>]</span><span>;</span><br> <span>вернуть</span> <span>{</span><br> протокол<span>,</span> хост<span>,</span> домен<span>:</span> протокол<span>.</span><span>конкатенация</span><span>(</span><span>'://'</span><span>,</span> host<span>)</span><br> <span>}</span><br><span>}</span><br><br><span>function</span> <span>parseJSONToSheet_</span><span>(</span><span>json<span>,</span> sheet<span>,</span> dork</span><span>)</span> <span>{</span><br> <span>const</span> <span>{</span>results<span>}</span> <span>=</span> json<span>;</span><br> <span>for</span><span>(</span><span>let</span> i <span>=</span> json<span>.</span>first<span>;</span> i <span><=</span> json<span>.</span>last<span>;</span> i<span>++</span><span>)</span> <span>{</span><br> <span>const</span> clearURLData <span>=</span> <span>getClearURLData</span><span>(</span>results<span>[</span>i<span>]</span><span>.</span>url<span>)</span><br> console<span>.</span><span>log</span><span>(</span><span>'Добавить данные: '</span><span>,</span> <span>[</span>results<span>[</span>i<span>]</span><span>.</span>url<span>,</span> результаты<span>[</span>i<span>]</span><span>.</span>название<span>,</span> результаты<span>[</span>i<span>]</span><span>.</span>проход<span>,</span> <span>,</span>dork<span>]</span><span>)</span><br> лист<span>.</span><span>appendRow</span><span>(</span><span>[</span>clearURLData<span>.</span>хост<span>,</span> clearURLData<span>.</span>домен<span>,</span> результаты<span>[</span>i<span>]</span><span>.</span>url<span>,</span> результаты<span>[</span>i<span>]</span><span>.</span>заголовок<span>,</span> результаты<span>[</span>i<span>]</span><span>.</span>проход<span>,</span> <span>,</span>придурок<span>]</span><span>)</span><span>;</span><br> <span>}</span><br><span>}</span>

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

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

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

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

View attachment 79864


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

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

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

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

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

Сначала находим таблицу, вернее сразу ее тело. Впоследствии беременне все 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 для анализа целей. Хоть это и реальный рабочий пример, но по сути только верхушка айсберга возможностей. ГАЗ позволяет творить очень много интересного. Вот некоторые мысли:

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

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

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

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