Главная » Софт » [Из песочницы] Как я боролся с воровством… с помощью php

[Из песочницы] Как я боролся с воровством… с помощью php


Когда мы платим ежедневно за услуги — это покупка услуг.
Когда мы платим ежедневно за ничего (порой даже не подозревая об этом) — это воровство.

Добрый день, читатели Хабра!

С чего всё началось

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

О том, где мы, гуляя по интернету, нажимаем на кнопочку «смотреть видео»,  грузится какая-то страница, видео почему-то не проигрывается, мы уходим и гуляем дальше, а на самом деле мы «добровольно» подключили себе услугу получать что-то, что никто никогда не видел за символическую плату 30 рублей в день со счёта своего мобильного. О котором из «воровств» я? Ещё бы, не включать же в список услуг «воровство по видеокнопке».
Вот здесь чуть подробнее. У людей это называется wap-click или мобильные подписки, а сотовые операторы придумывают разнообразные красивые названия. А здесь история о хорошем способе «заработать».

Неописанных — намного больше. Описанных случаев не совсем добровольных подписок много, этот, например.

Борцы тоже есть:

Что и зачем было автоматизировано

Поиск и блокирование объявлений в панели издателя Google AdSense.
Цель — повысить эффективность блокирования и освободить время, которое тратится на чистку вручную.

Суть проблемы и имеющиеся решения

Долгие годы (первое упоминание о подобном, что я нашёл было летом 2014-го) издатели вручную отлавливали потоки «смертей Якубовича», «каменных стояков», «смотреть видео смотреть, жми смотреть» и прочей нечисти (начало, продолжение), сей процесс почти никак не автоматизировался1 и это казалось практически невозможным.

1 Есть (по крайней мере когда-то было) два решения, но у них довольно серьёзные требования, которые не каждый может себе позволить.
Эти самые решения:

  1. AdSense Cleaner. Требуется много доп. ПО.
  2. AdsAutomation. Сценарий для управления браузером Google Chrome (как я понял, на ZennoPoster). Необходим отдельный ПК. И в данный момент с GitHub проект удалён.

Если делать ПО, которое заменит человека блокирующего объявления, то оно должен быть сделано с учётом ряда требований:

  • должно работать на том «железе» и ПО, которое есть практически у всех сайтовладельцев;
  • не требовать дополнительного ПО и изменения настроек имеющегося;
  • простота в установке и настройке, чтобы обычный пользователь смог поставить.

.

Закинуть можно прямо на свой сайт и работать без дополнительных компов и прочих сложностей. В общем, на php (с cURL) будет что надо.

И одно уточнение к требованиям.

В Cookie-файлах будет хранится только ключ для доступа к панели управления. Так как решение подразумевалось автоматизированным на php, следовательно, запуск через cron, то хранение пользовательских настроек и временных данных должно быть на диске (не в cookie). Для избранных, кто не имеет возможности настроить cron, но может на ПК/планшете/смартфоне держать одну вкладку открытой будет добавлена возможность периодического запуска по таймеру на Javascript.

Что предвещало начало или Google API

И для AdSense есть API, как-то краем глаза видел и не углублялся. А сейчас — самое время вникнуть. Возможностей много, но оказалось, что ни здесь, ну тут ничего не описано про API для ЦПО. Хочешь смотреть объявления, которые на сайте крутятся, пожалуйста — вручную.

Начало

Интерфейс Google AdSense построен на Javascript, там с виду всё красиво и довольно сложно с точки зрения устройства.

Запросов там уйма, самые интересные для меня были в разделе «XHR and Fetch», там-то я и нашёл, то что выглядело вполне разгадываемым, если хорошо подумать. Первым делом заглянул в инструменты разработчика Google Chrome на вкладку «Network», чтобы «подслушать» как этот навороченный интерфейс общается с сервером. Например, один из post-запросов:

Передаваемая строка.

}}}","xsrf":"ABOvogKvrE9fIqAKh0w02RIsB4OJ4hsB_g:1535467885347"}

В запросе сразу виден идентификатор издателя, под вторым пунктом набор параметров, суть которых можно выяснить экспериментальным путём и жетон XSRF.

А в ответ получает подробную информацию об объявлении, но не всю и без самого объявления (здесь и далее картинки, вытянутые в base64, подрезал).

Простыня на несколько страниц.

{"result":{"1":[{"1":0,"3":0,"4":{"1":"AClZvXJ2t4wiEZ/VZ0i54m0Qtqpi2DTqkI1kaPMTRi4LnsQn0iR5K1xBlFpS1xmJV7ko4a6qx5RcTkp7CzVjwoy5UDSWZ5jOCPLGRcoQdDt+wOk46bdr0yA\u003d"},"5":{"1":82,"2":0,"3":0,"4":"\u003cdiv id\u003d\"ad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60\"\u003e\u003c/div\u003e","5":"\u003cdiv id\u003d\"ad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60\"\u003e\u003c/div\u003e","6":"\u003cdiv\u003e\u041c\u043d\u043e\u0433\u043e\u0444\u043e\u0440\u043c\u0430\u0442\u043d\u044b\u0435\u003cspan id\u003d'multi-format-tooltip'\u003e\u003c/span\u003e\u003c/div\u003e\u003ca class\u003d'arc-url-link-ellipsis' target\u003d'_blank' href\u003d'https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/' title\u003d'https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/'\u003ehttps://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/\u003c/a\u003e","7":"\u003cdiv class\u003d'arc-one-by-one-legend'\u003e\u0422\u0438\u043f \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u044f\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-data'\u003e\u041c\u043d\u043e\u0433\u043e\u0444\u043e\u0440\u043c\u0430\u0442\u043d\u044b\u0435\u003cspan id\u003d'multi-format-tooltip'\u003e\u003c/span\u003e\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-legend'\u003e\u0426\u0435\u043b\u0435\u0432\u043e\u0439 URL\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-data'\u003e\u003ca class\u003d'arc-url-link-ellipsis' target\u003d'_blank' href\u003d'https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/' title\u003d'https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/'\u003ehttps://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/\u003c/a\u003e\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-legend'\u003e\u0414\u043e\u043c\u0435\u043d\u044b \u0438\u0437\u0434\u0430\u0442\u0435\u043b\u0435\u0439\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-data'\u003e4aynikam.ru\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-data'\u003eandroidphone.su\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-data'\u003eandroidphones.ru\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-data'\u003efull-repair.com\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-data'\u003ehowgadget.com\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-legend'\u003e\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 \u0440\u0435\u043a\u043b\u0430\u043c\u043e\u0434\u0430\u0442\u0435\u043b\u044c\u003cspan id\u003d'adx-advertiser-tooltip'\u003e\u003c/span\u003e\u003c/div\u003e\u003cdiv class\u003d'arc-one-by-one-data'\u003eDNS Shop\u003c/div\u003e","8":"\u003cdiv\u003e\u003cspan class\u003d'arc-impression-score high'\u003e\u0412\u042b\u0421\u041e\u041a\u041e\u0415\u003c/span\u003e \u0447\u0438\u0441\u043b\u043e \u043f\u043e\u043a\u0430\u0437\u043e\u0432\u003c/div\u003e","9":{"1":"\u003ca href\u003d\"https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/\" target\u003d\"_blank\"\u003e\u003cimg onerror\u003d\"this.src\u003d'data:image/gif;base64,RA7'\" src\u003d\"https://www.google.com/webpagethumbnail?c\u003d58\u0026s\u003d400:400\u0026r\u003d4\u0026d\u003dhttps://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/\u0026a\u003dAIYkKU9ZGGjFTOWtm771MQwgDYxqtlBLCw\" border\u003d0 alt\u003d\"\"\u003e\u003c/a\u003e","2":"\u003ca href\u003d\"https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/\" target\u003d\"_blank\"\u003e\u003cimg onerror\u003d\"this.src\u003d'data:image/gif;base64,R0AA7'\" src\u003d\"https://www.google.com/webpagethumbnail?c\u003d58\u0026s\u003d400:400\u0026r\u003d3\u0026d\u003dhttps://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/\u0026a\u003dAIYkKU_CQ2K6v5f11Nk1RXtc87FtmG2B1w\" border\u003d0 alt\u003d\"\"\u003e\u003c/a\u003e","3":"\u003ca href\u003d\"https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/\" target\u003d\"_blank\"\u003e\u003cimg onerror\u003d\"this.src\u003d'data:image/gif;base64,R0lAA7'\" src\u003d\"https://www.google.com/webpagethumbnail?c\u003d58\u0026s\u003d400:400\u0026r\u003d6\u0026d\u003dhttps://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/\u0026a\u003dAIYkKU_My0a48LAsW-ZKpQX-ATXkMoPEVg\" border\u003d0 alt\u003d\"\"\u003e\u003c/a\u003e"},"10":"https://adwords-displayads.googleusercontent.com/da/b/preview.js?client\u003dasfe-arc-external-preview\u0026obfuscatedCustomerId\u003d5240877441\u0026creativeId\u003d288930210411\u0026htmlParentId\u003dad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60\u0026sig\u003dACiVB_yMUjLwDjRO2T-0VAaVuRPt8uLHGQ","13":"https://adwords-displayads.googleusercontent.com/da/b/preview.js?client\u003dasfe-arc-external-preview\u0026obfuscatedCustomerId\u003d5240877441\u0026creativeId\u003d288930210411\u0026htmlParentId\u003dad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60\u0026showVariations\u003dtrue\u0026sig\u003dACiVB_yMUjLwDjRO2T-0VAaVuRPt8uLHGQ","14":"https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/","15":"","17":"","18":"DNS Shop","20":"adv-5594449542310820","21":["site1.ru","site2.com","site3.com","site4.ru"]},"6":{"5":"-6668648012302470727","7":["DNS"],"9":0},"7":1,"9":{"3":[{"1":{"1":"AClZvXLE9HJbFYq9TrAsXFgV4YkXsQt9lXp1xWjSB5aT5bFBpe4VNgo\u003d"},"2":"\u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0438 \u0442\u0435\u043b\u0435\u043a\u043e\u043c\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u0438","3":"\u0422\u043e\u0432\u0430\u0440\u044b \u0438 \u0443\u0441\u043b\u0443\u0433\u0438, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0442\u0435\u043b\u0435\u043a\u043e\u043c\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f\u043c\u0438, \u0432 \u0442\u043e\u043c \u0447\u0438\u0441\u043b\u0435 \u043a\u0430\u0431\u0435\u043b\u044c\u043d\u043e\u0435 \u0438 \u0441\u043f\u0443\u0442\u043d\u0438\u043a\u043e\u0432\u043e\u0435 \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0432 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442."},{"1":{"1":"AClZvXKrUJJ3kKBen2scP56BynOtGhf160i1F1LLmtBj3b/oh2dUFg8\u003d"},"2":"\u041c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0435 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u044b","3":"\u041c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0435 \u0438 \u0441\u043e\u0442\u043e\u0432\u044b\u0435 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u044b, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0441\u043e\u043f\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0438 \u0438 \u0441\u0440\u0430\u0432\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0430\u043d\u0430\u043b\u0438\u0437 \u0442\u043e\u0432\u0430\u0440\u043e\u0432. \u0412 \u044d\u0442\u0443 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044e \u043d\u0435 \u0432\u0445\u043e\u0434\u044f\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u044b \u0434\u043b\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0445 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u043e\u0432."},{"1":{"1":"AClZvXL4W+khZ4O9SJiu97cTbTs2+0Wecf1IVNju8ffd4ysIT9PJ7XY\u003d"},"2":"\u041c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0435 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u044b \u0438 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u044b \u0434\u043b\u044f \u043d\u0438\u0445","3":"\u041c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0435 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u044b, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0441\u043e\u043f\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u044b \u0438 \u0430\u043f\u043f\u0430\u0440\u0430\u0442\u043d\u043e\u0435 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u0435, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0447\u0435\u0445\u043b\u044b, \u043c\u043e\u043d\u043e\u043f\u043e\u0434\u044b \u0434\u043b\u044f \u0441\u0435\u043b\u0444\u0438, \u0437\u0430\u0449\u0438\u0442\u043d\u044b\u0435 \u044d\u043a\u0440\u0430\u043d\u044b \u0438 \u0437\u0430\u0440\u044f\u0434\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430."},{"1":{"1":"AClZvXLQ3gPoVwjQbokDpB3+nni4xURwH5+YlnwkqjYtUowjhiKvk8Q\u003d"},"2":"\u041f\u041a \u0438 \u0431\u044b\u0442\u043e\u0432\u0430\u044f \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u0438\u043a\u0430","3":"\u0422\u043e\u0432\u0430\u0440\u044b, \u0443\u0441\u043b\u0443\u0433\u0438 \u0438 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0430\u043c\u0438 \u0438 \u0431\u044b\u0442\u043e\u0432\u043e\u0439 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u0438\u043a\u043e\u0439."},{"1":{"1":"AClZvXLKYOGgOROaa32IUxU15jP89AtTM4dV24WKS+daMhqJMTNmeSY\u003d"},"2":"\u0422\u0435\u043b\u0435\u0444\u043e\u043d\u0438\u044f","3":"\u0422\u043e\u0432\u0430\u0440\u044b, \u0443\u0441\u043b\u0443\u0433\u0438, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u0438 \u0434\u0440\u0443\u0433\u0438\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u044b, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0438\u0435\u0439 \u0438 \u0433\u043e\u043b\u043e\u0441\u043e\u0432\u043e\u0439 \u0441\u0432\u044f\u0437\u044c\u044e."}]},"10":{"1":"AClZvXLdGOShgJo+BM3apOUAFzQkE41z1/hiZhIY8eUlC7p7xXPm82P3dq7yXhbEI+tN/YHgdH4P"}}],"2":0.0,"3":"60609","4":1,"5":"","6":"ClD3Z2nP2P/////1/ff99fXV98nMyMrJz8rH9fHV883Hx8bMz83Oz8vOzv8A/v/+9f33/fX11ffJzMjKyc/Kx/Xx1fPNx8fGzM/Nzs/Lzs7//hABIWxUk293Pm+qOQAAAAAnMJaYSAFQAFoLCS8wxxaTatL1EAJgp7737gY\u003d","7":"3639","9":0},"xsrf":"ABOvogKaRsVZECZZJU-gDWrOqoP0CSqf7Q:1535467886413"}

После json_decode это выглядит примерно так:

Объект из json-строки (осторожно, 175 строк).

object(stdClass)#19 (2) { ["result"]=> object(stdClass)#18 (8) { ["1"]=> array(1) { [0]=> object(stdClass)#1 (8) { ["1"]=> int(0) ["3"]=> int(0) ["4"]=> object(stdClass)#2 (1) { ["1"]=> string(120) "AClZvXJ2t4wiEZ/VZ0i54m0Qtqpi2DTqkI1kaPMTRi4LnsQn0iR5K1xBlFpS1xmJV7ko4a6qx5RcTkp7CzVjwoy5UDSWZ5jOCPLGRcoQdDt+wOk46bdr0yA=" } ["5"]=> object(stdClass)#3 (17) { ["1"]=> int(82) ["2"]=> int(0) ["3"]=> int(0) ["4"]=> string(102) "<div id="ad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60"></div>" ["5"]=> string(102) "<div id="ad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60"></div>" ["6"]=> string(355) "<div>Многоформатные<span id='multi-format-tooltip'></span></div><a class='arc-url-link-ellipsis' target='_blank' href='https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/' title='https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/'>https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/</a>" ["7"]=> string(1066) "<div class='arc-one-by-one-legend'>Тип объявления</div><div class='arc-one-by-one-data'>Многоформатные<span id='multi-format-tooltip'></span></div><div class='arc-one-by-one-legend'>Целевой URL</div><div class='arc-one-by-one-data'><a class='arc-url-link-ellipsis' target='_blank' href='https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/' title='https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/'>https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/</a></div><div class='arc-one-by-one-legend'>Домены издателей</div><div class='arc-one-by-one-data'>4aynikam.ru</div><div class='arc-one-by-one-data'>androidphone.su</div><div class='arc-one-by-one-data'>androidphones.ru</div><div class='arc-one-by-one-data'>full-repair.com</div><div class='arc-one-by-one-data'>howgadget.com</div><div class='arc-one-by-one-legend'>Обнаруженный рекламодатель<span id='adx-advertiser-tooltip'></span></div><div class='arc-one-by-one-data'>DNS Shop</div>" ["8"]=> string(98) "<div><span class='arc-impression-score high'>ВЫСОКОЕ</span> число показов</div>" ["9"]=> object(stdClass)#4 (3) { ["1"]=> string(4191) "<a href="https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/" target="_blank"><img onerror="this.src='data:image/gif;base64,RCw" border=0 alt=""></a>" ["2"]=> string(4191) "<a href="https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/" target="_blank"><img onerror="this.src='data:image/gif;base64,R1w" border=0 alt=""></a>" ["3"]=> string(4191) "<a href="https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/" target="_blank"><img onerror="this.src='data:image/gif;base64,Rg" border=0 alt=""></a>" } ["10"]=> string(291) "https://adwords-displayads.googleusercontent.com/da/b/preview.js?client=asfe-arc-external-preview&obfuscatedCustomerId=5240877441&creativeId=288930210411&htmlParentId=ad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60&sig=ACiVB_yMUjLwDjRO2T-0VAaVuRPt8uLHGQ" ["13"]=> string(311) "https://adwords-displayads.googleusercontent.com/da/b/preview.js?client=asfe-arc-external-preview&obfuscatedCustomerId=5240877441&creativeId=288930210411&htmlParentId=ad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60&showVariations=true&sig=ACiVB_yMUjLwDjRO2T-0VAaVuRPt8uLHGQ" ["14"]=> string(69) "https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/" ["15"]=> string(0) "" ["17"]=> string(0) "" ["18"]=> string(8) "DNS Shop" ["20"]=> string(20) "adv-5594449542310820" ["21"]=> array(4) { [0]=> string(8) "site1.ru" [1]=> string(9) "site2.com" [2]=> string(9) "site3.com" [3]=> string(8) "site4.ru" } } ["6"]=> object(stdClass)#5 (3) { ["5"]=> string(20) "-6668648012302470727" ["7"]=> array(1) { [0]=> string(3) "DNS" } ["9"]=> int(0) } ["7"]=> int(1) ["9"]=> object(stdClass)#16 (1) { ["3"]=> array(5) { [0]=> object(stdClass)#7 (3) { ["1"]=> object(stdClass)#6 (1) { ["1"]=> string(56) "AClZvXLE9HJbFYq9TrAsXFgV4YkXsQt9lXp1xWjSB5aT5bFBpe4VNgo=" } ["2"]=> string(52) "Интернет и телекоммуникации" ["3"]=> string(217) "Товары и услуги, связанные с телекоммуникациями, в том числе кабельное и спутниковое обслуживание и доступ в Интернет." } [1]=> object(stdClass)#9 (3) { ["1"]=> object(stdClass)#8 (1) { ["1"]=> string(56) "AClZvXKrUJJ3kKBen2scP56BynOtGhf160i1F1LLmtBj3b/oh2dUFg8=" } ["2"]=> string(35) "Мобильные телефоны" ["3"]=> string(359) "Мобильные и сотовые телефоны, а также сопутствующая информация, например технические характеристики и сравнительный анализ товаров. В эту категорию не входят аксессуары для мобильных телефонов." } [2]=> object(stdClass)#11 (3) { ["1"]=> object(stdClass)#10 (1) { ["1"]=> string(56) "AClZvXL4W+khZ4O9SJiu97cTbTs2+0Wecf1IVNju8ffd4ysIT9PJ7XY=" } ["2"]=> string(73) "Мобильные телефоны и аксессуары для них" ["3"]=> string(283) "Мобильные телефоны, а также сопутствующие аксессуары и аппаратное обеспечение, например чехлы, моноподы для селфи, защитные экраны и зарядные устройства." } [3]=> object(stdClass)#13 (3) { ["1"]=> object(stdClass)#12 (1) { ["1"]=> string(56) "AClZvXLQ3gPoVwjQbokDpB3+nni4xURwH5+YlnwkqjYtUowjhiKvk8Q=" } ["2"]=> string(45) "ПК и бытовая электроника" ["3"]=> string(142) "Товары, услуги и информация, связанные с компьютерами и бытовой электроникой." } [4]=> object(stdClass)#15 (3) { ["1"]=> object(stdClass)#14 (1) { ["1"]=> string(56) "AClZvXLKYOGgOROaa32IUxU15jP89AtTM4dV24WKS+daMhqJMTNmeSY=" } ["2"]=> string(18) "Телефония" ["3"]=> string(181) "Товары, услуги, а также информационные и другие ресурсы, связанные с телефонией и голосовой связью." } } } ["10"]=> object(stdClass)#17 (1) { ["1"]=> string(76) "AClZvXLdGOShgJo+BM3apOUAFzQkE41z1/hiZhIY8eUlC7p7xXPm82P3dq7yXhbEI+tN/YHgdH4P" } } } ["2"]=> float(0) ["3"]=> string(5) "60609" ["4"]=> int(1) ["5"]=> string(0) "" ["6"]=> string(168) "ClD3Z2nP2P/////1/ff99fXV98nMyMrJz8rH9fHV883Hx8bMz83Oz8vOzv8A/v/+9f33/fX11ffJzMjKyc/Kx/Xx1fPNx8fGzM/Nzs/Lzs7//hABIWxUk293Pm+qOQAAAAAnMJaYSAFQAFoLCS8wxxaTatL1EAJgp7737gY=" ["7"]=> string(4) "3639" ["9"]=> int(0) } ["xsrf"]=> string(48) "ABOvogKaRsVZECZZJU-gDWrOqoP0CSqf7Q:1535467886413"
}

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

  • getWebPropertyMetricsToken
  • getAdDisplayLanguages
  • getArcSettings
  • getAdNetworkApprovals
  • getPubControlsCapabilities

Теоретически возможно. В бой?

Ладно, разгадать их общение возможно (теоретически), но это всё будет бесполезно, да теорией так и останется, если не сделать авторизацию в Google.

Авторизация. Или как войти в Google на php+cURL

Опять инструменты разработчика, выходим из учётной записи и смотрим на обмен данными. Детально не помню, ибо ничего там понять не смог. Огромное количество JS, похоже, что какие-то вычисления прямо на клиенте делаются, результаты на сервер отправляются. В общем, нечеловеку войти практически невозможно.

Куча JS. Думаем дальше. Не обделит же Google пользователей без JS возможностью авторизоваться? А если JS отключить? Внешне окошко авторизации уже выглядит куда проще. Что ж, попробуем без JS. Самое главное, что и в плане HTML тоже намного проще! Как и ранее, сначала вводим логин, а пароль на следующей странице. Но скрытые поля — не проблема, ведь что на входе получили, то следующему скрипту и передали. Обычный тэг «form» с обычными полями «input», правда не без кучи защитных или системных скрытых полей. А двухэтапная авторизация? И таким образом получилось войти в Google. Сначала надо убедиться, что получится вытаскивать объявления для их досмотра, иначе это всё смысла не имеет. Об этом позже.

Возможно ли теоретическое на практике?

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

Что нужно было сделать, чтобы понять, что продолжение имеет смысл?

  1. Войти в ЦПО.
  2. Получить список объявлений.
  3. Получить конкретное объявление (для начала текстовое).

Вход в ЦПО — самое простое, грубо говоря, просто переходим по ссылке. Получилось.

Подробности

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

Назову их условно старый и новый. В AdSense на момент написания статьи есть два ЦПО.

Для старого ЦПО.

Post-запрос «без нагрузки»:

https://www.google.com/adsense/gwt-properties?pid=pub-8958890276790964&authuser=0&tpid=pub-8958890276790964&ov=3&hl=en

Ответ:

<meta name="gwt:property" content="usePropertyService=true">
<meta name="gwt:property" content="applicationType=ASFE3">
<meta name="gwt:property" content="syn.token=ABOvogJ1yQyL9pgHcGYM-J3OLj_9VSh31w:1535115071772">
<meta name="gwt:property" content="syn.token.pb=ABOvogKJ6-xmsNWK4Mbe_H5bT1xXhyj8SQ:1535115071772">
<meta name="gwt:property" content="syn.login=XXXXXX@gmail.com">
<meta name="gwt:property" content="syn.csi.backendUrl=">
<meta name="gwt:property" content="syn.helpCenterUrl=//support.google.com/adsense/">
<meta name="gwt:property" content="syn.helpHost=//support.google.com">
<meta name="gwt:property" content="syn.helpCenterUri=/adsense">
<meta name="gwt:property" content="syn.newHelpHost=https://clients6.google.com">
<meta name="gwt:property" content="syn.newHelpCenterUri=/adsense">
<meta name="gwt:property" content="syn.helpCenterGaiaAuthDisabled=false">
<meta name="gwt:property" content="syn.billing3BaseUri=https://bpui0.google.com">
<meta name="gwt:property" content="syn.contextPath=/adsense">
<meta name="gwt:property" content="syn.userLanguage=en-US">
<meta name="gwt:property" content="syn.bruschettaContextPath=/adsense/new">
<meta name="gwt:property" content="userProfileImageUrl=https://lh5.googleusercontent.com/-v7nuoAI4eEQ/AAAAAAAAAAI/AAAAAAAAAAA/AT3-yjmKyg8/s96/photo.jpg">
<meta name="gwt:property" content="userDisplayName="Имя Фамилия">
<meta name="gwt:property" content="userSettingsUrl=https://www.google.com/settings">
<meta name="gwt:property" content="googlePlusProfileUrl=https://plus.google.com/me">
<meta name="gwt:property" content="googlePrivacyUrl=http://www.google.com/intl/en_US/policies/privacy/">
<meta name="gwt:property" content="syn.features=562,465,612,604,616,618">
<meta name="gwt:property" content="analyticsHomePageUrl=https://www.google.com/analytics/web/">
<meta name="gwt:property" content="disableDebugIds=true">
<meta name="gwt:property" content="syn.pubControlsCapabilitiesLoadTimeout=5000">
<meta name="gwt:property" content="pid=pub-8958890276790964">
<meta name="gwt:property" content="tpid=pub-8958890276790964">
<meta name="gwt:property" content="syn.asfeGtmCampaignId=GTM-K7ТМWZ">

Нам нужна четвёртая строчка, а именно «syn.token.pb». Сохраняем это значение для дальнейшего формирования запросов.

Для нового ЦПО.

Post-запрос «без нагрузки»:

https://www.google.com/ads-publisher-controls/acx/5/darc/loader?onearcClient=adsense&pc=ca-pub-8958890276790964&tpid=pub-8958890276790964&hl=en

Ответ:

(function() {function loadAsyncOrDefer() {var scriptElement = document.createElement('script'); scriptElement.src = 'https:\/\/ssl.gstatic.com\/ads-publisher-controls\/onearc_20180822-12_RC00\/darc\/arc_app.dart.js';scriptElement.type = 'application\/javascript';scriptElement.defer = true;scriptElement.nonce = window['acxCspNonce'];scriptElement = document.head.appendChild(scriptElement); if ('_resourceTimingBuffer' in window) {_resourceTimingBuffer.add(scriptElement.src);}};loadAsyncOrDefer();})();window['__darc_app_params'] = {'onearcClient': 'ADSENSE','hl': 'ru','pc': 'ca-pub-8958890276790964','tpid': 'pub-8958890276790964',};window['__app_metadata'] = {'token': 'ABiMD8TT9vzK99SFB7iaI0ssBySxT9jjrQ:1535116725529','pre': '\/ads-publisher-controls\/acx','scs': 'https:\/\/ssl.gstatic.com\/ads-publisher-controls\/onearc_20180822-12_RC00','oacf': '\x7b\x221\x22:\x5b5,25,22,8,27,32,43,44,45,48,49,5,25,22,8,27,32,43,44,45,48,49,29,46\x5d\x7d','hats': 'ibhswcm2x2iztju5i6jbbzlkma',};

Нужная нам последовательность здесь:

'token': 'ABiMD8TT9vzK99SFB7iaI0ssBySxT9jjrQ:1535116725529'

Плюс цифровой жетон XSRF на каждый запрос. Получение списка — задача интересная, ведь нужно передать кучу настроек — сообщить что мы хотим получить (тип объявлений, проверенные/новые/заблокированные, количество объявлений и прочее). В ответ пришёл большой объём данных, который содержал даже миниатюры изображений тех сайтов куда вели объявления. Получилось. Ну и, конечно, ссылки на объявления.

Подробности

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

Далее запросами буду называть названия методов (только для нового ЦПО, для старого метод указан в теле запроса) и json-строку, так как они являются «носителями» информации, всё остальное (адрес, заголовки, цифровые жетоны, прочие параметры) — «обёртка», они не принципиальны, про это всё расскажу позже.

Для старого ЦПО (переменная «params» json-запроса):

{ "1":"ca-pub-8958890276790964", "2":{ "1":0, // Стартовая позиция, то есть начиная с какого по счёту показывать объявления "2":32, // Кол-во запрашиваемых объявлений "3":0, // 0 - незаблокированные, 1- заблокированные "4":{ "1":{"1":"какая-то муть из предыдущего ответа"} //Жетон из предыдущего ответа }, "5":{ "1":"video" // поисковое слово "2":1, // Если есть, то смотрим только непроверенные "3":1, // Появляется если поставить только прогнозируемую блокировку "6":7, // Объявления за указанное количество дней "16":[0], // 0 - текстовые; 1 - графические; 2 - медийные. При включении всех этот параметр отсутствует. "17":0 // При включении медийных этот параметр пропадает } }, "3":"-3945261286198141534" //Похоже, параметр не обязательный
}

Расшифровка есть, формируем запрос и получаем ответ.

Для старого ЦПО предварительно нужно получить жетон — сделать ещё один запрос перед самим запросом объявлений:

{"method":"getWebPropertyMetricsToken","params":"{\"1\":\"ca-pub-8958890276790964\"}","xsrf":"ABOvogKJ6-xmsNWK4Mbe_H5bT1xXhyj8SQ:1535115071772"}

Ответ:

{"result":{"1":{"1":{"1":"AClZvXKte+4mEwsFB7kw20LrbWQ6jOMxmK8j4At4Vxqc7w+5dDDYWIx2k1ldCvvGbAT59UClLSkQty6zyZZQSmgxKvpKhq22bKRfGy8ywt0B5L8WE53vo+YtI8ixM8Xe0RPixTjPtOLQA8sCZod+hvHxqU5Depi3I9XUV6JMn8uCOg67m+5oe5TT1L0OytnUBDIsjAaQ+kcldN23yGoppKKCs2Zf5XI6i7nk5QHehS8wvsDlugvkKSU3fUo3J+ZHJvoUXyCGLP3lP9Gh+6fOMir/SLrOJx8udRbtjTJhLsvXTXUN2QbjcEfFFAIaWfgMr5euHtYwYYWuMoI5ofZTc9L8sCY5pA0Q/CWyZ6QLH85XI70vxH6cBZtsnfrPLRh18cxSxFgzXuAwPHW8+CueCznqiHcY7gOhxQc2YWmSgwMIP9Cpgr089dWoB58wulcK0g+EqnTJiQdI9MMUj4zzLpu5DYja5ftP7lF3jeCSuKT9q70B9OqMDvlGlruZd2hhHe3k5S+LoyWo/4WZDUTvWpCMmnPzCP3R4OIQnrhS0s5ffOVxjyNHrXJXtrNhppap3BY4iByIn1cowMfVFfx3hNep0JW59db9fVuXKaSy/mqHZKC1ToRM/UyCoSZ9ZjY/Ot091ptURLRYoTFal5TBbMKISgxn5UCz4vSoxVe1fC64dwXHatSzCCg9AjJOpKR4p/9smxOaKg73pmMHsEY98I6TJhvaeJ9o6lcHsG8PZnB6xNS4ZJHBtN1baHkrCHOfqaepMVyRCF2kPNhr9SgujjTTbiKGMUO3UVamOQQ5/EckTgFMr0PIda7PPw7op8qFEhxZmkoo9KgERcYLGHxzGePjfo0IiNbf7k50lgDipwk5ag3CI0tw3CtDicQn6isHwKOmlfSctrEGv/Fjlmcgjhl1sTAL/rTWxDCABKN7/OhdysBAOq0j6viFgzjM8WI0ZuYPIVIm19CQ+YGcOx77oiyxev+3sAj7uSJoYFslmgiZV4jrF5P+b+U/5fknRf2Ho8plAUh4AHweXMeaPFYZAYooe6jC79EzgizqXvx1H/HrKKQcaXdDZ1ivoOM/7DtzJbawzO7ALUnHkqR1ZYmw3+3E/pmsDXedYgzERWYWvJltS+P46iWYOS43SUVw+whDWZnjJOwVOFFLDWcg4ykfzNmbq4B/vUibrV1dCiRpTIXSP92xk1I8MCfQGiptqo5MiKttqJ9Orj7nrGXEDz5pJBTTem919nz5rNIjI/sus3GZ+G4rBE+9i1sJN0jxszvpRD2AKsl1KSOrPCuOBhpNbD2HnFgQd+EUw8CpH2MLZlrZ8l3cqzDVc5aeCQ1eiUKlONlZpIxZi5wE5HyKZRxC8ljtX5xe+Fpg8R8/yDarvAkjeb0yKzN/e893nEVz3CmF68pphNp71kjJtvwBS2JtSWhFc81Ys51GEw\u003d\u003d"}}},"xsrf":"ABOvogJLbcTkcBxU_TCJddIrW4L-mVwPcw:1535115072920"}

Этот огромный жетон («1»:«AClZ...») нам понадобится для запроса объявлений.

Запрос объявлений:

{"method":"searchArcApprovals","params":"{"1":"ca-pub-8958890276790964","2":{"1":0,"2":24,"3":0,"4":{"1":{"1":"AClZvXKte+4mEwsFB7kw20LrbWQ6jOMxmK8j4At4Vxqc7w+5dDDYWIx2k1ldCvvGbAT59UClLSkQty6zyZZQSmgxKvpKhq22bKRfGy8ywt0B5L8WE53vo+YtI8ixM8Xe0RPixTjPtOLQA8sCZod+hvHxqU5Depi3I9XUV6JMn8uCOg67m+5oe5TT1L0OytnUBDIsjAaQ+kcldN23yGoppKKCs2Zf5XI6i7nk5QHehS8wvsDlugvkKSU3fUo3J+ZHJvoUXyCGLP3lP9Gh+6fOMir/SLrOJx8udRbtjTJhLsvXTXUN2QbjcEfFFAIaWfgMr5euHtYwYYWuMoI5ofZTc9L8sCY5pA0Q/CWyZ6QLH85XI70vxH6cBZtsnfrPLRh18cxSxFgzXuAwPHW8+CueCznqiHcY7gOhxQc2YWmSgwMIP9Cpgr089dWoB58wulcK0g+EqnTJiQdI9MMUj4zzLpu5DYja5ftP7lF3jeCSuKT9q70B9OqMDvlGlruZd2hhHe3k5S+LoyWo/4WZDUTvWpCMmnPzCP3R4OIQnrhS0s5ffOVxjyNHrXJXtrNhppap3BY4iByIn1cowMfVFfx3hNep0JW59db9fVuXKaSy/mqHZKC1ToRM/UyCoSZ9ZjY/Ot091ptURLRYoTFal5TBbMKISgxn5UCz4vSoxVe1fC64dwXHatSzCCg9AjJOpKR4p/9smxOaKg73pmMHsEY98I6TJhvaeJ9o6lcHsG8PZnB6xNS4ZJHBtN1baHkrCHOfqaepMVyRCF2kPNhr9SgujjTTbiKGMUO3UVamOQQ5/EckTgFMr0PIda7PPw7op8qFEhxZmkoo9KgERcYLGHxzGePjfo0IiNbf7k50lgDipwk5ag3CI0tw3CtDicQn6isHwKOmlfSctrEGv/Fjlmcgjhl1sTAL/rTWxDCABKN7/OhdysBAOq0j6viFgzjM8WI0ZuYPIVIm19CQ+YGcOx77oiyxev+3sAj7uSJoYFslmgiZV4jrF5P+b+U/5fknRf2Ho8plAUh4AHweXMeaPFYZAYooe6jC79EzgizqXvx1H/HrKKQcaXdDZ1ivoOM/7DtzJbawzO7ALUnHkqR1ZYmw3+3E/pmsDXedYgzERWYWvJltS+P46iWYOS43SUVw+whDWZnjJOwVOFFLDWcg4ykfzNmbq4B/vUibrV1dCiRpTIXSP92xk1I8MCfQGiptqo5MiKttqJ9Orj7nrGXEDz5pJBTTem919nz5rNIjI/sus3GZ+G4rBE+9i1sJN0jxszvpRD2AKsl1KSOrPCuOBhpNbD2HnFgQd+EUw8CpH2MLZlrZ8l3cqzDVc5aeCQ1eiUKlONlZpIxZi5wE5HyKZRxC8ljtX5xe+Fpg8R8/yDarvAkjeb0yKzN/e893nEVz3CmF68pphNp71kjJtvwBS2JtSWhFc81Ys51GEw\u003d\u003d"}}},"3":""}","xsrf":"ABOvogI3FCm29t4pdIded8L-Q98R0Voy-Q:1535121289188"}

Перевожу раздел 2 переменной «params»:
Дорогой Google, покажи нам, пожалуйста:
начиная с нулевого объявления ("1":0),
24 объявления ("2":24),
незаблокированных ("3":0),
свежий одноразовый пароль: AClZvX....

Ряд параметров можно не указывать, они принимают значения по-умолчанию:

  • тип объявлений: все;
  • период: все доступные;
  • прогнозируемая блокировка: нет;
  • показывать только непроверенные: нет.

В ответ приходят десятки или сотни килобайт в зависимости от количества запрошенных объявлений. Самое тяжёлое — это графика, «вытянутая» в тексте (data:image/gif;base64....). А если непроверенных нет, то ответ прост:

{"result":{"4":1,"5":"","8":"0","9":0},"xsrf":"ABOvogLWqmyC7KH1zfvmPxk-Y69-Jzj5XQ:1535115074392"}

Если б объявления были они бы содержались здесь: result->{5}.

Для нового ЦПО:

{ "1":"ca-pub-8958890276790964", "2":{ "1":10, // Стартовая позиция, то есть начиная с какого по счёту показывать объявления "2":7, // Кол-во запрашиваемых объявлений "3":11, // проверенные - 10; заблокированные - 1; Непроверенные - 11; "5":{ "6":3 // Объявления за указанное количество дней "7":3534 // номер рекламной сети "14":"en" // язык "16":[0] // 0 - текстовые; 1 - графические; 2 - медийные. "18":"dfd.com" // домен издателя "24":"video" // поисковое слово }, "7":""}, // Жетон из предыдущего ответа для перехода к следующей странице "3":"-2876348936240321457", // Жетон из предыдущего ответа для перехода к следующей странице "5":true // Просто истина и всё. Всегда.
}

Предварительных запросов делать уже не нужно, можно сразу запрашивать объявления.
SearchApprovals (это метод)

{"1":"ca-pub-8958890276790964","2":{"2":100,"3":11,"5":{"16":[0]},"7":""},"5":true}

Google, выдай, пожалуйста:
100 объявлений ("2":100),
непроверенных ("3":11),
текстовых ("5":{"16":[0]},
идентификатора сессии поиска у меня пока нет ("7":"")

Необязательные параметры и умолчания:

  • порядковый номер первого запрашиваемого объявления: 0;
  • период: все доступные;

В ответ мы получаем практически то же самое, что в случае старого ЦПО. Отличается только одним словом — названием контейнера с данными. В старом это «result», в новом — «default».

Здесь уже никакой защиты, доступ свободный для всех. Получение конкретного объявления — всё просто, берём из предыдущего ответа ссылку и скачиваем объявление.

Подробности

Ссылка на объявление. Будем её искать в предыдущем ответе, где мы получили много-много килобайт текста в ответ на запрос объявлений.

Чтоб не было слишком много непонятного кода привожу ответ на запрос одного объявления (да и то нещадно порезанный, он был раз в 10 больше, оставлено только самое главное на данный момент):

{"result":{"1":[{"1":0,"3":0,"4":{"1":"AClZvXJ2t4wiEZ/VZ0i54m0Qtqpi2DTqkI1kaPMTRi4LnsQn0iR5K1xBlFpS1xmJV7ko4a6qx5RcTkp7CzVjwoy5UDSWZ5jOCPLGRcoQdDt+wOk46bdr0yA\u003d"},"5":{"1":82,"2":0,"3":0,"4":"\u00GQ","13":"https://adwords-displayads.googleusercontent.com/da/b/preview.js?client\u003dasfe-arc-external-preview\u0026obfuscatedCustomerId\u003d5240877441\u0026creativeId\u003d288930210411\u0026htmlParentId\u003dad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60\u0026showVariations\u003dtrue\u0026sig\u003dACiVB_yMUjLwDjRO2T-0VAaVuRPt8uLHGQ","14":"https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/","15":"","17":"","18":"","20":"adv-5594449542310820","21":["domain1.com","domain2.com"]},"6":{"5":"-6668648012302470727","9":0},"7":1,"9":{"3":[{"1":{"1":"AClZvXLE9HJbFYq9TrAsXFgV4YkXsQt9lXp1xWjSB5aT5bFBpe4VNgo\u003d"},"2":"\u041/YHgdH4P"}}],"2":0.0,"3":"59917","4":1,"5":"","6":"ClD3Z2nP2P/////1/ff9oPjm7gU\u003d","7":"5751","9":0},"xsrf":"ABOvogJJJuNM1d0i22yN48ibBAY8vpvC_A:1535125743731"}

Из параметра {13} можно вытащить ссылку на объявление:

https://adwords-displayads.googleusercontent.com/da/b/preview.js?client=asfe-arc-external-preview&obfuscatedCustomerId=5240877441&creativeId=288930210411&htmlParentId=ad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60&showVariations=true&sig=ACiVB_yMUjLwDjRO2T-0VAaVuRPt8uLHGQ.

Там примерно 100 — 150 килобайт и в самом низу (и не только) можно найти отрывки из текста объявления. Какое-то время (дни, может недели) ссылка будет жить и по ней любой желающий сможет получить объявление.

Хранится он тут:
result->{1}->{4}->{1}. Кроме это важный параметр — это внутренний идентификатор объявления, который мы далее будем использовать для управления (блокировка/разблокировка объявления, блокировка/разблокировка аккаунта AdWords, который это объявление откручивает, запрос статистики показов, установка пометки «проверено», отправка сообщения о нарушении правил).

Выглядит так:

AClZvXJ2t4wiEZ/VZ0i54m0Qtqpi2DTqkI1kaPMTRi4LnsQn0iR5K1xBlFpS1xmJV7ko4a6qx5RcTkp7CzVjwoy5UDSWZ5jOCPLGRcoQdDt+wOk46bdr0yA=

Его длина 120 символов (с редкими исключениями).

Всего в этим потоке данных довольно много информации:

  • Тип объявления.
  • Целевой URL.
  • Домены, на которых откручивалось.
  • Сведения о рекламодателе (название, если есть и идентификатор).
  • Качественная характеристика кол-ва показов, например, «высокое».
  • Три миниатюры посадочной страницы в виде data:image.
  • Категория, куда относится объявление, например, «Телефония».

Результат получен — автоматизации поддаётся. Далее был наведён порядок в функциях, ибо рабочий прототип был ужасен, хотелось лишь скорее понять удастся ли процесс автоматизировать. Первая версия был предложена людям и началось доделывание и исправление ошибок. Первая проблема — «двухэтапники» не могли авторизоваться.

Двухэтапная авторизация

Если пойти проверять как это выглядит при выключенном JS, то можно увидеть множество вариантов авторизации: пароль по SMS, одноразовые пароли с бумажки, через приложение…
Каждый вариант автоматизировать, чтобы всем было удобно — это с ума сойти можно.

Спасение разработчика

Когда без JS в Chrome смотрел на механизм двухэтапной авторизации увидел ссылочку на выбор другого метода, за это и зацепился. Какой бы метод не был выбран по-умолчанию, всегда есть вариант перейти к выбору и выбрать SMS. Это и стало настоящим спасением. Конечно, пришлось делать проверку выбранного по-умолчанию метода, и в случае «неправильного» метода «нажимать» кнопку смены и выбирать «одноразовый пароль по SMS».

Всё, «двухэтапники» тоже смогли войти. Для самой авторизации лишь сделал сохранение в файл промежуточных данных из формы (та самая куча всяких скрытых полей) и форму ввода одноразового пароля.

Завершение процесс создания

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

Далее шли доработки, доделки, исправление недостатков...

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

Добавленные для удобства дополнения:

  • Список заблокированных рекламодателей.
  • Список заблокированных доменов.
  • Табличка доходов.
  • Ссылки AdSense.

Список заблокированных рекламодателей — это возможность смотреть и править, причём удобнее (но не красивее внешне) чем в штатном интерфейсе! Плюс есть возможность разблокирования «оптом», которой нету в штатном AdSense.

Список заблокированных доменов — аналогично предыдущему списку.

Возможность работы с AdX (через AdManager, куда AdX недавно переехал).

Доработок много, выше перечислены самые интересные на мой взгляд.

Функции отправки запроса и приёма результата

Ранее писал про запросы в виде json-строк, а подробнее обещал раскрыть позже.

Когда всё это делал нового ЦПО ещё не было, следовательно всё делалось для старого, с него и начнём.

Общение со старым ЦПО

С помощью наблюдений удалось выяснить, что основной обмен запросами идёт по одному адресу:

https://www.google.com/adsense/gp/creativeReview?ov=3&pid=pub-8958890276790964&authuser=0&tpid=pub-8958890276790964(&hl=ru)

Это важно, так как у меня везде используется английский и ПО распознаёт некоторые параметры, ожидая ответа на английском. То, что в скобках бывает не всегда, это просто параметр, который указывает на язык ответа, его можно применить почти ко всем продуктам Google.

Кроме адреса есть стандартная форма передаваемых post-данных (в инструментах разработчика они видны в разделе «Request Payload») — это json-строка с переменными method, params и xsrf:

{"method":"getArcSettings","params":"{\"1\":[\"ca-pub-8958890276790964\"]}","xsrf":"ABOvogJlvXKkBQUbPYEsM04recgCsukFMg:1535467881599"}

method — тут, вроде, всё ясно.
params — в зависимости от метода свой стандартный формат передаваемой json-строки.
xsrf — выше описано изначальное получение цифрового жетона, который мы используем для запроса, а в ответе нам приходит новый XSRF-token для следующего запроса.

Ответ тоже приходит в виде json-строки из частей result (запрошенная информация) и xsrf:

{"result":{"1":[{"1":"ca-pub-8958890276790964","2":{"1":"ca-pub-8958890276790964","2":0},"3":{"1":"ca-pub-8958890276790964","2":0}}]},"xsrf":"ABOvogIH7wJjD8t1xmuu8WbGplQowqjjJA:1535467883406"}

php-код функции

function creative_review($method, $params)
{ $xsrftoken = file_get_contents($GLOBALS['xsrftoken_file']); $creativeReview = new stdClass(); //to make json request string $creativeReview->method = $method; $creativeReview->params = $params; $creativeReview->xsrf = $xsrftoken; $creativeReview_post_request = json_encode($creativeReview); unset($creativeReview); $result = curl_post($GLOBALS['creative_review_req_string'], $creativeReview_post_request, $GLOBALS['arc_tab_req_string'], $GLOBALS['myheaders']); $result = json_decode($result); // decode result string if ($result->xsrf) file_put_contents($GLOBALS['xsrftoken_file'], $result->xsrf); // Renew standard XSRF token return $result;
}

Где своя функция post-запроса — curl_post($url, $postfields, $referer, $myheaders).

Переменные названы чтоб понятно было что есть что.
$myheaders почти во всех запросах содержит эти два заголовка:

accept-language:en-US;q=1,en;q=0.4
content-type:application/javascript; charset=UTF-8

$GLOBALS['creative_review_req_string']:

https://www.google.com/adsense/gp/creativeReview?ov=3&pid=pub-8958890276790964&authuser=0&tpid=pub-8958890276790964&hl=en

Тот самый адрес, через который идёт обмен данными.

$GLOBALS['arc_tab_req_string']:

https://www.google.com/adsense/new/u/0/pub-8958890276790964/main/allowAndBlockAds?webPropertyCode=ca-pub-8958890276790964&tab=arcTab

Это значение referer было и в оригинальных запросах, оно тоже не меняется.

Общение с новым ЦПО

Здесь с адресом для запроса сложнее — он меняется. Есть только начальная общая часть. Схема такая:

Общая часть + метод + '?' + GET-параметры + rpcTrackingId = <повторение предыдущих GET-параметров в URL-кодировке>:1.

https://www.google.com/ads-publisher-controls/acx/5/proto/creativereview/GetArcSettings?hl=ru&pc=ca-pub-8958890276790964&onearcClient=adsense&rpcTrackingId=%2Fads-publisher-controls%2Facx%2F5%2Fproto%2Fcreativereview%2FGetArcSettings%3Fhl%3Dru%26pc%3Dca-pub-8958890276790964%26onearcClient%3Dadsense%3A1

XSRF-token здесь передаётся в заголовке 'x-framework-xsrf-token' и он многоразовый, следовательно, в ответах он не приходит и обновлять его постоянно не нужно.

php-код функции

function creative_review_new($method, $params)
{ if (!isset($GLOBALS['xsrftoken_new'])) $GLOBALS['xsrftoken_new'] = file_get_contents($GLOBALS['temp_folder'] . 'xsrftoken_new.txt'); $myheaders = $GLOBALS['myheaders_new']; $myheaders[] = 'x-framework-xsrf-token:' . $xsrftoken; $query['pc'] = 'ca-' . $GLOBALS['pub_id']; $query['onearcClient'] = 'adsense'; $query['hl'] = 'en_US'; foreach ($query as $index => $value) $rpc[] = $index . '=' . $value; $append = ':1'; $query['rpcTrackingId'] = $GLOBALS['creative_review_new_string'] . $method . '?' . implode('&', $rpc) . $append; $query = http_build_query($query); $url = 'https://www.google.com' . $GLOBALS['creative_review_new_string'] . $method . '?' . $query; $result = curl_post($url, $params, $GLOBALS['new_arc_tab_req_string'], $myheaders); if (mb_strpos($result, 'Error 400 (Not Found)', 0, 'UTF-8') !== false) { return '-32000 XSRF token validation'; } $list = explode("\n", $result, 2); $result = $list[1]; $result = json_decode($result); // decode result string if (@$result->default->{5}) file_put_contents($GLOBALS['temp_folder'] . 'some_digi_token.txt', $result-> default->{5}); // Renew digi token if (@$result->default->{6}) file_put_contents($GLOBALS['temp_folder'] . 'some_long_token.txt', $result-> default->{6}); // Renew this long token return $result;
}

Где всё та же функция post-запроса — curl_post($url, $postfields, $referer, $myheaders).

Здесь $myheaders немного отличаются (javascript → json):

accept-language:en-US;q=1,en;q=0.4
content-type:application/json; charset=UTF-8

$GLOBALS['creative_review_new_string']:

/ads-publisher-controls/acx/5/proto/creativereview/

Небольшая постоянная часть.

$GLOBALS['new_arc_tab_req_string']:

https://www.google.com/adsense/new/u/0/pub-8958890276790964/arc/ca-pub-8958890276790964

Это значение referer было и в оригинальных запросах, оно не меняется.

Это необходимо для «листания» страниц с объявлениями. Снизу есть обновление двух разных жетонов. Здесь мы передаём сколько объявлений нам надо показать и один из жетонов из предыдущего ответа, порядковый номер первого запрашиваемого объявления не передаём. Здесь механизм запроса второй и последующей страницы не стандартный («покажи мне 10 объявления, начиная с 30-го»).

Функция запроса списка доменов и управления ими

Она практически такая же как функция общения со старым ЦПО, отличается только адресом обращения.

php-код функции

function blocking_controls($method, $params) { $xsrftoken = file_get_contents($GLOBALS['xsrftoken_file']); $creativeReview = new stdClass(); //to make json request string $creativeReview->method = $method; $creativeReview->params = $params; $creativeReview->xsrf = $xsrftoken; $creativeReview_post_request = json_encode($creativeReview); unset($creativeReview); $result = curl_post($GLOBALS['blocking_controls_req_string'], $creativeReview_post_request, $GLOBALS['arc_tab_req_string'], $GLOBALS['myheaders']); $result = json_decode($result); // decode result string if ($result->xsrf) file_put_contents($GLOBALS['xsrftoken_file'], $result->xsrf); // Renew standard XSRF token return $result;
}

$GLOBALS['blocking_controls_req_string']:

https://www.google.com/adsense/gp/blockingControls?ov=3&pid=pub-8958890276790964&authuser=0&tpid=pub-8958890276790964

Адрес для обмена данными.

XSRF-жетоны сохраняются на диск в файл, это необходимо чтобы работали запросы на блокировку/разблокировку объявлений, аккаунтов AdWords и прочих действий прямо из панели управления без необходимости запрашивать новый.

Обработка полученных ответов

Данные приходят либо в виде json-строк (ответы получаемые тремя функциями выше) и в виде JS-кода (запрашиваемые объявления), где ряд символов «зашифрован» шестнадцатеричной кодировкой (\xсимволов знаков>).

Отрывок из того объявления, ссылка на которое есть выше:

С 24 по 26 августа. target\x3d_blank title\x3d\x22\x22\x3e\x3cspan\x3eКупи Xiaomi Redmi S2 и получи Redmi 5 \x3cbr\x3eв подарок. \x3cbr\x3eПодробнее на сайте.

Для json есть в php функция, которая на выходе даст хоть объект, хоть массив.
Для «косоиксов» где-то в сети нашёл небольшую функцию, которая приводит данные к человеческому виду.

php-код функции

function hex_repl($html) { $i = 256; while ($i >= 0) { $hex = dechex($i); $html = str_ireplace("\x$hex", chr($i), $html); $i--; } return $html;
}

Результат расшифровки:

target=_blank title=""><span>Купи Xiaomi Redmi S2 и получи Redmi 5 <br>в подарок. С 24 по 26 августа. <br>Подробнее на сайте.

Распознавание объявлений

Текстовые. Начинал я с них. Они были важнее и, как оказалось, с ними было всё намного проще. Их только два вида: старые, с одним заголовком (которых уже практически не осталось) и новые, с двумя заголовками.

Объявление приходит уже в виде HTML-кода, но кроме объявления в получаемом ответе содержится очень много ненужных нам данных — код Javascript (даже не вникал в суть этого кода).

Распознавание в итоге свелось к следующим шагам:

  • обрезке «большого начала», оставляя лишь «хвост», где и содержится текст объявления;
  • созданию объекта с помощью класса DOMDocument;
  • поиску в цикле нужных значений: заголовки, текст объявления, текст ссылки.

Заголовки, текст и ссылка содержат определённые классы, за них и «цеплялся» распознавальщик.

Что где содержится и функция обработки текстовых объявлений

rhtitleline1 — заголовок 1;
rhtitleline2 — заголовок 2;
rhtitle — заголовок (только для объявлений с одним заголовком);
rhbody — текст объявления;
rhurl — отображаемый URL.

function text_ad($html) { $list = explode('</head>', $html); $ad_html = array_pop($list); unset($list, $html); $dom = new DOMDocument('1.0', 'UTF-8'); @$dom->loadHTML($ad_html); unset($ad_html); foreach ($dom->getElementsByTagName('a') as $a_node) { if (stripos($a_node->getAttribute('class'), 'rhtitleline1') !== false) { $ad['header1'] = $a_node->textContent; continue; } if (stripos($a_node->getAttribute('class'), 'rhtitleline2') !== false) { $ad['header2'] = $a_node->textContent; continue; } if (stripos($a_node->getAttribute('class'), 'rhbody') !== false) { $ad['body'] = $a_node->textContent; continue; } //Old ads (with just 1 header) support if (stripos($a_node->getAttribute('class'), 'rhtitle ') !== false || stripos($a_node->getAttribute('class'), 'rhtitle"') !== false) { $ad['header1'] = $a_node->textContent; continue; } if (stripos($a_node->getAttribute('class'), 'rhurl ') !== false || stripos($a_node->getAttribute('class'), 'rhurl"') !== false) { $ad['displayUrl'] = $a_node->textContent; continue; } } $fulltext = implode(' ', $ad); $ad['fulltext'] = $fulltext; if (!isset($GLOBALS['set_gl']['utf8_off'])) foreach ($ad as $index => $value) $ad[$index] = utf8_decode($value); return $ad;
}

Первые три строчки — обрезка лишнего текста. Всё нужное в самом конце.

$fulltext — все заголовки и тексты объявления для дальнейшей проверки.

На разных системах DOMDocument в разных кодировках выдаёт ответ. utf8_decode применяется или нет в зависимости от выбранной пользователем настройки. Чтобы у всех всё правильно отображалось внедрено такое преобразование.

В них проверяется только целевой URL. Графические. Не вижу здесь смысла изобретать велосипед (скорее всего, кривой и никому не нужный). Распознавания картинок нет, сохранения картинок для досмотра тоже (ибо картинки при желании можно и в ЦПО посмотреть).

Под этим общим названием скрывается целый ряд разных объявлений: Мультимедийные.

  • Многоформатные (Multi-Format).
  • Медийные (Rich Media).
  • Произвольный шаблон (HTML5).

Для многоформатных создано 3 функции распознавания в зависимости от типа объявления.
Для медийных создано 2 функции.

Для HTML5 создано 3 функции.

Фильтрация

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

  • Наличие в домене «blogspot.com».
  • Наличие в словах смеси кириллицы и латиницы.
  • Наличие «плохих» слов (список «плохих» слов настраивается пользователем).
  • Наличие перенаправления пользователя на домен, отличный от исходного.

Отчёт о работе

По итогам фильтрации составляется отчёт о проделанной работе.
Он строится в виде списка объявлений по каждому фильтру в своей графе, плюс графа для «хороших» объявлений, в отчёт включается следующая информация:

  • Идентификатор и текстовое название рекламодателя, если последнее есть.
  • Причина блокировки (только для заблокированных).
  • Заголовки и текст объявления.
  • Целевой и отображаемый URL.
  • Дата и время проверки.
  • Суммарное количество просмотров, что успело набрать объявление (только для заблокированных).
  • Ссылки для блокрования/разблокирования объявления и аккаунта рекламодателя.
  • Ссылки для блокировки целевого URL или домена.
  • Ссылка для подачи жалобы на объявление (есть в новом ЦПО).
  • Ссылки для добавления различных частей объявления в «белый список».
  • Ссылка на удаление объявления из отчёта.

Внешний вид основан на базе старого ЦПО (и единственном на момент создания оформления).


Кликабельно

При просмотре с мобильного каждая колонка занимает всю ширину экрана и появляются кнопки для выбора просматриваемой колонки.

Немного о безопасности

Можно сделать ограниченный доступ к панели управления (чтобы самому управлять из одного места) или «всемирный», чтобы из любого места можно было управлять.

Во втором случае адрес, где стоит ПО надо держать в секрете, плюс предусмотрена установка пароля на вход в панель управления. Первый случай безопасный — никто не влезет, если за рабочий ПК не сядет. Чтобы Ваш секретный адрес не «просочился» при переходах по ссылкам на сторонние сайты (из объявлений) сделано следующее:

Результат автоматизации

24 часа 7 дней в неделю все вновь появившиеся в ЦПО объявления досматриваются с интервалом в пару-тройку минут. В результате чего неугодные (по критериям, заданным пользователем) отправляются в раздел «заблокировано». Точно никогда не считал, но примерно из 100 заблокированных штук 90 — 95 заблокированы не зря. Из ста «чистых» по мнению ПО в среднем менее одного «плохого».

Всё что ведёт на мобильные подписки, всё что предлагает «скачать», просто скачать или «скачать файл» без какой-либо конкретики вообще, всё что предлагает «смотреть видео», опять же без каких-либо подробностей, всё что ведёт совсем не туда, о чём указано в заголовке и тексте объявления, любые упоминания казино в странах, где это запрещено законом. Что я называю «плохими объявлениями»?

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

Стало меньше и воровства в виде неосознанных подписок даже без карты «МегаФона»!

А причём здесь карта «МегаФона»?

По ссылке выше есть такое «весьма интересное» «ключевое преимущество карты» «дополнительная защита денежных средств»:

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

Но не у всех пользователей наших сайтов есть «Мегафонкарты» и аналоги других ОПСОСов.
Поэтому, господа, защищайте посетителей своих ресурсов от нежелательных списаний самостоятельно!

Проект с открытым исходным кодом есть на GitHub.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Из песочницы] От var b до собеседования

Вы почти закончили универ или колледж? Вас пригласили на собеседования, но вы идете туда без подготовки? У вас нет образования (высшего), но хотите работать программистом или в сфере IT? Речь пойдёт по большей степени о поиске работы, я буду говорить ...

OpenSceneGraph: Основы работы с геометрией сцены

OpenGL, являющийся бэкэндом для OpenSceneGraph, использует геометрические примитивы (такие как точки, линии, треугольники и полигональные грани) для построения всех объектов трехмерного мира. Эти данные хранятся в специальных массивах. Эти примитивы задаются данными об их вершинах, в которые входят координаты вершин, ...