Skip to content

Instantly share code, notes, and snippets.

@alexchexes
Last active March 4, 2024 17:47
Show Gist options
  • Save alexchexes/09c81155b6dec118714825bddd814fe9 to your computer and use it in GitHub Desktop.
Save alexchexes/09c81155b6dec118714825bddd814fe9 to your computer and use it in GitHub Desktop.
Google SERP Analysis Extension (tampermonkey / greasemonkey)
// ==UserScript==
// @name Google SERP Analysis Extension
// @namespace http://tampermonkey.net/
// @version 2024-03-02
// @author https://gist.github.com/alexchexes
// @homepageURL https://gist.github.com/alexchexes/09c81155b6dec118714825bddd814fe9
// @updateURL https://gist.githubusercontent.com/alexchexes/09c81155b6dec118714825bddd814fe9/raw/google_serp_analysis.user.js
// @downloadURL https://gist.githubusercontent.com/alexchexes/09c81155b6dec118714825bddd814fe9/raw/google_serp_analysis.user.js
// @description try to take over the world!
// @match https://www.google.com/search?*
// @icon https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant none
// @require https://code.jquery.com/jquery-latest.min.js
// ==/UserScript==
/* global $ */
(function() {
$(function() {
let is_mobile = true;
let $table;
// let curr_pos = 0;
let curr_org_pos = 0;
// let curr_adv_pos = 0;
let search_query;
let search_term_in_query;
let detected_city;
let geolocation;
let search_query_city;
const TITLE_SEL_MOB = 'a > div > div[role="heading"]';
const TITLE_SEL_PC = 'a > h3';
if (!$(TITLE_SEL_MOB).length) {
is_mobile = false;
}
function parseResults() {
let $all_headings = $(TITLE_SEL_MOB);
if (!$all_headings.length) {
is_mobile = false;
$all_headings = $(TITLE_SEL_PC);
}
getSearchQuery();
getDetectedCity();
getGeolocation();
getCityFromQuery();
renderTable();
// на моб сниппет с таблицей или списком имеет другую разметку.
// Опираемся на то, что такой сниппет это всегда ТОП-1, и добавляем его в таблицу первым
if (is_mobile) {
const $mobile_top_snip_A = $('h2').next().find('h3 a');
const $mobile_top_snippet = $mobile_top_snip_A
.parent().parent().parent().parent().parent().parent().parent().parent().parent().parent().parent();
if ($mobile_top_snippet.length) {
console.log('$mobile_top_snippets (ALL MATCHING): ', $mobile_top_snippet);
const title = $mobile_top_snip_A.text();
const url = $mobile_top_snip_A.attr('href');
addToTable($mobile_top_snippet, title, url);
}
}
$all_headings.each(function() {
console.log('heading elem: ', $(this));
const $serp_item = $(this).parent().parent().parent().parent().parent().parent().parent().parent();
let title;
let url;
if (is_mobile) {
title = $(this).text();
url = $(this).parents('a').attr('href');
} else {
title = $(this).text();
url = $(this).parent('a').attr('href');
}
if (url === undefined || url.indexOf('/search?') === 0) {
return true;
}
// skip results from "People also ask" blocks
if($serp_item.parents('[data-initq]').length) {
return true;
}
addToTable($serp_item, title, url);
});
}
if (is_mobile) {
// костыль т.к. гугл в зависимости от аккаунта/браузера подгружает только 4 результата на мобильном, и надо скроллить
$('html, body').animate({
scrollTop: 2000, // куда скроллим
}, 1); // Длительность эффекта прокрутки в миллисекундах
setTimeout(function(){
$('html, body').animate({
scrollTop: 0, // куда скроллим
}, 1); // Длительность эффекта прокрутки в миллисекундах
parseResults();
}, 1000);
} else {
parseResults();
}
function getSearchQuery() {
let $input;
if (is_mobile) {
$input = $('textarea[enterkeyhint="search"]');
} else {
$input = $('textarea[role="combobox"]');
}
search_query = $input.val();
}
function getDetectedCity() {
detected_city = $('#taw omnient-visibility-control > div > div > div > span:nth-child(2) > span').text().trim();
}
function getGeolocation() {
geolocation = $('.known_loc').next().text().trim();
if (!geolocation) {
geolocation = $('.unknown_loc').next().text().trim();
}
if (geolocation === 'Unknown') {
geolocation = '';
}
}
function addToTable($serp_item, title, url) {
console.log($serp_item.eq(0));
curr_org_pos++;
const domain = getDomainFromUrl(url);
const tld = getTopLevelDomainFromUrl(url);
let snippet_features = [];
if ($serp_item.find('table').length) {
snippet_features.push('ТОП-Таблица');
}
if ($serp_item.find('ul, ol').length) {
snippet_features.push('ТОП-Список');
}
if ($serp_item.find('[style*="width:calc"]').length) {
snippet_features.push('Звёзды рейтинга');
}
const $possibly_price_elems = $serp_item.find(
'span:contains("₽"), span:contains("£"), span:contains("$"), span:contains("€"), span:contains("RUB")'
);
$possibly_price_elems.each(function(){
console.log('$possibly_price_elems', $possibly_price_elems.text(), $possibly_price_elems);
let $text = $(this).text().trim().replaceAll(' ', ' ');
if ($text.match(/^(([A-Z]{3}|\D)\s?\d+([,\. ]\d+)*|\d+([,\. ]\d+)*\s\D)$/)
|| $text.match(/^(от|до)\s\d+([\., ]\d+)*\s./i)) {
snippet_features.push('Цены');
return false;
}
});
if ($serp_item.find('img').length > 1) {
snippet_features.push('Результат с картинкой');
}
if ($serp_item.find('[data-attrid="wa:/description"]').length) {
snippet_features.push('Выделенное описание');
}
const snippet_features_str = snippet_features.join(', ');
let has_navigation = '';
if ($serp_item.find('a[href]:not([href*="translate.google"])').length > 2) {
has_navigation = 'Быстрые ссылки';
}
const row = `
<tr>
<td>${curr_org_pos}</td>
<td></td>
<td></td>
<td>${tld || ''}</td>
<td>${(domain !== tld) ? domain : ''}</td>
<td>${title || ''}</td>
<td>${url || ''}</td>
<td>${search_query}</td>
<td>${search_term_in_query}</td>
<td>${search_query_city}</td>
<td>${detected_city}</td>
<td>${geolocation}</td>
<td>${snippet_features_str}</td>
<td></td>
<td>${has_navigation}</td>
<td></td>
<td>${is_mobile ? 'моб.' : 'ПК'}</td>
<td>${getMoscowDateTime()}</td>
<td>Google</td>
</tr>
`;
$table.append(row);
}
function getAdvUrl($serp_item) {
const json_str = $serp_item.find('.organic__greenurl').attr('data-bem');
const json_parsed = json_str ? JSON.parse(json_str) : '';
return json_parsed?.click?.arguments?.url || '';
}
function getDomainFromUrl(url) {
try {
const parsedUrl = new URL(url);
let hostname = parsedUrl.hostname.toLowerCase();
// Remove 'www.' prefix if present
if (hostname.startsWith("www.")) {
hostname = hostname.substring(4).toLowerCase();
}
return hostname;
} catch (error) {
console.error("Invalid URL:", error);
return null;
}
}
function getTopLevelDomainFromUrl(url) {
return url.replace(/.+?([\w\-]+\.[\w\-]+?)[\/\?].*/gi, '$1').toLowerCase();
}
function renderTable() {
const $overlay = $(`<div class="__us_overlay"> </div>`);
$table = $(`<table> <tbody></tbody> </table>`);
const $copy_btn = $(`<div class="to__clipboard">Скопировать</div>`);
const css = `
<style>
.__us_overlay {
background: #ffffffb8;
border-radius: 5px;
border: 1px solid black;
color: #000;
font-family: arial;
font-size: 12px;
margin: 8px 0;
overflow-x: auto;
padding: 5px 10px;
position: relative;
}
.__us_overlay table td {
max-width: 12vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.__us_overlay table td:empty:after {
color: #b3b3b3;
content: "—";
}
.to__clipboard {
color: crimson;
cursor: pointer;
}
@media only screen and (max-width: 767px) {
.__us_overlay table td {
/* max-width: 12vw; */
}
}
</style>
`;
$overlay.append($copy_btn);
$overlay.append($table);
$('body').prepend(css);
if(is_mobile) {
$('#main').prepend($overlay);
} else {
$overlay.css('margin-top', '70px');
$('#main > div').prepend($overlay);
}
$copy_btn.click(function(){
let $btn = $(this);
copyWithStyle($table.get(0));
let btn_text = $btn.html();
$btn.html('✔');
setTimeout(function(){
$btn.html(btn_text);
}, 1500);
});
}
function selectElement(elem) {
if(document.body.createTextRange) {
let range = document.body.createTextRange();
range.moveToElement(elem);
range.select();
} else if (window.getSelection) {
let selection = window.getSelection();
let range = document.createRange();
range.selectNodeContents(elem);
selection.removeAllRanges();
selection.addRange(range);
}
}
function copyWithStyle(elem) {
selectElement(elem);
document.execCommand('copy');
window.getSelection().removeAllRanges();
}
function unifyString(str) {
return str.replaceAll(/[^\wа-яёa-z]/gi, '').toLowerCase();
}
function getCityFromQuery() {
const cities = [
'Балашиха',
'Барнаул',
'Белгород',
'Владимир',
'Волгоград',
'Воронеж',
'Гатчина',
'Екатеринбург',
'Ижевск',
'Иркутск',
'Казань',
'Королёв',
'Краснодар',
'Красноярск',
'Курск',
'Липецк',
'Люберцы',
'Москва',
'Мытищи',
'Нижний Новгород',
'Новая усмань',
'Новосибирск',
'Омск',
'Пермь',
'Подольск',
'Ростов-на-Дону',
'Рязань',
'Самара',
'Санкт-Петербург',
'Саратов',
'Севастополь',
'Симферополь',
'Сочи',
'Тверь',
'Тольятти',
'Тюмень',
'Уфа',
'Химки',
'Чебоксары',
'Челябинск',
'Ярославль',
// @todo Добавить больше городов
// @todo подумать над склонением многосложных типа "ростовА на дону"
];
cities.sort((a, b) => a.length - b.length);
const unified_search_query =
search_query
.trim()
.toLowerCase()
.replace(/([а-яё]{3,})а /gi, '$1 ') // кейс "ростова на дону"
.replaceAll(/[^\wа-яёa-z]/gi, '')
.replace(/ё/gi, 'е')
.replace(/[ия]$/gi, ''); // тюменИ, ярославлЯ
console.log(unified_search_query)
for (let i = 0; i < cities.length; i++) {
const unified_city =
cities[i]
.toLowerCase()
.replaceAll(/[^\wа-яёa-z]/gi, '')
.replace(/ё/gi, 'е')
.replace(/ь$/gi, '') // тюменЬ
.replace(/[ия]$/gi, ''); // т.к. выше убираем для тюменИ, ярославлЯ - чтобы не поломать химкИ, тольяттИ
if (unified_search_query.includes(unified_city)) {
search_query_city = cities[i];
break;
}
}
search_query_city = search_query_city || '';
search_term_in_query = search_query_city ? search_query.replace(search_query_city, '').trim() : '';
}
/**
* Gets the current date and time in Moscow time zone formatted as dd.mm.yyyy hh:mm.
* This function is robust and designed for production use, ensuring that it always
* returns the date and time in the specified format for the Moscow time zone,
* regardless of the user's local settings.
*
* @returns {string} The current date and time in Moscow in the format "dd.mm.yyyy hh:mm".
*/
function getMoscowDateTime() {
try {
// Initialize the current date and time
const now = new Date();
// Define formatting options for Moscow time zone
const options = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
timeZone: 'Europe/Moscow',
hour12: false
};
// Initialize the Intl.DateTimeFormat with Russian locale and the defined options
const formatter = new Intl.DateTimeFormat('ru-RU', options);
// Format the current date and time according to Moscow time zone
let moscowTime = formatter.format(now);
// Adjust the format to dd.mm.yyyy hh:mm
let formattedMoscowTime = moscowTime.replace(/(\d{2})\.(\d{2})\.(\d{4}), (\d{2}):(\d{2})/, '$1.$2.$3 $4:$5');
return formattedMoscowTime;
} catch (error) {
// Log the error and potentially notify a monitoring service
console.error('Failed to get Moscow time:', error);
// Depending on the use case, you might want to rethrow the error, return null, or provide a default response
throw error; // or return a default value like 'Error fetching time'
}
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment