Last active
March 4, 2024 17:48
-
-
Save alexchexes/ca2cad7ffc4c0a089294b045783dd5f9 to your computer and use it in GitHub Desktop.
Yandex SERP analysis extension (tampermonkey / greasemonkey)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Yandex SERP Analysis Extension | |
// @namespace http://tampermonkey.net/ | |
// @version 2024-03-02.a | |
// @author https://gist.github.com/alexchexes | |
// @homepageURL https://gist.github.com/alexchexes/ca2cad7ffc4c0a089294b045783dd5f9 | |
// @updateURL https://gist.githubusercontent.com/alexchexes/ca2cad7ffc4c0a089294b045783dd5f9/raw/yandex_serp_analysis.user.js | |
// @downloadURL https://gist.githubusercontent.com/alexchexes/ca2cad7ffc4c0a089294b045783dd5f9/raw/yandex_serp_analysis.user.js | |
// @description try to take over the world! | |
// @match https://ya.ru/search* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=ya.ru | |
// @grant none | |
// @require https://code.jquery.com/jquery-latest.min.js | |
// ==/UserScript== | |
/* global $ */ | |
(function() { | |
$(function() { | |
let is_mobile = false; | |
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 geolocation; | |
let search_query_city; | |
function parseResults() { | |
const $all_items = $('.serp-item[data-cid], .serp-item.t-construct-adapter__suggest-fact'); | |
getSearchQuery(); | |
getSearchCity(); | |
getCityFromQuery(); | |
renderTable(); | |
$all_items.each(function() { | |
addToTable($(this)); | |
}); | |
} | |
setTimeout(function(){ | |
parseResults(); | |
}, 500); | |
function getSearchQuery() { | |
let $input = $('[accesskey="s"]'); | |
if (!$input.length) { | |
$input = $('.mini-suggest__input'); | |
is_mobile = true; | |
} | |
search_query = $input.val(); | |
} | |
function getSearchCity() { | |
geolocation = $('.SerpFooter-LinksGroup_type_geo').text().trim(); | |
if (!geolocation) { | |
geolocation = $('div.region-change').text().trim(); | |
} | |
} | |
function isAdv($serp_item) { | |
let $sub_label = $serp_item.find('.organic__subtitle').children(':not(style)'); | |
let $small_label = $sub_label.eq(1); | |
if ($small_label.length && $small_label.text().toLowerCase().includes('вчера') ) { | |
return false; | |
} | |
if ($sub_label.length > 1 && !$small_label.children().length) { | |
$serp_item.css('background', '#dfdfdf'); | |
$sub_label.eq(1).css('background', '#ff0'); | |
return true; | |
} | |
return false; | |
} | |
function isOrganicResult($serp_item) { | |
// "колдунщики" | |
if ($serp_item.attr('data-fast-subtype') !== undefined) { | |
return false; | |
} | |
// "быстрые ответы... (и список вопросов)" | |
if ($serp_item.attr('data-fast-name') === 'related_discovery') { | |
return false; | |
} | |
return true; | |
} | |
function addToTable($serp_item) { | |
console.log($serp_item); | |
curr_pos++; | |
const is_adv = isAdv($serp_item); | |
const is_organic = (!is_adv) && isOrganicResult($serp_item); | |
if (is_organic) { | |
curr_org_pos++; | |
} | |
const title = getItemTitle($serp_item); | |
const url = is_adv ? getAdvUrl($serp_item) : getSerpItemUrl($serp_item); | |
let domain; | |
let tld; | |
if (url) { | |
domain = getDomainFromUrl(url); | |
tld = getTopLevelDomainFromUrl(url); | |
} else { | |
domain = getAdvDomain($serp_item); | |
tld = extractTopLevelDomain(domain); | |
} | |
const data_fast_subtype = $serp_item.attr('data-fast-subtype') || ''; | |
let data_fast_name = $serp_item.attr('data-fast-name') || ''; | |
if($serp_item.find('.entity-search__header_type_large').length) { | |
data_fast_name += ' Большая карточка'; | |
data_fast_name = data_fast_name.trim(); | |
} | |
if ($serp_item.is('.t-construct-adapter__suggest-fact')) { | |
data_fast_name += ' Быстрый факт'; | |
data_fast_name = data_fast_name.trim(); | |
} | |
if (is_adv) { | |
curr_adv_pos++; | |
} | |
let has_navigation = false; | |
if (!is_adv && $serp_item.find('.sitelinks__item').length > 1) { | |
has_navigation = true; | |
} | |
const row = ` | |
<tr> | |
<td>${is_organic ? curr_org_pos : ''}</td> | |
<td>${is_adv ? curr_adv_pos : ''}</td> | |
<td>${curr_pos}</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></td> | |
<td>${geolocation}</td> | |
<td>${data_fast_subtype}</td> | |
<td>${getFastNameRus(data_fast_name)}</td> | |
<td>${has_navigation ? 'Навигация' : ''}</td> | |
<td>${is_adv ? 'Реклама' : ''}</td> | |
<td>${is_mobile ? 'моб.' : 'ПК'}</td> | |
<td>${getMoscowDateTime()}</td> | |
<td>Yandex</td> | |
</tr> | |
`; | |
$table.append(row); | |
} | |
function getFastNameRus(data_fast_name) { | |
const known_names = { | |
'fact_instruction': 'Инструкция', | |
'gen_answer': 'Сгенерированный ответ', | |
'suggest_fact': 'Быстрый факт', | |
}; | |
if (typeof known_names[data_fast_name] !== 'undefined') { | |
return known_names[data_fast_name]; | |
} else { | |
return data_fast_name || ''; | |
} | |
} | |
function getItemTitle($serp_item) { | |
let title = $serp_item.find('h2').text() || ''; | |
if (!title) { | |
title = $serp_item.find('[class*="Title"]').text() || ''; | |
} | |
if (!title) { | |
title = $serp_item.find('.fact__title a').text() || ''; | |
} | |
return title; | |
} | |
function getSerpItemUrl($serp_item) { | |
let url = $serp_item.find('.organic__title-wrapper > a').attr('href') || ''; | |
if (!url) { | |
url = $serp_item.find('a').attr('href') || ''; | |
} | |
return url; | |
} | |
function getAdvUrl($serp_item) { | |
const json_str = $serp_item.find('.OrganicTitle-Link').attr('data-bem'); | |
const json_parsed = json_str ? JSON.parse(json_str) : ''; | |
let url = json_parsed?.click?.arguments?.url || ''; | |
return url; | |
} | |
function getAdvDomain($serp_item) { | |
let text = $serp_item.find('.organic__path').text()?.trim(); | |
return text.replaceAll(/([\wа-яё\-\.]+\.[\wа-яё\-]+\b).+/gi, '$1'); | |
} | |
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 extractTopLevelDomain(url) { | |
// This regex matches the last two sections of the domain name | |
// It accounts for any characters in Unicode letter categories, including Cyrillic | |
const domainPattern = /([^.]+\.[^.]+)$/; | |
// Extract hostname in case the URL is provided | |
// This is to ensure we get just the domain and subdomain part | |
let hostname; | |
try { | |
hostname = (new URL(url.startsWith('http://') || url.startsWith('https://') ? url : `http://${url}`)).hostname; | |
} catch (error) { | |
console.error("Invalid URL:", error); | |
hostname = url; | |
} | |
// Find and return the domain and top-level domain | |
const match = hostname.match(domainPattern); | |
return match ? match[0] : url; | |
} | |
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 ($('main').length) { | |
$('main').prepend($overlay); | |
} else { | |
$('.serp-list').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