Article Массовая проверка посещаемости сайтов через Google Apps Script

  • Автор темы Admin

Admin

#1
Администратор
Регистрация
31.12.2019
Сообщения
7,124
Реакции
34
В предыдущей статье, рассмотрел, как можно парсить цели используя ресурсы Google Sheets. Было бы неплохо, обогатить данные. Как минимум, получить статистику посещений и какую-то базовую информацию для планирования дальнейшей деятельности.

Начнем с посещалки

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

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

Мой вариант - это расширения для браузеров. Есть кучка расширений для Chrome и Firefox, которые позволяют посмотреть данные по статистике посещаемости, основным странам посетителей, типу трафика и т.д. и т.п. Кстати, не стоит недооценивать последнюю метрику, если много покупного трафика (пофиг, контекст, тизерки и т.п.), значит в ресурс валивются деньги и народ идет заточенный на конверсию. Для баг баунти, это важный аргумент для поднятия цены за уязвимость.

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

Почему требуют открытый исходный код, думаю не сложно догадаться. Расширение может сделать почти что угодно в браузере и получить доступ к многим данным. Здесь на форуме есть тема, как через дубль расширения браузера крадут криптовалюту. Раньше была тема с дублированием расширений, но при поиске в Google или Яндекс, дубль расширения подменял ссылки на партнерские или фишинговые.

Вскрываем расширения

Выбрал два хороших, на мой взгляд, расширения для FF: HypeStat Analyzer и StatsCrop. Идем в официальный магазин Add-ons для Firefox Browser. Устанавливаем оба. Следом топаем по следующему пути:

  1. %appdata%\Mozilla\Firefox\Profiles\{PROFILE_ID}\extensions\ для Windows
  2. ~/.mozilla/firefox/{PROFILE_ID}/extensions/ для Linux (наверное, и для Mac, нет под рукой)

Кстати, точный путь к профилю FF можно узнать, если в адресной строке браузера вбить about:support. В целом, советую полистать для себя, можно найти новую информацию о своем браузере.

В папке “extensions” лежат расширения в виде файла .xpi. Это обычный архив, поэтому спокойно разархивируем. Внутри папок встретим набор файлов, схожий с обычным сайтом.

Ищем обращения к API и удивляемся тому, насколько легко получаем два важных адреса:
  1. h_ttps://hypestat.com/api/E9asC1KOutzPrlALHn34X/
  2. h_ttps://extensions.statscrop.com/api/v1/data/?domain=

Обращаю внимание, что “E9asC1KOutzPrlALHn34X” это не какой-то идентификатор конкретной установки или что-то подобное. Эта строчка, по сути своей, просто попытка закрыться от автоматического парсинга. У всех установивших расширение она будет одинаковой. При обновлении расширения, может измениться и тогда наш код перестанет работать. Нужно будет снова залезть в расширение и получить новое значение.

Открываем обе ссылки в браузере, подставив какие-то тестовые доменные, чтобы понимать с чем придется работать.

JSON:
{
"success": true,
"data": {
"domain": [
{
"domain": "google.com",
"updated": "2024-06-13 18:08:48"
}
],
"rank": [
{
"hype_rank": "1",
"semrush_rank": "8",
"similarweb_rank": "1"
}
],
"traffic": [
{
"daily_visits": "2846234317",
"monthly_visits": "86240899805",
"daily_page_views": "24911429532",
"pageviews_per_user": "8.75",
"average_visit_duration": "641",
"bounce_rate": "0.2802404",
"global_reach": "57.7338120",
"monthly_visits_sem": "132200570116",
"monthly_unique_visitors_sem": "6112926145",
"monthly_visits_similarweb": "86249524745",
"monthly_users_diff": "-15.71",
"monthly_visits_diff": "-3.21",
"time_on_site_diff": "3.56",
"pages_per_visit_diff": "7.32",
"bounce_rate_diff": "1.63"
}
],
"traffic_sources": [
{
"direct": "95.54",
"referral": "3.02",
"search": "0.65",
"social": "0.79",
"paid": "0.00"
}
],
"desktop_vs_mobile": [
{
"desktop": "26.26",
"mobile": "73.74"
}
],
"total_visits_last_3_months": [
{
"month": "MAR",
"mothly_visits": 85458446252,
"monthly_visits_short": "85.5B",
"percent": 96
},
{
"month": "APR",
"mothly_visits": 89101043295,
"monthly_visits_short": "89.1B",
"percent": 100
},
{
"month": "MAY",
"mothly_visits": 86240899805,
"monthly_visits_short": "86.2B",
"percent": 97
}
],
"visitors_by_country": [
{
"country": "United States",
"visits_percent": "26.49",
"page_views_percent": "",
"country_rank": ""
},
{
"country": "India",
"visits_percent": "4.56",
"page_views_percent": "",
"country_rank": ""
},
{
"country": "Brazil",
"visits_percent": "4.41",
"page_views_percent": "",
"country_rank": ""
},
{
"country": "United Kingdom",
"visits_percent": "4.07",
"page_views_percent": "",
"country_rank": ""
},
{
"country": "Japan",
"visits_percent": "3.91",
"page_views_percent": "",
"country_rank": ""
}
],
"subdomains": [
{
"subdomains": "google.com",
"reach_percent": "88.69",
"page_views_percent": "35.05",
"page_views_per_user": "4.69"
},
{
"subdomains": "mail.google.com",
"reach_percent": "19.45",
"page_views_percent": "27.32",
"page_views_per_user": "16.67"
},
{
"subdomains": "docs.google.com",
"reach_percent": "14.23",
"page_views_percent": "13.16",
"page_views_per_user": "10.98"
},
{
"subdomains": "accounts.google.com",
"reach_percent": "13.67",
"page_views_percent": "5.16",
"page_views_per_user": "4.48"
},
{
"subdomains": "drive.google.com",
"reach_percent": "9.24",
"page_views_percent": "4.41",
"page_views_per_user": "5.660"
}
],
"backlinks": [
{
"total_backlinks": "3500063660",
"follow_links": "2879769796",
"no_follow_links": "620293863",
"referring_domains": "1299823",
"referring_ips": "326017"
}
],
"backlinks_by_country": [
{
"country": "Russian Federation",
"domains": "465,557"
},
{
"country": "United States",
"domains": "418,969"
},
{
"country": "Germany",
"domains": "61,760"
},
{
"country": "France",
"domains": "23,442"
},
{
"country": "United Kingdom",
"domains": "18,677"
}
],
"backlinks_by_tlds": [
{
"tld": ".com",
"domains": "554,290"
},
{
"tld": ".ru",
"domains": "416,966"
},
{
"tld": ".ua",
"domains": "45,993"
},
{
"tld": ".edu",
"domains": "918"
},
{
"tld": ".gov",
"domains": "249"
}
],
"search_engine_indexes": [
{
"Google Index": "543,000,000",
"Bing Index": "228,000",
"Baidu Index": "81"
}
],
"sem_rush": [
{
"rank": "8",
"organic_keywords": "173075173",
"organic_traffic": "519144364",
"organic_cost": "2086380030"
}
],
"moz": [
{
"domain_authority": 94,
"page_authority": 89,
"moz_rank": 9
}
],
"page_speed": [
{
"load_time": "150",
"slower_sites_percent": "64",
"speed_score_d": "98",
"speed_score_m": "69"
}
],
"estimated_earnings": [
{
"daily_ads_revenue": "94315262.66",
"estimated_website_worth": "136563108878.06"
}
],
"mywot": [
{
"mw_status": "SAFE",
"mw_safety_reputations": 94,
"mw_safety_confidence": 64,
"mw_child_safety_reputations": 93,
"mw_child_safety_confidence": 64
}
],
"ssl": [
{
"domain_ssl_status": "1"
}
],
"http2": [
{
"http2": "1"
}
],
"host": [
{
"server_ip": "142.250.191.174",
"asn": "AS15169",
"isp": "Google LLC",
"latitude": "37.751",
"longitude": "-97.822",
"city": "Farmingdale",
"region": "New York",
"region_code": "NY",
"postal_code": "11735",
"country_name": "United States",
"country_code": "US"
}
],
"technologies": {
"Web Servers": [
{
"st_name": "Google Web Server",
"st_icon": "Google.svg"
}
]
},
"whois": [
{
"domain_created": "1997-09-15",
"whois": "Domain Name: google.com\nRegistry Domain ID: 2138514_DOMAIN_COM-VRSN\nRegistrar WHOIS Server: whois.markmonitor.com\nRegistrar URL: http:\/\/www.markmonitor.com\nUpdated Date: 2019-09-09T15:39:04+0000\nCreation Date: 1997-09-15T07:00:00+0000\nRegistrar Registration Expiration Date: 2028-09-13T07:00:00+0000\nRegistrar: MarkMonitor, Inc.\nRegistrar IANA ID: 292\nRegistrar Abuse Contact Email: [email protected]\nRegistrar Abuse Contact Phone: +1.2086851750\nDomain Status: clientUpdateProhibited (https:\/\/www.icann.org\/epp#clientUpdateProhibited)\nDomain Status: clientTransferProhibited (https:\/\/www.icann.org\/epp#clientTransferProhibited)\nDomain Status: clientDeleteProhibited (https:\/\/www.icann.org\/epp#clientDeleteProhibited)\nDomain Status: serverUpdateProhibited (https:\/\/www.icann.org\/epp#serverUpdateProhibited)\nDomain Status: serverTransferProhibited (https:\/\/www.icann.org\/epp#serverTransferProhibited)\nDomain Status: serverDeleteProhibited (https:\/\/www.icann.org\/epp#serverDeleteProhibited)\nRegistrant Organization: Google LLC\nRegistrant State\/Province: CA\nRegistrant Country: US\nRegistrant Email: Select Request Email Form at https:\/\/domains.markmonitor.com\/whois\/google.com\nAdmin Organization: Google LLC\nAdmin State\/Province: CA\nAdmin Country: US\nAdmin Email: Select Request Email Form at https:\/\/domains.markmonitor.com\/whois\/google.com\nTech Organization: Google LLC\nTech State\/Province: CA\nTech Country: US\nTech Email: Select Request Email Form at https:\/\/domains.markmonitor.com\/whois\/google.com\nName Server: ns2.google.com\nName Server: ns1.google.com\nName Server: ns3.google.com\nName Server: ns4.google.com\nDNSSEC: unsigned\nURL of the ICANN WHOIS Data Problem Reporting System: http:\/\/wdprs.internic.net\/\n>>> Last update of WHOIS database: 2023-04-06T14:03:28+0000 <<<\n\nFor more information on WHOIS status codes, please visit:\n  https:\/\/www.icann.org\/resources\/pages\/epp-status-codes\n\nIf you wish to contact this domain\u00e2\u20ac\u2122s Registrant, Administrative, or Technical\ncontact, and such email address is not visible above, you may do so via our web\nform, pursuant to ICANN\u00e2\u20ac\u2122s Temporary Specification. To verify that you are not a\nrobot, please enter your email address to receive a link to a page that\nfacilitates email communication with the relevant contact(s).\n\nWeb-based WHOIS:\n  https:\/\/domains.markmonitor.com\/whois\n\nIf you have a legitimate interest in viewing the non-public WHOIS details, send\nyour request and the reasons for your request to [email protected]\nand specify the domain name in the subject line. We will review that request and\nmay ask for supporting documentation and explanation.\n\nThe data in MarkMonitor\u00e2\u20ac\u2122s WHOIS database is provided for information purposes,\nand to assist persons in obtaining information about or related to a domain\nname\u00e2\u20ac\u2122s registration record. While MarkMonitor believes the data to be accurate,\nthe data is provided \"as is\" with no guarantee or warranties regarding its\naccuracy.\n\nBy submitting a WHOIS query, you agree that you will use this data only for\nlawful purposes and that, under no circumstances will you use this data to:\n  (1) allow, enable, or otherwise support the transmission by email, telephone,\nor facsimile of mass, unsolicited, commercial advertising, or spam; or\n  (2) enable high volume, automated, or electronic processes that send queries,\ndata, or email to MarkMonitor (or its systems) or the domain name contacts (or\nits systems).\n\nMarkMonitor reserves the right to modify these terms at any time.\n\nBy submitting this query, you agree to abide by this policy.\n\nMarkMonitor Domain Management(TM)\nProtecting companies and consumers in a digital world.\n\nVisit MarkMonitor at https:\/\/www.markmonitor.com\nContact us at +1.8007459229\nIn Europe, at +44.02032062220\n--\n"
}
],
"other": [
{
"estimated_hidden": "0"
}
]
}
}
JSON:
{"domain":"google.com","domain_decode":"Google.com","thumbnail_url":"https:\/\/assets.statscrop.com\/g\/oo\/gle\/com\/thumbnail.jpg","thumbnail_webp_url":"https:\/\/assets.statscrop.com\/g\/oo\/gle\/com\/thumbnail.webp","favicon_url":"https:\/\/assets.statscrop.com\/g\/oo\/gle\/com\/favicon.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/g\/oo\/gle\/com\/favicon.webp","title":"Google","description":"Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for.","globalRank":"1","visitors":"256200000","chart":{"visitors":["1710633600%3A253200000","1710720000%3A259900000","1710806400%3A245600000","1710892800%3A267600000","1710979200%3A232400000","1711065600%3A243300000","1711152000%3A237300000","1711238400%3A273400000","1711324800%3A260600000","1711411200%3A272500000","1711497600%3A246700000","1711584000%3A252200000","1711670400%3A227100000","1711756800%3A245400000","1711843200%3A267000000","1711929600%3A248600000","1712016000%3A258700000","1712102400%3A237800000","1712188800%3A261200000","1712275200%3A251200000","1712361600%3A261200000","1712448000%3A260800000","1712534400%3A271600000","1712620800%3A229500000","1712707200%3A265400000","1712793600%3A258400000","1712880000%3A244800000","1712966400%3A236500000","1713052800%3A259500000","1713139200%3A256800000","1713225600%3A234500000","1713312000%3A257400000","1713398400%3A232400000","1713484800%3A267400000","1713571200%3A225300000","1713657600%3A267100000","1713744000%3A257700000","1713830400%3A230500000","1713916800%3A270000000","1714003200%3A243300000","1714089600%3A253100000","1714176000%3A243800000","1714262400%3A271700000","1714348800%3A251300000","1714435200%3A272900000","1714521600%3A246400000","1714608000%3A235100000","1714694400%3A238900000","1714780800%3A225600000","1714867200%3A229900000","1714953600%3A241900000","1715040000%3A244600000","1715126400%3A273500000","1715212800%3A240800000","1715299200%3A235100000","1715385600%3A266400000","1715472000%3A226600000","1715558400%3A260300000","1715644800%3A247000000","1715731200%3A256000000","1715817600%3A232400000","1715904000%3A260300000","1715990400%3A259300000","1716076800%3A242900000","1716163200%3A266800000","1716249600%3A228100000","1716336000%3A244400000","1716422400%3A270300000","1716508800%3A256400000","1716595200%3A273900000","1716681600%3A249500000","1716768000%3A262200000","1716854400%3A251100000","1716940800%3A274300000","1717027200%3A251900000","1717113600%3A235500000","1717200000%3A238600000","1717286400%3A239200000","1717372800%3A250600000","1717459200%3A235900000","1717545600%3A262700000","1717632000%3A253600000","1717718400%3A269500000","1717804800%3A234400000","1717891200%3A238700000","1717977600%3A268600000","1718064000%3A254400000","1718150400%3A233700000","1718236800%3A227100000","1718323200%3A256200000"],"countries":[{"percent":"17.85%","code":"in","name":"India","rank":"1","visitors":45730000},{"percent":"7.59%","code":"us","name":"United States","rank":"1","visitors":19450000},{"percent":"5.53%","code":"ir","name":"Iran","rank":"1","visitors":14170000},{"percent":"4.86%","code":"ca","name":"Canada","rank":"2","visitors":12450000},{"percent":"4.33%","code":"sg","name":"Singapore","rank":"1","visitors":11090000},{"percent":"4.32%","code":"eg","name":"Egypt","rank":"3","visitors":11070000},{"percent":"3.86%","code":"ch","name":"Switzerland","rank":"2","visitors":9890000},{"percent":"3.08%","code":"bd","name":"Bangladesh","rank":"1","visitors":7890000},{"percent":"2.44%","code":"cz","name":"Czech Republic","rank":"","visitors":6250000},{"percent":"2.25%","code":"au","name":"Australia","rank":"1","visitors":5760000},{"name":"(Other)","is_other":true,"code":"","percent":"43.89%","rank":"","visitors":112400000}],"subdomains":[{"subdomain":"www.google.com","percent":"40.87%","visitors":104700000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"},{"subdomain":"mail.google.com","percent":"21.61%","visitors":55360000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"},{"subdomain":"docs.google.com","percent":"11.61%","visitors":29740000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"},{"subdomain":"translate.google.com","percent":"7.23%","visitors":18520000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"},{"subdomain":"search.google.com","percent":"3.16%","visitors":8100000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"},{"subdomain":"accounts.google.com","percent":"2.06%","visitors":5280000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"},{"subdomain":"drive.google.com","percent":"1.74%","visitors":4460000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"},{"subdomain":"analytics.google.com","percent":"1.56%","visitors":4000000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"},{"subdomain":"chromewebstore.google.com","percent":"1.23%","visitors":3150000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"},{"subdomain":"gemini.google.com","percent":"1.22%","visitors":3130000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"},{"subdomain":"(Other)","is_other":true,"percent":"7.71%","visitors":19750000,"favicon_url":"https:\/\/assets.statscrop.com\/favicons.png","favicon_webp_url":"https:\/\/assets.statscrop.com\/favicons.webp"}],"keywords":[{"keyword":"google charger 30w","percent":"4.17%","visitors":87000},{"keyword":"hash symbol","percent":"4.17%","visitors":87000},{"keyword":"\u00c7EV\u0130R\u0130","percent":"3.47%","visitors":72000},{"keyword":"translation","percent":"3.47%","visitors":72000},{"keyword":"ProSkill Services","percent":"2.78%","visitors":58000},{"keyword":"\u7edf\u8ba1\u9e1f","percent":"2.08%","visitors":43000},{"keyword":"translate to Persian","percent":"2.08%","visitors":43000},{"keyword":"zilla font","percent":"2.08%","visitors":43000},{"keyword":"\u5728\u7ebf\u7ffb\u8bd1","percent":"2.08%","visitors":43000},{"keyword":"admob android mediation","percent":"2.08%","visitors":43000},{"keyword":"(Other)","is_other":true,"percent":"71.54%","visitors":1490000}]},"country":{"code":"in","name":"India","rank":"1"}}

Hypestat отдает гораздо более богатые данные. Будем работать с ним. Но, скажу сразу, что не проверял какое расширение показывает стату точнее. Но в любом случае, с heptstat мы получаем значительно больше информации, чем сильно упрощаем себе задачу. SEO-шники от такого пищали бы, но мне больше нравятся свойства host, subdomains и technologies:

JSON:
"subdomains": [
            {
                "subdomains": "google.com",
                "reach_percent": "88.69",
                "page_views_percent": "35.05",
                "page_views_per_user": "4.69"
            },
            {
                "subdomains": "mail.google.com",
                "reach_percent": "19.45",
                "page_views_percent": "27.32",
                "page_views_per_user": "16.67"
            },
            {
                "subdomains": "docs.google.com",
                "reach_percent": "14.23",
                "page_views_percent": "13.16",
                "page_views_per_user": "10.98"
            },
            {
                "subdomains": "accounts.google.com",
                "reach_percent": "13.67",
                "page_views_percent": "5.16",
                "page_views_per_user": "4.48"
            },
            {
                "subdomains": "drive.google.com",
                "reach_percent": "9.24",
                "page_views_percent": "4.41",
                "page_views_per_user": "5.660"
            }
        ],


JSON:
"host": [
            {
                "server_ip": "142.250.191.174",
                "asn": "AS15169",
                "isp": "Google LLC",
                "latitude": "37.751",
                "longitude": "-97.822",
                "city": "Farmingdale",
                "region": "New York",
                "region_code": "NY",
                "postal_code": "11735",
                "country_name": "United States",
                "country_code": "US"
            }
        ],


JSON:
"technologies": {
            "Web Servers": [
                {
                    "st_name": "Google Web Server",
                    "st_icon": "Google.svg"
                }
            ]
        },


Пишем код​


Напомню, что у Google Apps Script есть ограничения на количество исходящих запросов - 20000 в сутки на аккаунт. Не на скрипт или таблицу, а на аккаунт! Поэтому, стоит хорошо подумать, как лучше обогащать данные.

Я делаю почти все внутри Google Sheets. Делаю два триггера: один парсит таргеты, второй проходит и добавляет данные. Можно использовать таблицы, как хранилище. Реализовать получение доменов из таблицы через doGet(), а добавление через doPost(). Реализую это чуть дальше, чтобы переложить часть работы на другие скрипты и экономить запросы гугла.

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

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


  const xss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetResults = xss.getSheetByName(SHEET_RESULTS);
  const lastRow = sheetResults.getLastRow() + 1;


  //...General loop...
}

Дальше надо пройтись циклом, в котором получим все хосты (столбец B) и запросим статистику. Но перед этим слегка изменим старый код, а именно получение данных. Сам процесс получения и парса в JSON выкину в отдельную функцию, превратив getDataFromAPI(dork) в надстройку для функции:

Было

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


Стало:

JavaScript:
function fetchData(url, options={}) {
  const response = UrlFetchApp.fetch(url, options);
  const content = response.getContentText();
  console.log('Response:');
  console.log(content);
  const json = JSON.parse(content);
  return json;
}


function getDataFromAPI(dork) {
  const url = `${API_URL}?user=${API_USER}&key=${API_KEY}&groupby=100&domain=37&device=desktop&hl=en&lr=2840&filter=1&query=${encodeURIComponent(dork)}`;
  console.log('Start fetching by url: ', url);
  return fetchData(url);
}


function getHypeStatData(domain) {
  const url = `${API_GET_HYPESTAT}${domain}`;
  const options = {
      muteHttpExceptions: true
  }
  console.log(url)
  return fetchData(url, options);
}

Как видно из нового кода, добавил простую функцию-надстройку для получения данных с hypestat. muteHttpExceptions: true позволит нам не прерывать выполнение кода, если прилетел корявый ответ от сервера. В ином случае, будем получать исключение.

Дописываю основной цикл. В коде нашей функции, getStatistics() заменяю “//...General loop...” на:

JavaScript:
for(let i = domainRow; i < lastRow; i++) {
    const domain = sheetResults.getRange(i, 1).getValue();
    const data = getHypeStatData(domain);


    if (!data.success) {
      console.log('Error request: ' + domain);
      //Останавливаем выполнение, если ошибка.
      return;
    }


    saveHypeStatToSheet(sheetResults, i, data);


    ScriptProperties.setProperty('domainRow', i);
  }

Что уже сделано?
  1. Сделал проверку на время выполнения скрипта (максимум 360 секунды)
  2. Получил домен с листа результатов
  3. Сделал запрос к HypeStat
  4. Сохранил данные через вспомогательную функцию saveHypeStatToSheet()
  5. Обновил значение последней обработанной строки

Займусь функцией saveHypeStatToSheet(). Есть несколько принципиальных моментов, которые нужно для себя решить. Во-первых, какие данные собирать. Во-вторых, как их хранить. Как видно из JSON, который возвращает API, все свойства представляют собой массивы. Причем, многая информация не имеет какого-то смыслового значения для атаки.

Я решил выделить ряд значений в отдельные ячейки, чтобы удобно было искать, сортировать, фильтровать. Все остальное тупо целиком сохранять в ячейки:
  1. Количество визитов в день traffic[0].daily_visits
  2. Объем платного трафика traffic_sources[0].daily_visits
  3. Количество мобильного трафика desktop_vs_mobile[0].mobile
  4. Первую страну из визитов по стране, чтобы понимать откуда больше трафика идет. visitors_by_country[0],country и visitors_by_country[0],visits_percent
  5. В отдельные ячейки сохранять целиком: subdomains, host, technologies, whois

В итоге, функция сохранения выглядит следующим образом:

JavaScript:
function saveHypeStatToSheet(sheet, row, {data}) {
  const {traffic, traffic_sources, desktop_vs_mobile,
        visitors_by_country, subdomains, host,
        technologies, whois} = data;


  sheet.getRange(row, 8, 1, 9).setValues([[
    traffic[0]?.daily_visits,
    traffic_sources[0]?.paid,
    desktop_vs_mobile[0]?.mobile,
    visitors_by_country[0]?.country,
    visitors_by_country[0]?.visits_percent,
    subdomains,
    host,
    technologies,
    whois
  ]]);
}

Обращаю внимание людей не знакомых с синтаксисом ES6 JS, что при получении параметров, дата получается через деструктуризацию. Если посмотреть на объект, который отдает HypeStat, одно из свойств “data’, оно нам и нужно.

Делаю тестовый запуск и получаю ошибку:

AD_4nXcm6QZ8hR7OF_LgOYi34Oeowa4Cu2IdaeU9s-_OImkUTLJm1iDOO9MIec6e-aiBomvV8xdS8tjvAUUimB5PkX4F2DbK0O3ZeewB0P59BFoqapmvAY62NCYY9NTJCFziuVV97dCcU_vbI8YsAz2ph06ktdV-

Отлично, есть повод добавить обработку подобной ошибки. Жаль не возвращает код ошибки, придется весь текст запихать в константу. Причем, учитывая наличие домена в ответе, проще искать текст ошибки, а не сравнивать. Текст ошибки добавляю в константы.

Пока вешаю заглушку, позже добавим поиск информации через API StatsCorp, мы же делаем нормальное стабильное решение с максимум данных.
JavaScript:
if (!data.success) {
      console.log('Error request: ' + domain);
      if (data.errors.reason.includes(ERROR_HYPSTAT_NONE)) {
        sheetResults.getRange(i, 8).setValue('NOT HAVE DATA')
        continue
      } else {
        //Останавливаем выполнение, если неизвестная ошибка.
        return;
      }
    }

AD_4nXfVIF1JEpclFfX1k5l0fQ77Ws4v3hMKNEMBQMoex7QGmYp6dhzcklfR6dFYj6a38jYk5rNAy2TmMHMQohclSIgtpT_2k-9kM3JWDdKUvjDmusX9hBymjrQHhQY4mPPNoqlR_gDflPZYHzuf3ifl4Lurymre


Как видно, все прекрасно работает. Да, к сожалению, множества данных нет. Но что поделать. Нет универсального решения, которое позволило бы получать все необходимые данные по любому сайту в один клик. В целом, и так получено не мало. Но самое время добавить функционал дополнения данными из StatsCorp.
JavaScript:
function getStatsCorpData(domain) {
  const url = `${API_GET_CORPSTAT}${domain}`;
  return fetchData(url);
}


function saveStatsCorpData(sheet, row, data) {
  const {visitors, countries, subdomains} = data;
  sheet.getRange(row,8,6).setValues([[
    visitors,
    '',
    '',
    countries[0].name,
    countries[0].percent.replace('%',''),
    subdomains
  ]])
}

По сути, тоже самое, что и с HypeStat, только объем возвращаемых данных гораздо меньше и отсутствует muteHttpExceptions: true.

Правильно будет выделить код, который отвечает за статистику в отдельный файл. Добавил фйал stats.gs и перетащил в него функции связанные со статистикой:

  1. getStatistics()
  2. getHypeStatData()
  3. saveHypeStatToSheet()
  4. getStatsCorpData()
  5. saveStatsCorpData()

Заодно, перекладываю doGet() и fetchData() в файл http (теперь у нас порядок и структура) и добавлю в проверку времени выполнения в getStatistics():
JavaScript:
let currentTime = new Date().getTime();
    let seconds = (currentTime - startTime) / 1000;


    if (seconds > MAX_TIME_STATS) {
      console.log('Time end');
      return;
    }
Для этой функции таймер поставил побольше, так как она работает в разы быстрее и скрипт не прервется на середине выполнения, в отличии от скрипта парсинга целей.

AD_4nXelmsB9ITpZmuMEGKZ7b8HF-FUHJDeHGNT5iCMd3sfqnsZHJB_dVJGYDzw5mMquI9O1p_VU8X2edbIKumlzhMfFUyT0AU6oUNdkgCTM8aHSLAggESzklKYLmQVAAvEQIQYOJc54khufic4d307-k2antr0


Послесловие​

На мой взгляд, тема стоит внимания.Сама по себе статистика посещаемости крайне важна, позволяет не распылять свои ресурсы. А уж паразитирование на чужих сервисах еще и бесплатно… ну вы поняли)))

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

В конце хочу напомнить еще раз про квоту Google - 20 000 запросов с 1 аккаунта в сутки. К сожалению, это сильно связывает руки и не дает возможности переложить все на ресурсы таблиц. Но есть элегантный выход из ситуации — обогащать данные снаружи, например, скриптом на Python и передавать их в таблицу, используя стандартный триггер doPost(). Приведу простой пример этой функции, который будет записывать данные из POST-запроса в таблицу на лист log:
JavaScript:
function doPost(e) {
  /**
   * Добавьте лист с именем "log" для тестирования
   * После добавления функции не забудьте сделать Deploy веб-приложения
   * Когда все готово, отправьте POST запрос на url, который выдал Google
   * после Deploy. Данные отправленные в запросе будут лежать в
   * e.postData.contents
   *
   * В ответ, скрипт отправит весь объек e
  */
  const xss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetLog = xss.getSheetByName('log')
  sheetLog.getRange(1,1).setValue(e.postData.contents)
  return ContentService.createTextOutput(JSON.stringify(e))
}

Опираясь на этот пример и статью, вам не составит труда разобраться, как передавать данные в обе стороны. Подробнее про деплой в первой части: https://xss.is/threads/116628/

Итоговые файлы проекта:

JavaScript:
let startTime = new Date().getTime();
function getDataFromXMLAPI(dork) {
  const url = `${API_URL}?user=${API_USER}&key=${API_KEY}&groupby=100&domain=37&device=desktop&hl=en&lr=2840&filter=1&query=${encodeURIComponent(dork)}`;
  console.log('Start fetching by url: ', url);
  return fetchData(url);
}
function getClearURLData(url) {
  const [protocol, tail] = url.split(':');
  const host = tail.replace('//','').split('/')[0];
  return {
    protocol, host, domain: protocol.concat('://', host)
  }
}
function parseJSONToSheet_(json, sheet, dork) {
  const {results} = json;
  for(let i = json.first; i <= json.last; i++) {
    const clearURLData = getClearURLData(results[i].url)
    console.log('Append data: ', [results[i].url, results[i].title, results[i].passage, ,dork])
    sheet.appendRow([clearURLData.host, clearURLData.domain, results[i].url, results[i].title, results[i].passage, ,dork]);
  }
}
function startParsing(currentDork) {
  const xss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetDorks = xss.getSheetByName(SHEET_DORKS);
  const sheetResults = xss.getSheetByName(SHEET_RESULTS);
  const lastDork = sheetDorks.getLastRow() + 1;
  for(let i = currentDork; i < lastDork; i++) {
    let currentTime = new Date().getTime();
    let seconds = (currentTime - startTime) / 1000;
 
    if (seconds > MAX_TIME_SEC) {
      console.log('Time end');
      return;
    }
    const dorkValue = sheetDorks.getRange(i, 1).getValue();
    const result = getDataFromXMLAPI(dorkValue);
    parseJSONToSheet_(result , sheetResults, dorkValue);
    currentDork++;
    ScriptProperties.setProperty('currentDork', currentDork);
  }
}

JavaScript:
const API_URL = `https://xmlstock.com/google/json/`;
const API_KEY = `YOUR_API_KEY`;
const API_USER = 00000;
const API_REGION = 2840;
const SHEET_DORKS = `dorks`;
const SHEET_RESULTS = `results`;
const SHEET_ADMIN_PANELS = `panels`;
const MAX_TIME_SEC = 280;
const MAX_TIME_STATS = 330;
const API_GET_HYPESTAT = 'https://hypestat.com/api/E9asC1KOutzPrlALHn34X/';
const API_GET_CORPSTAT = 'https://extensions.statscrop.com/api/v1/data/?domain=';
const ERROR_HYPESTAT_NONE = 'is not found in HypeStat database. It will be analyzed as soon as possible.';

JavaScript:
function doGet(e) {
  let {offset,count} = e.parameters;
  const xss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetResults = xss.getSheetByName(SHEET_RESULTS);
  if (!offset || offset < 1) offset = 1
  if (!count || count > 1000) count = 1000;
  if (offset >= sheetResults.getLastRow()) {
    return ContentService.createTextOutput({success:true, count: 0, results:[]}).setMimeType(ContentService.MimeType.JSON)
  }
 
  const results = sheetResults.getRange(offset, 1, count).getValues().map(el => el[0]).filter(Boolean)
  return ContentService.createTextOutput(JSON.stringify({success:true, count: results.length, results})).setMimeType(ContentService.MimeType.TEXT);
}
function resetDorkRow() {
  ScriptProperties.setProperty('currentDork', 1);
}
function startParsing() {
  let currentDork = parseInt(ScriptProperties.getProperty('currentDork'));
  if (!currentDork) {
    currentDork = 1;
    ScriptProperties.setProperty('currentDork', currentDork);
  }
  startParsing(currentDork);
}
function doPost(e) {
  /**
   * Добавьте лист с именем "log" для тестирования
   * После добавления функции не забудьте сделать Deploy веб-приложения
   * Когда все готово, отправьте POST запрос на url, который выдал Google
   * после Deploy. Данные отправленные в запросе будут лежать в
   * e.postData.contents
   *
   * В ответ, скрипт отправит весь объек e
  */
  const xss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetLog = xss.getSheetByName('log')
  sheetLog.getRange(1,1).setValue(e.postData.contents)
  return ContentService.createTextOutput(JSON.stringify(e))
}
function fetchData(url, options={}) {
  const response = UrlFetchApp.fetch(url, options);
  const content = response.getContentText();
  console.log('Response:');
  console.log(content);
  const json = JSON.parse(content);
  return json;
}

JavaScript:
function getHypeStatData(domain) {
  const url = `${API_GET_HYPESTAT}${domain}`;
  const options = {
      muteHttpExceptions: true
  }
  console.log(url)
  return fetchData(url, options);
}
function saveHypeStatToSheet(sheet, row, {data}) {
  const {traffic, traffic_sources, desktop_vs_mobile,
        visitors_by_country, subdomains, host,
        technologies, whois} = data;
  sheet.getRange(row, 8, 1, 9).setValues([[
    traffic[0]?.daily_visits,
    traffic_sources[0]?.paid,
    desktop_vs_mobile[0]?.mobile,
    visitors_by_country[0]?.country,
    visitors_by_country[0]?.visits_percent,
    subdomains,
    host,
    technologies,
    whois
  ]]);
  checkAndSelectAdminSub(sheet, row, subdomains);
}
function getStatsCorpData(domain) {
  const url = `${API_GET_CORPSTAT}${domain}`;
  return fetchData(url);
}
function saveStatsCorpData(sheet, row, data) {
  const {visitors, countries, subdomains} = data;
  sheet.getRange(row,8,6).setValues([[
    visitors,
    '',
    '',
    countries[0].name,
    countries[0].percent.replace('%',''),
    subdomains
  ]])
 
}
function getStatistics() {
  let domainRow = parseInt(ScriptProperties.getProperty('domainRow'));
  if (!domainRow) {
    domainRow = 1;
    ScriptProperties.setProperty('domainRow', domainRow);
  }
  console.log('Initialized rows;' + domainRow)
  const xss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetResults = xss.getSheetByName(SHEET_RESULTS);
  const lastRow = sheetResults.getLastRow() + 1;
  console.log('Last row: ' + lastRow)
  for(let i = domainRow; i < 33 + 1; i++) {
    let currentTime = new Date().getTime();
    let seconds = (currentTime - startTime) / 1000;
    if (seconds > MAX_TIME_STATS) {
      console.log('Time end');
      return;
    }
    const domain = sheetResults.getRange(i, 1).getValue();
    console.log(domain)
    let data = getHypeStatData(domain);
    console.log(data)
    if (!data.success) {
      console.log('Error request: ' + domain);
      if (data.errors.reason.includes(ERROR_HYPESTAT_NONE)) {
        try{
          data = getStatsCorpData(domain);
          saveStatsCorpData(sheetResults, i, data);
        }catch(e) {
          sheetResults.getRange(i, 8).setValue('NOT HAVE DATA')
          continue
        }
      } else {
        //Останавливаем выполнение, если неизвестная ошибка.
        return;
      }
    }
    console.log('Start save to sheet')
    saveHypeStatToSheet(sheetResults, i, data);
    console.log('Saved')
    ScriptProperties.setProperty('domainRow', i);
  }
}

by: petrinh1988
 

Members, viewing this thread

Сейчас на форуме нет ни одного пользователя.