Skip to content

Instantly share code, notes, and snippets.

@gro-ove
Created November 9, 2020 09:39
Show Gist options
  • Save gro-ove/b5abcc7e7d6f4c8693bad6667ca64e26 to your computer and use it in GitHub Desktop.
Save gro-ove/b5abcc7e7d6f4c8693bad6667ca64e26 to your computer and use it in GitHub Desktop.
Если запустить на странице с отзывами на товар на Яндекс.Маркете, соберёт остальные отзывы комментаторов и поищет интересные совпадения, когда разные люди оставляют одинаковые оценки на одинаковые товары или магазины.
javascript:(function () {
function wait(ms = null) {
return new Promise(r => setTimeout(r, ms == null ? (500 + Math.random() * 1e3) : ms));
}
async function clickWait(parent, query) {
const e = parent.querySelector(query);
if (!e) return false;
e.click();
await wait();
return true;
}
function collectPageReviewersFromElement(element) {
return [].map.call(element.querySelectorAll('[data-zone-name="product-review"]'), x => ({
name: (x.querySelector('a[href^="/user/"][href$="/reviews"]') || {}).textContent,
url: (x.querySelector('a[href^="/user/"][href$="/reviews"]') || {}).href,
rating: +x.querySelector('[data-rate]').getAttribute('data-rate')
})).filter(x => x.url).reduce((a, b) => ((a[b.url] = b), a), {});
}
function collectOtherReviewsFromElement(element) {
return [].map.call(element.querySelectorAll('[href^="/shop--"], [href^="/product--"]'), x => ({
name: x.textContent,
url: x.href,
rating: x.parentNode.parentNode.parentNode.querySelector('[data-rate]'),
shop: /\/shop--/.test(x.href)
})).filter(x => x.rating).map(x => ((x.rating = +x.rating.getAttribute('data-rate')), x));
}
const style = `#_scanPopup{ position:fixed;z-index:99999; top:0;left:0;bottom:0;right:0; background:rgba(0,0,0,0.5); }
#_scanPopup>div{ position:absolute; top:5vh;left:10vw;bottom:5vh;right:10vw; background:#fff; border-radius:4px;box-shadow:0 4px 10px rgba(0,0,0,0.5); padding:20px }
#_scanPopup>div>h1{margin-bottom:20px}
.loading:before{content:"";display:block;position:absolute;left:50%;top:50%;font-size:10px;text-indent:-9999em}.loader-animation-8 .loading:before,.loading:before{width:8em;height:8em;margin:-5em 0 0 -5em;border:1em solid rgba(0,0,0,.2);border-left:1em solid #000;-webkit-border-radius:50%;border-radius:50%;background:0 0;-webkit-animation:loader-animation-8 1.1s infinite linear;animation:loader-animation-8 1.1s infinite linear;-webkit-transform:translateZ(0);transform:translateZ(0)}@-webkit-keyframes loader-animation-8{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loader-animation-8{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}`;
function setPopup(...content) {
if (document.querySelector('#_scanStatus') == null) {
const popup = document.querySelector('#_scanPopup') || document.body.appendChild(document.createElement('div'));
popup.id = '_scanPopup';
popup.innerHTML = `<style>${style}</style><div><h1>Hivemind Scan</h1><div id=_scanStatus></div></div>`;
}
const popupContent = document.querySelector('#_scanStatus');
popupContent.innerHTML = content.join('');
}
function setLoadingPopup(...lines) {
const html = lines.filter(x => x).join('<br>').replace(/\n\s*/g, '<br>');
const existing = document.querySelector('#_scanLoading');
if (existing != null) {
existing.innerHTML = html;
} else {
setPopup(`<div style="position:relative;width:120px;height:120px;left:50%;margin-left:-60px"><div class="loading"></div></div><p id="_scanLoading" style="position:relative;width:400px;left:50%;margin-left:-200px">${html}</p><p id="_scanLoadingExtra"></p>`);
}
}
function setLoadingExtra(...content) {
const extra = document.querySelector('#_scanLoadingExtra');
if (extra != null) {
extra.innerHTML = content.join('');
}
}
async function collectAllReviewers() {
const reviews = {};
updatePopup();
await clickWait(document, `[data-apiary-widget-name="@MarketNode/ProductReviewsPaginator"] [aria-label="Страница 1"]`);
do {
Object.assign(reviews, collectPageReviewersFromElement(document));
updatePopup();
} while (await clickWait(document, `[data-apiary-widget-name="@MarketNode/ProductReviewsPaginator"] [aria-label="Следующая страница"]`));
return reviews;
function updatePopup() {
setLoadingPopup(`Собираю список отзывов…`, `Собрано: ${Object.keys(reviews).length}`);
}
}
async function collectAllOtherReviews(reviewerUrl, stepCallback) {
var t = document.body.appendChild(document.createElement('iframe'));
t.setAttribute('style', 'position:fixed;top:0px;left:0px');
t.src = reviewerUrl;
await wait();
for (let i = 0; i < 5; ++i) {
if (!await clickWait(t.contentDocument, '[data-apiary-widget-name="@MarketNode/UserReviews"] > div > button')) break;
console.log(' Link to load more reviews is detected');
stepCallback(collectOtherReviewsFromElement(t.contentDocument));
await wait();
}
const ret = collectOtherReviewsFromElement(t.contentDocument);
document.body.removeChild(t);
return ret;
}
async function runProcessing() {
const reviewers = await collectAllReviewers();
console.log(`In total, ${Object.keys(reviewers).length} reviewers are found`);
if (Object.keys(reviewers).length === 0){
setPopup(`<p>Не нашлось ни одного отзыва. Это точно страница с отзывами?</p>`);
return;
}
const summary = {};
const currentProduct = location.pathname.split('/')[1];
const productName = (document.querySelector('[href^="/product--"] h1') || {}).textContent;
if (productName == null){
setPopup(`<p>Не нашлось названия товара. Это точно страница с отзывами?</p>`);
return;
}
let progress = 0;
for (const [reviewerURL, reviewerData] of Object.entries(reviewers)) {
updatePopup(reviewerData);
console.log(`Processing ${reviewerData.name} (${reviewerURL}, review rating: ${reviewerData.rating})`);
const otherReviews = await collectAllOtherReviews(reviewerURL, data => updatePopup(reviewerData, data));
console.log(` Loaded ${otherReviews.length} reviews left by ${reviewerData.name}, ${otherReviews.filter(x => x.shop).length} of which are for shops, average rating: ${otherReviews.reduce((a, b) => a += +b.rating, 0) / otherReviews.length}`);
for (const otherReview of otherReviews) {
if (otherReview.url.includes(currentProduct)) continue;
otherReview.origin = reviewerData;
if (summary[otherReview.url] == null) {
summary[otherReview.url] = [otherReview];
} else {
summary[otherReview.url].push(otherReview);
console.log(` Intersection found: ${otherReview.shop ? 'shop' : 'product'} ${otherReview.name}, review rating: ${otherReview.rating}`);
}
}
++progress;
updatePopup(reviewerData);
}
function updatePopup(currentReviewer, data = null) {
setLoadingPopup(`Собираю другие отзывы авторов (${progress}/${Object.keys(reviewers).length})…`,
data == null ? `Загружаю отзывы: ${currentReviewer.name}` : `Загружаю отзывы: ${currentReviewer.name} (уже загружено ${data.length})`);
const ordered = Object.values(summary).filter(x => x.length > 1).sort((a, b) => b.length - a.length);
setLoadingExtra(`<p style="white-space:nowrap;height:calc(45vh - 72px);overflow-y:auto;overflow-x:hidden;font-size:11px">${ordered.map(x => `<span style="display:inline-block;width:232px;margin-right:8px;overflow:hidden;vertical-align:bottom;"><a href="${x[0].url}" target="_blank">${x[0].name}</a></span><span style="display:inline-block;width:100px">${x.length} ${(x.length < 10 || x.length > 20) && x.length % 10 > 0 && x.length % 10 < 5 ? 'отзыва' : 'отзывов'}</span><span style="display:inline-block;width:100px">оценка: ${(x.reduce((a, b) => a += b.rating, 0) / x.length).toFixed(1)}/${x.sort((a, b) => a.rating - b.rating)[x.length / 2 | 0].rating.toFixed(1)}</span><span>${x.map(y => `<a href="${y.origin.url}" title="${productName}: ${y.origin.rating}\n${y.name}: ${y.rating}">${y.origin.name}</a>`).join(', ')}</span>`).join('<br>')}</p>`);
}
const ordered = Object.values(summary).filter(x => x.length > 1).sort((a, b) => b.length - a.length);
if (ordered.length == 0) {
setPopup(`<p>Не нашлось товаров и магазинов, отзывы от которых оставило сразу несколько людей 👌</p>`);
} else {
const amount = ordered.length;
const isSingle = amount % 10 == 1 && amount != 11;
const isMultiple = amount % 10 == 0 || amount % 10 > 4 || amount > 10 && amount < 20;
const hasShops = ordered.some(x => x[0].shop);
const hasProducts = ordered.some(x => !x[0].shop);
const clean = Object.values(summary).flat().map(x => x.origin).reduce((a, b) => (a.includes(b) || a.push(b), a), [])
.filter(x => !ordered.some(y => y.some(z => z.origin == x)));
setPopup(`<p>${isSingle ? 'Нашёлся' : 'Нашлось'} ${amount} ${amount == 1 ? (hasShops ? 'магазин' : 'продукт') : [isMultiple ? 'продуктов' : 'продукта', isMultiple ? 'магазинов' : 'магазина'].slice(hasProducts ? 0 : 1, hasShops ? 2 : 1).join(' и ')}, отзывы ${isSingle ? 'которому' : 'которым'} оставило сразу несколько людей ${ordered.some(x => x.length > 3) ? '🤔' : ''}</p>`,
`<p style="white-space:nowrap;height:calc(45vh - 72px);overflow-y:auto;overflow-x:hidden;font-size:11px">${ordered.map(x => `<span style="display:inline-block;width:232px;margin-right:8px;overflow:hidden;vertical-align:bottom;"><a href="${x[0].url}" target="_blank">${x[0].name}</a></span><span style="display:inline-block;width:100px">${x.length} ${(x.length < 10 || x.length > 20) && x.length % 10 > 0 && x.length % 10 < 5 ? 'отзыва' : 'отзывов'}</span><span style="display:inline-block;width:100px">оценка: ${(x.reduce((a, b) => a += b.rating, 0) / x.length).toFixed(1)}/${x.sort((a, b) => a.rating - b.rating)[x.length / 2 | 0].rating.toFixed(1)}</span><span>${x.map(y => `<a href="${y.origin.url}" title="${productName}: ${y.origin.rating}\n${y.name}: ${y.rating}">${y.origin.name}</a>`).join(', ')}</span>`).join('<br>')}</p>`,
`<p>Остальные отзывы: ${clean.length}</p>`,
`<p style="white-space:nowrap;height:calc(45vh - 72px);overflow-y:auto;overflow-x:hidden;font-size:11px">${clean.map(x => `<span style="display:inline-block;width:232px;margin-right:8px;overflow:hidden;vertical-align:bottom;"><a href="${x.url}" target="_blank">${x.name}</a></span><span style="display:inline-block;width:100px">оценка: ${x.rating.toFixed(1)}</span>`).join('<br>')}</p>`);
}
window['lastScanSummary'] = summary;
return summary;
}
return runProcessing();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment