Created
November 9, 2020 09:39
-
-
Save gro-ove/b5abcc7e7d6f4c8693bad6667ca64e26 to your computer and use it in GitHub Desktop.
Если запустить на странице с отзывами на товар на Яндекс.Маркете, соберёт остальные отзывы комментаторов и поищет интересные совпадения, когда разные люди оставляют одинаковые оценки на одинаковые товары или магазины.
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
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