Как я боролся с воровством… с помощью php

в 13:34, , рубрики: Google AdSense, open source, php, блокировщик рекламы, контекстная реклама, мобильные подписки, ненормальное программирование, подписка одним кликом, Софт

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

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

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

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

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

Вот здесь чуть подробнее. А здесь история о хорошем способе «заработать».

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

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

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

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

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

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

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

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

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

.

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

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

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

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

Начало

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

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

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

{"method":"searchArcApprovals","params":"{"1":"ca-pub-6928690776790362","2":{"1":0,"2":1,"3":0,"4":{"1":{"1":"AClZvXKL6S3HChRty5YBa81BLWDBQkb3FYDsifZ9V/mBTKbOGlj3gMWVpzTtXggA1880Le9NyVZIicNm/4pz724e/MO8fyLfjOReF205cyjLV9C8OCCeKe7VvZHyvyKpXh8x9smTQ0n8qIIqzuIXle5UK0hD4VBkZDvy//qoSPRCr94UtWYqqi//Rot22LJ2JFNjWEGb4n1YQbAw0cKWPR3LAugPBajInWXEFGWJRTnmY2TkI5VzUzIkcXpJ/bkajn3c8GnecCfFNvNhGLS10VXdRwiykngG3xfoMTRhQOR5GXbm4kwdIhzQUM/d6xP0Xda3FOIZGGk9bymneg+9oDY+rMFiRfDFCb66g50t9J9r++oHXjek09Ci1rqC7LOw2pvkqp3hjG6RyVmsiT/eWGq+OsfjE7CgRk43QIRMSa+jlZBQhARUPlpUXzyZyoTiIPTRZ5ND/4MnIMqaUWSRoDGffiE/XkHJPEkNZtLX2XR5gZ3x5/K+ejU/fqxfZIjI6A3kueJybNA46wSLbmflhDCGDJEE2aeYemLFGqNzFG43B80LzU3yuwgZhrLu/jaMvBJozi0nq+gXEz6r+8tic4fvsQ9lWDA+IXzXw6MKzamgfWV0ORGDW0+966KIY6IkjtIlNRKGyp3pSAd2Po+br4Dl4WNwSkMdmuV60wOrkb5BpnKZKIhDtpjWF7q6ly3FFhwo8Ktdq5ddVJ8ijJ9Y9tQhs2O0idA9N0yV86khV1IQ72OgbMv15qAswnbqF9WCo3qpfJNjJqMCHBRTohPCxhRp0cWz2thszZTmDDADPxU46sclnurd/JxHFO7lJZVdrsFB4vdLIx9kObV3bP1gOpU66kdcmom2tiedknugj7s0jLcgf1EfXnp+SUUAQyoqwS+kdhhQtGqSXgI2TopsuaLVzj+EtAuPwWeLvtI9CFPSe4o2x+gjCRPl8wVvWKV5FIrZavUVOAHZIL4nKyJjHxZi3jPfVnAia/hq1gW6XKoCg1eWGg/cAWZY4mZYQ6W4XnC0MY0uMC6fhPQdXnIS5iLZNhan80jbr/leBr4fO22+tXc6oZpZsDkXd0r3ilBJFPS2I/zAhotuzZgNA+nF2N86pyiSrdeEYFDhKWKadcKAVc3BMxxlrqZYcAXnlus9GW7R9F/ImXQ/fjRfSjVRUaJuQ0EnFejNAwdGcS6STYMa1G0wnNMAKcZ52xcHgil1SZ6N9BQ7A27z6eViOxw0LHBqNJIRZwQml2KjPd5b00D9XvohDr6jBqYXLGS/HMVvpGDJZLDI2LRlmkqBqx7YEgDZqvspeoMLHIJP22SkQDnaJtsOLGVBSi20ZD5nRyjAgS6MmcgFCvfJVWjCIL1RPHqmUU90eK4WXve0ayH9cJnpbtWrkXYCibhVPCMmYowMROw7rI4bPir0"}}}}","xsrf":"ABOvogKvrE9fIqAKh0w02RIsB4OJ4hsB_g:1535467885347"}

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

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

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

{"result":{"1":[{"1":0,"3":0,"4":{"1":"AClZvXJ2t4wiEZ/VZ0i54m0Qtqpi2DTqkI1kaPMTRi4LnsQn0iR5K1xBlFpS1xmJV7ko4a6qx5RcTkp7CzVjwoy5UDSWZ5jOCPLGRcoQdDt+wOk46bdr0yAu003d"},"5":{"1":82,"2":0,"3":0,"4":"u003cdiv idu003d"ad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60"u003eu003c/divu003e","5":"u003cdiv idu003d"ad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60"u003eu003c/divu003e","6":"u003cdivu003eu041cu043du043eu0433u043eu0444u043eu0440u043cu0430u0442u043du044bu0435u003cspan idu003d'multi-format-tooltip'u003eu003c/spanu003eu003c/divu003eu003ca classu003d'arc-url-link-ellipsis' targetu003d'_blank' hrefu003d'https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/' titleu003d'https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/'u003ehttps://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/u003c/au003e","7":"u003cdiv classu003d'arc-one-by-one-legend'u003eu0422u0438u043f u043eu0431u044au044fu0432u043bu0435u043du0438u044fu003c/divu003eu003cdiv classu003d'arc-one-by-one-data'u003eu041cu043du043eu0433u043eu0444u043eu0440u043cu0430u0442u043du044bu0435u003cspan idu003d'multi-format-tooltip'u003eu003c/spanu003eu003c/divu003eu003cdiv classu003d'arc-one-by-one-legend'u003eu0426u0435u043bu0435u0432u043eu0439 URLu003c/divu003eu003cdiv classu003d'arc-one-by-one-data'u003eu003ca classu003d'arc-url-link-ellipsis' targetu003d'_blank' hrefu003d'https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/' titleu003d'https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/'u003ehttps://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/u003c/au003eu003c/divu003eu003cdiv classu003d'arc-one-by-one-legend'u003eu0414u043eu043cu0435u043du044b u0438u0437u0434u0430u0442u0435u043bu0435u0439u003c/divu003eu003cdiv classu003d'arc-one-by-one-data'u003e4aynikam.ruu003c/divu003eu003cdiv classu003d'arc-one-by-one-data'u003eandroidphone.suu003c/divu003eu003cdiv classu003d'arc-one-by-one-data'u003eandroidphones.ruu003c/divu003eu003cdiv classu003d'arc-one-by-one-data'u003efull-repair.comu003c/divu003eu003cdiv classu003d'arc-one-by-one-data'u003ehowgadget.comu003c/divu003eu003cdiv classu003d'arc-one-by-one-legend'u003eu041eu0431u043du0430u0440u0443u0436u0435u043du043du044bu0439 u0440u0435u043au043bu0430u043cu043eu0434u0430u0442u0435u043bu044cu003cspan idu003d'adx-advertiser-tooltip'u003eu003c/spanu003eu003c/divu003eu003cdiv classu003d'arc-one-by-one-data'u003eDNS Shopu003c/divu003e","8":"u003cdivu003eu003cspan classu003d'arc-impression-score high'u003eu0412u042bu0421u041eu041au041eu0415u003c/spanu003e u0447u0438u0441u043bu043e u043fu043eu043au0430u0437u043eu0432u003c/divu003e","9":{"1":"u003ca hrefu003d"https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/" targetu003d"_blank"u003eu003cimg onerroru003d"this.srcu003d'data:image/gif;base64,RA7'" srcu003d"https://www.google.com/webpagethumbnail?cu003d58u0026su003d400:400u0026ru003d4u0026du003dhttps://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/u0026au003dAIYkKU9ZGGjFTOWtm771MQwgDYxqtlBLCw" borderu003d0 altu003d""u003eu003c/au003e","2":"u003ca hrefu003d"https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/" targetu003d"_blank"u003eu003cimg onerroru003d"this.srcu003d'data:image/gif;base64,R0AA7'" srcu003d"https://www.google.com/webpagethumbnail?cu003d58u0026su003d400:400u0026ru003d3u0026du003dhttps://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/u0026au003dAIYkKU_CQ2K6v5f11Nk1RXtc87FtmG2B1w" borderu003d0 altu003d""u003eu003c/au003e","3":"u003ca hrefu003d"https://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/" targetu003d"_blank"u003eu003cimg onerroru003d"this.srcu003d'data:image/gif;base64,R0lAA7'" srcu003d"https://www.google.com/webpagethumbnail?cu003d58u0026su003d400:400u0026ru003d6u0026du003dhttps://www.dns-shop.ru/actions/c09a061b-a048-11e8-9547-00155d03330d/u0026au003dAIYkKU_My0a48LAsW-ZKpQX-ATXkMoPEVg" borderu003d0 altu003d""u003eu003c/au003e"},"10":"https://adwords-displayads.googleusercontent.com/da/b/preview.js?clientu003dasfe-arc-external-previewu0026obfuscatedCustomerIdu003d5240877441u0026creativeIdu003d288930210411u0026htmlParentIdu003dad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60u0026sigu003dACiVB_yMUjLwDjRO2T-0VAaVuRPt8uLHGQ","13":"https://adwords-displayads.googleusercontent.com/da/b/preview.js?clientu003dasfe-arc-external-previewu0026obfuscatedCustomerIdu003d5240877441u0026creativeIdu003d288930210411u0026htmlParentIdu003dad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60u0026showVariationsu003dtrueu0026sigu003dACiVB_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":"AClZvXLE9HJbFYq9TrAsXFgV4YkXsQt9lXp1xWjSB5aT5bFBpe4VNgou003d"},"2":"u0418u043du0442u0435u0440u043du0435u0442 u0438 u0442u0435u043bu0435u043au043eu043cu043cu0443u043du0438u043au0430u0446u0438u0438","3":"u0422u043eu0432u0430u0440u044b u0438 u0443u0441u043bu0443u0433u0438, u0441u0432u044fu0437u0430u043du043du044bu0435 u0441 u0442u0435u043bu0435u043au043eu043cu043cu0443u043du0438u043au0430u0446u0438u044fu043cu0438, u0432 u0442u043eu043c u0447u0438u0441u043bu0435 u043au0430u0431u0435u043bu044cu043du043eu0435 u0438 u0441u043fu0443u0442u043du0438u043au043eu0432u043eu0435 u043eu0431u0441u043bu0443u0436u0438u0432u0430u043du0438u0435 u0438 u0434u043eu0441u0442u0443u043f u0432 u0418u043du0442u0435u0440u043du0435u0442."},{"1":{"1":"AClZvXKrUJJ3kKBen2scP56BynOtGhf160i1F1LLmtBj3b/oh2dUFg8u003d"},"2":"u041cu043eu0431u0438u043bu044cu043du044bu0435 u0442u0435u043bu0435u0444u043eu043du044b","3":"u041cu043eu0431u0438u043bu044cu043du044bu0435 u0438 u0441u043eu0442u043eu0432u044bu0435 u0442u0435u043bu0435u0444u043eu043du044b, u0430 u0442u0430u043au0436u0435 u0441u043eu043fu0443u0442u0441u0442u0432u0443u044eu0449u0430u044f u0438u043du0444u043eu0440u043cu0430u0446u0438u044f, u043du0430u043fu0440u0438u043cu0435u0440 u0442u0435u0445u043du0438u0447u0435u0441u043au0438u0435 u0445u0430u0440u0430u043au0442u0435u0440u0438u0441u0442u0438u043au0438 u0438 u0441u0440u0430u0432u043du0438u0442u0435u043bu044cu043du044bu0439 u0430u043du0430u043bu0438u0437 u0442u043eu0432u0430u0440u043eu0432. u0412 u044du0442u0443 u043au0430u0442u0435u0433u043eu0440u0438u044e u043du0435 u0432u0445u043eu0434u044fu0442 u0430u043au0441u0435u0441u0441u0443u0430u0440u044b u0434u043bu044f u043cu043eu0431u0438u043bu044cu043du044bu0445 u0442u0435u043bu0435u0444u043eu043du043eu0432."},{"1":{"1":"AClZvXL4W+khZ4O9SJiu97cTbTs2+0Wecf1IVNju8ffd4ysIT9PJ7XYu003d"},"2":"u041cu043eu0431u0438u043bu044cu043du044bu0435 u0442u0435u043bu0435u0444u043eu043du044b u0438 u0430u043au0441u0435u0441u0441u0443u0430u0440u044b u0434u043bu044f u043du0438u0445","3":"u041cu043eu0431u0438u043bu044cu043du044bu0435 u0442u0435u043bu0435u0444u043eu043du044b, u0430 u0442u0430u043au0436u0435 u0441u043eu043fu0443u0442u0441u0442u0432u0443u044eu0449u0438u0435 u0430u043au0441u0435u0441u0441u0443u0430u0440u044b u0438 u0430u043fu043fu0430u0440u0430u0442u043du043eu0435 u043eu0431u0435u0441u043fu0435u0447u0435u043du0438u0435, u043du0430u043fu0440u0438u043cu0435u0440 u0447u0435u0445u043bu044b, u043cu043eu043du043eu043fu043eu0434u044b u0434u043bu044f u0441u0435u043bu0444u0438, u0437u0430u0449u0438u0442u043du044bu0435 u044du043au0440u0430u043du044b u0438 u0437u0430u0440u044fu0434u043du044bu0435 u0443u0441u0442u0440u043eu0439u0441u0442u0432u0430."},{"1":{"1":"AClZvXLQ3gPoVwjQbokDpB3+nni4xURwH5+YlnwkqjYtUowjhiKvk8Qu003d"},"2":"u041fu041a u0438 u0431u044bu0442u043eu0432u0430u044f u044du043bu0435u043au0442u0440u043eu043du0438u043au0430","3":"u0422u043eu0432u0430u0440u044b, u0443u0441u043bu0443u0433u0438 u0438 u0438u043du0444u043eu0440u043cu0430u0446u0438u044f, u0441u0432u044fu0437u0430u043du043du044bu0435 u0441 u043au043eu043cu043fu044cu044eu0442u0435u0440u0430u043cu0438 u0438 u0431u044bu0442u043eu0432u043eu0439 u044du043bu0435u043au0442u0440u043eu043du0438u043au043eu0439."},{"1":{"1":"AClZvXLKYOGgOROaa32IUxU15jP89AtTM4dV24WKS+daMhqJMTNmeSYu003d"},"2":"u0422u0435u043bu0435u0444u043eu043du0438u044f","3":"u0422u043eu0432u0430u0440u044b, u0443u0441u043bu0443u0433u0438, u0430 u0442u0430u043au0436u0435 u0438u043du0444u043eu0440u043cu0430u0446u0438u043eu043du043du044bu0435 u0438 u0434u0440u0443u0433u0438u0435 u0440u0435u0441u0443u0440u0441u044b, u0441u0432u044fu0437u0430u043du043du044bu0435 u0441 u0442u0435u043bu0435u0444u043eu043du0438u0435u0439 u0438 u0433u043eu043bu043eu0441u043eu0432u043eu0439 u0441u0432u044fu0437u044cu044e."}]},"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+qOQAAAAAnMJaYSAFQAFoLCS8wxxaTatL1EAJgp7737gYu003d","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. А если JS отключить? Не обделит же Google пользователей без JS возможностью авторизоваться? Что ж, попробуем без JS. Внешне окошко авторизации уже выглядит куда проще. Как и ранее, сначала вводим логин, а пароль на следующей странице. Самое главное, что и в плане HTML тоже намного проще! Обычный тэг «form» с обычными полями «input», правда не без кучи защитных или системных скрытых полей. Но скрытые поля — не проблема, ведь что на входе получили, то следующему скрипту и передали. И таким образом получилось войти в Google. А двухэтапная авторизация? Об этом позже. Сначала надо убедиться, что получится вытаскивать объявления для их досмотра, иначе это всё смысла не имеет.

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

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

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

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

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

Подробности

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

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

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

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

https://www.google.com/adsense/gwt-properties?pid=pub-0000000000000000&authuser=0&tpid=pub-0000000000000000&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-0000000000000000">
<meta name="gwt:property" content="tpid=pub-0000000000000000">
<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-6928690776790362&tpid=pub-6928690776790362&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-6928690776790362','tpid': 'pub-6928690776790362',};window['__app_metadata'] = {'token': 'ABiMD8TT9vzK99SFB7iaI0ssBySxT9jjrQ:1535116725529','pre': '/ads-publisher-controls/acx','scs': 'https://ssl.gstatic.com/ads-publisher-controls/onearc_20180822-12_RC00','oacf': 'x7bx221x22:x5b5,25,22,8,27,32,43,44,45,48,49,5,25,22,8,27,32,43,44,45,48,49,29,46x5dx7d','hats': 'ibhswcm2x2iztju5i6jbbzlkma',};

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

'token': 'ABiMD8TT9vzK99SFB7iaI0ssBySxT9jjrQ:1535116725529'

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

Подробности

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

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

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

{
  "1":"ca-pub-6928690776790362",
  "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-6928690776790362"}","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/e893nEVz3CmF68pphNp71kjJtvwBS2JtSWhFc81Ys51GEwu003du003d"}}},"xsrf":"ABOvogJLbcTkcBxU_TCJddIrW4L-mVwPcw:1535115072920"}

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

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

{"method":"searchArcApprovals","params":"{"1":"ca-pub-6928690776790362","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/e893nEVz3CmF68pphNp71kjJtvwBS2JtSWhFc81Ys51GEwu003du003d"}}},"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-6928690776790362",
  "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-6928690776790362","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+wOk46bdr0yAu003d"},"5":{"1":82,"2":0,"3":0,"4":"u00GQ","13":"https://adwords-displayads.googleusercontent.com/da/b/preview.js?clientu003dasfe-arc-external-previewu0026obfuscatedCustomerIdu003d5240877441u0026creativeIdu003d288930210411u0026htmlParentIdu003dad-parent-id-6A2DE3D206234468F53C743C0EEACD67A59E6C5B62C0371F770419826258CB1AD9591F60u0026showVariationsu003dtrueu0026sigu003dACiVB_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":"AClZvXLE9HJbFYq9TrAsXFgV4YkXsQt9lXp1xWjSB5aT5bFBpe4VNgou003d"},"2":"u041/YHgdH4P"}}],"2":0.0,"3":"59917","4":1,"5":"","6":"ClD3Z2nP2P/////1/ff9oPjm7gUu003d","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 килобайт и в самом низу (и не только) можно найти отрывки из текста объявления.

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

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

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-6928690776790362&authuser=0&tpid=pub-6928690776790362(&hl=ru)

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

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

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

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

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

{"result":{"1":[{"1":"ca-pub-6928690776790362","2":{"1":"ca-pub-6928690776790362","2":0},"3":{"1":"ca-pub-6928690776790362","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-6928690776790362&authuser=0&tpid=pub-6928690776790362&hl=en

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

$GLOBALS['arc_tab_req_string']:

https://www.google.com/adsense/new/u/0/pub-6928690776790362/main/allowAndBlockAds?webPropertyCode=ca-pub-6928690776790362&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-6928690776790362&onearcClient=adsense&rpcTrackingId=%2Fads-publisher-controls%2Facx%2F5%2Fproto%2Fcreativereview%2FGetArcSettings%3Fhl%3Dru%26pc%3Dca-pub-6928690776790362%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-6928690776790362/arc/ca-pub-6928690776790362

Это значение 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-6928690776790362&authuser=0&tpid=pub-6928690776790362

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

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

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

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

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

targetx3d_blank titlex3dx22x22x3ex3cspanx3eКупи Xiaomi Redmi S2 и получи Redmi 5 x3cbrx3eв подарок. С 24 по 26 августа. x3cbrx3eПодробнее на сайте.

Для 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 — все заголовки и тексты объявления для дальнейшей проверки.

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

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

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

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

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

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

Фильтрация

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

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

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

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

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

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

Как я боролся с воровством… с помощью php - 2
Кликабельно

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

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

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

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

  • В каждой внешней ссылке стоит такой атрибут
    rel="noreferrer"
  • Чтобы referer передавался только в пределах одного домена в head стоит тэг:
    <meta name="referrer" content="same-origin">
  • Все внешние ссылки идут через «очиститель referer»:
    http://nullrefer.com/?http://free.da...

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

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

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

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

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

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

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

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

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

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

Автор: sergeich_gs

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js