-
-
Save listly-io/38f99fa91352f9f03c9e69a1ea60b36a to your computer and use it in GitHub Desktop.
wayfair_review_2026-07-16
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.wayfair.com/furniture/pdp/lark-manor-tyler-nailhead-swivel-barrel-chair-w113055765.html | |
| // | |
| // [주의] Wayfair 웹 사이트의 경우, 첫 페이지 이후의 페이지들에서 확인할 수 있는 리뷰들은 웹 사이트 로그인을 후 접근 가능합니다. | |
| // 그렇기 때문에 해당 웹 사이트에 먼저 로그인을 한 후 이 스크립트를 사용하시면 문제 없이 원하는 페이지에서 리뷰를 추출하실 수 있습니다. | |
| // | |
| // [사용법] | |
| // 0. 오픈코드 사용법 참고 (https://app.gitbook.com/o/Uu6AM3fgpZza0EpsJM4N/s/iKxoYELFf9WS8XCvYe6S/support/open-codes/how-to) | |
| // 1. 상품 리뷰 페이지에서 개발자 도구를 열어 콘솔에 아래 스크립트를 붙여넣기 → 자동 수집 시작 | |
| // 2. 수집 완료 후, "수집된 리뷰: N" 박스 영역을 Listly Parts로 선택하여 추출 | |
| // | |
| // ============================================================ | |
| // ▶ 사용자 설정 영역 (필요에 따라 수정해주세요) | |
| // ============================================================ | |
| const MAX_PAGE_COUNT = 10; // 수집할 페이지 수 (1 = 현재 페이지 / "다음" 클릭 횟수 + 1) | |
| const WAIT_AFTER_CLICK = 5000; // 다음 페이지("다음") 클릭 후 대기 시간 (ms) | |
| const WAIT_BEFORE_CLICK = 1000; // 스크롤 후 클릭 전 대기 시간 (ms) | |
| const REVIEW_TAB_SELECTOR = 'h2'; // 리뷰 탭 버튼 셀렉터 (텍스트 "리뷰" 로 식별) | |
| const REVIEW_LIST_SELECTOR = '._6o3atz1ay'; // 개별 리뷰 항목 셀렉터 | |
| const PAGINATION_SELECTOR = 'button'; // 페이지네이션(다음 버튼이 속한) 영역 셀렉터 | |
| const NEXT_BTN_TEXT = 'Show More Reviews'; // 다음("다음") 버튼 식별 텍스트 | |
| const CONTAINER_ID = '__review_collector'; // 수집 컨테이너 ID | |
| // ======================== 데이터 추출 ==================================== | |
| (async () => { | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| // 리뷰 탭 활성화 | |
| // 탭이 비활성 상태면 .review-items 가 렌더링되지 않으므로, 먼저 탭을 클릭 | |
| const ensureReviewTab = async () => { | |
| const tabs = Array.from(document.querySelectorAll(REVIEW_TAB_SELECTOR)); | |
| const reviewTab = tabs.find((t) => /리뷰|Customer Reviews/i.test(t.innerText)); | |
| if (reviewTab) { | |
| reviewTab.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } | |
| // 리뷰 카드가 실제로 렌더링될 때까지 대기 (최대 10초) | |
| for (let i = 0; i < 20; i++) { | |
| if (document.querySelector(REVIEW_LIST_SELECTOR)) break; | |
| await sleep(500); | |
| } | |
| }; | |
| await ensureReviewTab(); | |
| // 수집 컨테이너 생성 | |
| 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); | |
| } | |
| // 수집 저장 (숨김 영역): 컨테이너 내부에 display:none 인 래퍼 | |
| let store = container.querySelector('#__review_store'); | |
| if (!store) { | |
| store = document.createElement('div'); | |
| store.id = '__review_store'; | |
| store.style.display = 'none'; | |
| container.appendChild(store); | |
| } | |
| // 카운트 표시 라벨 | |
| let label = container.querySelector('#__review_label'); | |
| if (!label) { | |
| label = document.createElement('div'); | |
| label.id = '__review_label'; | |
| container.appendChild(label); | |
| } | |
| const updateLabel = () => { | |
| const count = store.children.length; | |
| label.textContent = `수집된 리뷰: ${count}개`; | |
| }; | |
| // 중복 방지용 키 집합 (Wayfair는 "Show More Reviews" 클릭 시 리뷰가 추가되므로, | |
| // 라이브 엘리먼트의 data-속성 대신 리뷰 내용 기반으로 중복을 판별함) | |
| const seen = new Set(); | |
| const reviewKey = (el) => { | |
| // 작성자 + 날짜 + 본문 앞부분을 조합해 고유 키 생성 | |
| const writer = (el.querySelector('span._6o3atzbl')?.innerText || '').trim(); | |
| const dateEl = Array.from(el.querySelectorAll('span')).find((s) => /^\d{2}\/\d{2}\/\d{4}$/.test(s.innerText.trim())); | |
| const date = (dateEl?.innerText || '').trim(); | |
| const desc = (el.querySelector('p._6o3atzbl')?.innerText || '').trim(); | |
| return `${writer}|${date}|${desc}`.slice(0, 200) || el.innerText.trim().slice(0, 200); | |
| }; | |
| // 현재 페이지 리뷰 수집 함수 | |
| // (Wayfair는 "Show More Reviews" 클릭 시 기존 리스트에 추가되므로, 내용 기반으로 이미 수집한 항목은 건너뜀) | |
| const collectCurrentPage = () => { | |
| const reviews = Array.from( | |
| document.querySelectorAll(`${REVIEW_LIST_SELECTOR}:not(#${CONTAINER_ID} *)`) | |
| ).filter((el) => /Rated \d out of 5/.test(el.innerText)); | |
| let added = 0; | |
| reviews.forEach((el) => { | |
| const key = reviewKey(el); | |
| if (seen.has(key)) return; // 이미 수집한 리뷰는 건너뜀 | |
| seen.add(key); | |
| const clone = el.cloneNode(true); | |
| store.appendChild(clone); | |
| added++; | |
| }); | |
| console.log(`[Scraper] ${added}개 리뷰 수집 (누적 ${store.children.length}개)`); | |
| updateLabel(); | |
| }; | |
| // 다음 페이지("다음") 버튼 클릭 함수 | |
| // 반환: true = 클릭 성공, false = 버튼 없음/비활성 | |
| const findNextBtn = () => | |
| Array.from(document.querySelectorAll(PAGINATION_SELECTOR)).find((b) => | |
| new RegExp(NEXT_BTN_TEXT, 'i').test(b.textContent) | |
| ); | |
| const goNextPage = async () => { | |
| const btn = findNextBtn(); | |
| if (!btn || btn.disabled || btn.classList.contains('disabled') || btn.offsetParent === null) return false; | |
| // 페이지네이션("다음" 버튼) 영역으로 스크롤 | |
| btn.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| await sleep(WAIT_BEFORE_CLICK); | |
| // 다시 한번 버튼 상태 확인 (스크롤 중 상태 변경 가능) | |
| const btnAgain = findNextBtn(); | |
| if (!btnAgain || btnAgain.disabled || btnAgain.classList.contains('disabled') || btnAgain.offsetParent === null) return false; | |
| btnAgain.click(); | |
| return true; | |
| }; | |
| // 메인 | |
| console.log(`[Scraper] 수집 시작 — 최대 ${MAX_PAGE_COUNT}페이지`); | |
| for (let page = 1; page <= MAX_PAGE_COUNT; page++) { | |
| // 현재 페이지 수집 | |
| collectCurrentPage(); | |
| console.log(`[Scraper] ${page}/${MAX_PAGE_COUNT} 페이지 수집 완료`); | |
| // 마지막 페이지면 종료 | |
| if (page === MAX_PAGE_COUNT) break; | |
| // 다음 페이지로 이동 | |
| const moved = await goNextPage(); | |
| if (!moved) { | |
| console.log('[Scraper] 다음 페이지 없음 — 수집 종료'); | |
| break; | |
| } | |
| // 페이지 로딩 대기 | |
| await sleep(WAIT_AFTER_CLICK); | |
| } | |
| console.log(`[Scraper] 수집 완료! 총 ${store.children.length}개 리뷰`); | |
| updateLabel(); | |
| // ▶ 추후 수집된 HTML을 꺼내려면 콘솔에서 아래 ID 확인 | |
| // document.getElementById('__review_store').innerHTML | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment