-
-
Save listly-io/9c7ca29db8959ad4a3e7952f79e3caa0 to your computer and use it in GitHub Desktop.
shein_review_2026-06-10
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://kr.shein.com/Short-Sleeve-T-Shirt-Versatile-Short-Sleeve-Tee-Women-s-Short-Sleeve-Top-Slim-Fit-Solid-Color-Undershirt-Casual-Black-Summer-review-142262475.html?mall_code=1&goods_spu=z250419597558&local_site_query_flag=&sku=sz25041959755899894&store_code=9108550356&isLowestPriceProductOfBuyBox=NaN&cat_id=1738&sku_code=&isAppointMall=&isTrusted=true&_vts=1781142343247 | |
| // ============================================================ | |
| // ▶ User configuration (edit as needed) | |
| // ============================================================ | |
| // Works from EITHER the product page (...-p-142262475.html) OR the | |
| // dedicated reviews page (...-review-142262475.html). | |
| // On the product page it auto-clicks "View more reviews" (which navigates), | |
| // then resumes automatically on the reviews page via sessionStorage. | |
| const MAX_PAGE_COUNT = 10; // Number of scroll-load batches (1 = current view only) | |
| const WAIT_AFTER_LOAD = 5000; // Wait after page load / new batch (ms) | |
| const WAIT_BEFORE_NAV = 1000; // Wait after scrolling, before checking (ms) | |
| const REVIEW_LIST_SELECTOR = '.common-reviews-new__list-item'; // Individual review item selector | |
| const VIEW_ALL_SELECTOR = '.common-reviews__pageViewAll'; // "View more reviews" button (product page) | |
| const REVIEW_ID_ATTR = 'data-comment-id'; // Attribute used to de-duplicate reviews (falls back to text) | |
| const CONTAINER_ID = '__review_collector'; // Collector container ID | |
| // ============================================================ | |
| // ▶ Internal logic (no edits needed) | |
| // ============================================================ | |
| (async () => { | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| // Build a stable de-dup key for a review element. | |
| // On the product page the id lives on the list item (REVIEW_ID_ATTR); | |
| // on the reviews page the item has no id, so we also probe descendants | |
| // for [data-comment-id]/[data-review-id], and finally fall back to text. | |
| const keyFor = (el) => { | |
| let id = el.getAttribute(REVIEW_ID_ATTR); | |
| if (!id) { | |
| const inner = el.querySelector('[data-comment-id], [data-review-id]'); | |
| if (inner) id = inner.getAttribute('data-comment-id') || inner.getAttribute('data-review-id'); | |
| } | |
| return ((id || el.textContent || '')).trim().slice(0, 120); | |
| }; | |
| // 1. Create the collector container (reuse if it already exists) | |
| 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 area) — a display:none wrapper inside the container | |
| let store = container.querySelector('#__review_store'); | |
| if (!store) { | |
| store = document.createElement('div'); | |
| store.id = '__review_store'; | |
| store.style.display = 'none'; | |
| container.appendChild(store); | |
| } | |
| // Count display 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}`; | |
| }; | |
| // Restore any previously saved progress (after navigating from the product page) | |
| const saved = sessionStorage.getItem('__review_store_html'); | |
| if (saved && store.children.length === 0) { | |
| store.innerHTML = saved; | |
| updateLabel(); | |
| } | |
| // De-dup set, rebuilt from whatever is already in the store | |
| const seen = new Set(); | |
| [...store.children].forEach((el) => { | |
| const key = keyFor(el); | |
| if (key) seen.add(key); | |
| }); | |
| // 2. Collect reviews currently in the DOM (de-duplicated) | |
| const collectCurrentPage = () => { | |
| const reviews = document.querySelectorAll( | |
| `${REVIEW_LIST_SELECTOR}:not(#${CONTAINER_ID} *)` | |
| ); | |
| let added = 0; | |
| reviews.forEach((el) => { | |
| const key = keyFor(el); | |
| if (!key || seen.has(key)) return; | |
| seen.add(key); | |
| store.appendChild(el.cloneNode(true)); | |
| added++; | |
| }); | |
| console.log(`[Scraper] Collected ${added} new reviews (total: ${store.children.length})`); | |
| updateLabel(); | |
| }; | |
| // 3a. If we're on the PRODUCT page, jump to the reviews page first. | |
| // Clicking "View more reviews" triggers a full navigation, so we save | |
| // progress (like the Amazon script did) and let the script re-run on load. | |
| const onReviewsPage = /-review-\d+\.html/.test(location.pathname); | |
| if (!onReviewsPage) { | |
| collectCurrentPage(); // grab the 3 preview reviews shown on the product page | |
| const viewAll = document.querySelector(VIEW_ALL_SELECTOR); | |
| if (viewAll) { | |
| console.log('[Scraper] On product page — opening full reviews page…'); | |
| sessionStorage.setItem('__review_store_html', store.innerHTML); | |
| viewAll.click(); | |
| return; // page navigates; re-run this script on the reviews page to continue | |
| } | |
| console.log('[Scraper] No "View more reviews" button found — staying on this page.'); | |
| } | |
| // 3b. Reviews page: load the next batch via infinite scroll. | |
| // Returns true if more reviews appeared, false at end of list. | |
| const goNextPage = async () => { | |
| const before = document.querySelectorAll(REVIEW_LIST_SELECTOR).length; | |
| window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' }); | |
| await sleep(WAIT_BEFORE_NAV); | |
| window.scrollTo(0, document.documentElement.scrollHeight); | |
| await sleep(WAIT_AFTER_LOAD); | |
| return document.querySelectorAll(REVIEW_LIST_SELECTOR).length > before; | |
| }; | |
| // 4. Main loop | |
| console.log(`[Scraper] Starting collection — up to ${MAX_PAGE_COUNT} batches`); | |
| for (let page = 1; page <= MAX_PAGE_COUNT; page++) { | |
| collectCurrentPage(); | |
| console.log(`[Scraper] Batch ${page}/${MAX_PAGE_COUNT} collected`); | |
| if (page === MAX_PAGE_COUNT) break; | |
| const moved = await goNextPage(); | |
| if (!moved) { | |
| console.log('[Scraper] No more reviews loaded — collection finished'); | |
| collectCurrentPage(); | |
| break; | |
| } | |
| } | |
| console.log(`[Scraper] Collection complete! Total reviews: ${store.children.length}`); | |
| updateLabel(); | |
| sessionStorage.removeItem('__review_store_html'); | |
| // ▶ To retrieve the collected HTML later, run in the console: | |
| // document.getElementById('__review_store').innerHTML | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment