Skip to content

Instantly share code, notes, and snippets.

@alexchexes
Last active March 4, 2024 17:48
Show Gist options
  • Save alexchexes/ca2cad7ffc4c0a089294b045783dd5f9 to your computer and use it in GitHub Desktop.
Save alexchexes/ca2cad7ffc4c0a089294b045783dd5f9 to your computer and use it in GitHub Desktop.
Yandex SERP analysis extension (tampermonkey / greasemonkey)
// ==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