-
-
Save listly-io/b55dca9a2cf8015b4666377e45a2eb38 to your computer and use it in GitHub Desktop.
costco_review_en_2026-06-04
This file contains hidden or 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
| // test page: https://www.costco.com/p/-/trunature-premium-milk-thistle-160-mg-120-vegetarian-capsules/100123488?langId=-1 | |
| // ============================================================ | |
| // ▶ User configuration (edit as needed) | |
| // ============================================================ | |
| const MAX_PAGE_COUNT = 10; // Number of pages to collect (1 = current page only) | |
| const WAIT_AFTER_CLICK = 5000; // Wait time after clicking to the next page (ms) | |
| const WAIT_BEFORE_CLICK = 1000; // Wait time after scrolling, before clicking (ms) | |
| const CONTAINER_ID = '__review_collector'; // Paste this into the Listly selector as-is | |
| // ============================================================ | |
| // ▶ Shadow DOM selectors (adjusted for Costco's BazaarVoice structure) | |
| // ============================================================ | |
| // Shadow root host: #CostcoBVContainer | |
| // Reviews wrapper: section[class*="bv-rnr__ioct8z"] | |
| // Each review: wrapper > section (direct child) | |
| // Next page anchor: a.next (an <a> tag, not a button!) | |
| // Pagination area: ul.bv-rnr__zv3ryj-1 | |
| // Accordion button: #member_reviews (click it to trigger the BV load) | |
| // ============================================================ | |
| // ▶ Internal logic (no edits needed) | |
| // ============================================================ | |
| (async () => { | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| // ── Helper: grab the shadow root ───────────────────────── | |
| const getShadow = () => { | |
| const host = document.getElementById('CostcoBVContainer'); | |
| return host ? host.shadowRoot : null; | |
| }; | |
| // ── 1. Open the accordion (if it's closed) ─────────────── | |
| const accordionBtn = document.getElementById('member_reviews'); | |
| if (accordionBtn && accordionBtn.getAttribute('aria-expanded') === 'false') { | |
| console.log('[Scraper] Opening the Member Reviews accordion...'); | |
| accordionBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| await sleep(800); | |
| accordionBtn.click(); | |
| await sleep(WAIT_AFTER_CLICK); | |
| } | |
| // ── 2. Check the Shadow DOM ────────────────────────────── | |
| let shadow = getShadow(); | |
| if (!shadow) { | |
| console.error('[Scraper] Couldn\'t find the shadow root. Check the page.'); | |
| return; | |
| } | |
| // ── 3. Create the UI container (Listly scrapes this container's children) ── | |
| let container = document.getElementById(CONTAINER_ID); | |
| if (!container) { | |
| container = document.createElement('div'); | |
| container.id = CONTAINER_ID; | |
| Object.assign(container.style, { | |
| position: 'fixed', | |
| right: '200px', | |
| bottom: '200px', | |
| zIndex: '999999', | |
| background: 'rgba(0, 0, 0, 0.85)', | |
| color: '#0f0', | |
| fontSize: '18px', | |
| fontWeight: 'bold', | |
| fontFamily: 'monospace', | |
| padding: '16px 24px', | |
| borderRadius: '12px', | |
| border: '2px solid #0f0', | |
| boxShadow: '0 4px 20px rgba(0,255,0,.3)', | |
| cursor: 'default', | |
| userSelect: 'none', | |
| overflow: 'hidden', | |
| }); | |
| document.body.appendChild(container); | |
| } | |
| // Collection store (hidden) — the actual data Listly reads via the selector | |
| let store = container.querySelector('#__review_store'); | |
| if (!store) { | |
| store = document.createElement('div'); | |
| store.id = '__review_store'; | |
| store.style.display = 'none'; | |
| container.appendChild(store); | |
| } | |
| // Running count label | |
| let label = container.querySelector('#__review_label'); | |
| if (!label) { | |
| label = document.createElement('div'); | |
| label.id = '__review_label'; | |
| container.appendChild(label); | |
| } | |
| const updateLabel = () => { | |
| label.textContent = `Reviews collected: ${store.children.length}`; | |
| }; | |
| // ── 4. Collect the reviews on the current page ─────────── | |
| const collectCurrentPage = () => { | |
| shadow = getShadow(); | |
| if (!shadow) { console.warn('[Scraper] No shadow root, skipping'); return; } | |
| const reviewsWrapper = shadow.querySelector('section[class*="bv-rnr__ioct8z"]'); | |
| if (!reviewsWrapper) { console.warn('[Scraper] Couldn\'t find the reviews wrapper'); return; } | |
| const reviewSections = reviewsWrapper.querySelectorAll(':scope > section'); | |
| let added = 0; | |
| reviewSections.forEach((sec) => { | |
| const card = sec.children[0]; | |
| if (!card) return; | |
| const lines = card.innerText.split('\n').map((l) => l.trim()).filter(Boolean); | |
| // Layout: [0]rating [1]title [2]author [?]Verified Purchaser [?]date [...]body | |
| const rating = lines[0] || ''; | |
| const title = lines[1] || ''; | |
| const author = lines[2] || ''; | |
| const verified = lines[3] === 'Verified Purchaser'; | |
| const date = verified ? (lines[4] || '') : (lines[3] || ''); | |
| const bodyStart = verified ? 5 : 4; | |
| const body = lines.slice(bodyStart).join('\n'); | |
| // Build text nodes so Listly can pull each field with a CSS selector | |
| const item = document.createElement('div'); | |
| item.className = 'scraped-review'; | |
| const addField = (cls, text) => { | |
| const el = document.createElement('div'); | |
| el.className = cls; | |
| el.textContent = text; | |
| item.appendChild(el); | |
| }; | |
| addField('review-rating', rating); | |
| addField('review-title', title); | |
| addField('review-author', author); | |
| addField('review-verified', verified ? 'Verified Purchaser' : ''); | |
| addField('review-date', date); | |
| addField('review-body', body); | |
| store.appendChild(item); | |
| added++; | |
| }); | |
| console.log(`[Scraper] Collected ${added} reviews (total: ${store.children.length})`); | |
| updateLabel(); | |
| }; | |
| // ── 5. Go to the next page ─────────────────────────────── | |
| const goNextPage = async () => { | |
| shadow = getShadow(); | |
| if (!shadow) return false; | |
| const nextA = shadow.querySelector('a.next'); | |
| if (!nextA || nextA.getAttribute('aria-disabled') === 'true' || !nextA.href) return false; | |
| const paginationUL = shadow.querySelector('ul.bv-rnr__zv3ryj-1'); | |
| if (paginationUL) { | |
| paginationUL.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| await sleep(WAIT_BEFORE_CLICK); | |
| const nextAAgain = shadow.querySelector('a.next'); | |
| if (!nextAAgain || nextAAgain.getAttribute('aria-disabled') === 'true') return false; | |
| nextAAgain.click(); | |
| return true; | |
| }; | |
| // ── 6. Main loop ───────────────────────────────────────── | |
| console.log(`[Scraper] Starting collection — up to ${MAX_PAGE_COUNT} pages`); | |
| for (let page = 1; page <= MAX_PAGE_COUNT; page++) { | |
| if (page > 1) await sleep(WAIT_AFTER_CLICK); | |
| collectCurrentPage(); | |
| console.log(`[Scraper] Page ${page}/${MAX_PAGE_COUNT} collected`); | |
| if (page === MAX_PAGE_COUNT) break; | |
| const moved = await goNextPage(); | |
| if (!moved) { | |
| console.log('[Scraper] No next page — collection finished'); | |
| break; | |
| } | |
| } | |
| console.log(`[Scraper] Collection complete! Total reviews: ${store.children.length}`); | |
| console.log(`[Scraper] Now open the Listly extension and enter #${CONTAINER_ID} in the CSS selector.`); | |
| updateLabel(); | |
| // ▶ To pull the results as JSON later, run in the console: | |
| // Array.from(document.querySelectorAll('#__review_store .scraped-review')).map(el => ({ | |
| // rating: el.querySelector('.review-rating')?.textContent, | |
| // title: el.querySelector('.review-title')?.textContent, | |
| // author: el.querySelector('.review-author')?.textContent, | |
| // verified: el.querySelector('.review-verified')?.textContent, | |
| // date: el.querySelector('.review-date')?.textContent, | |
| // body: el.querySelector('.review-body')?.textContent, | |
| // })) | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment