Skip to content

Instantly share code, notes, and snippets.

@listly-io
Last active November 3, 2025 08:41
Show Gist options
  • Select an option

  • Save listly-io/46bbee83449eb14163e42dd1c2bf1699 to your computer and use it in GitHub Desktop.

Select an option

Save listly-io/46bbee83449eb14163e42dd1c2bf1699 to your computer and use it in GitHub Desktop.
2025-10-23
// https://shopee.vn/-QU%C3%80-T%E1%BA%B6NG-385K-Kem-d%C6%B0%E1%BB%A1ng-%E1%BA%A9m-ph%E1%BB%A5c-h%E1%BB%93i-v%C3%A0-ch%C4%83m-s%C3%B3c-h%C3%A0ng-r%C3%A0o-b%E1%BA%A3o-v%E1%BB%87-da-AESTURA-ATOBARRIER365-Cream-80ml-i.1122867310.24257591496?is_from_signup=true
// ========== 사용자 설정 변수 ==========
const MAX_PAGE_COUNT = 10; // 수집할 페이지 수
const WAIT_TIME = 5000; // 페이지 로딩 대기 시간 (밀리초)
const SCROLL_WAIT_TIME = 1000; // 스크롤 후 대기 시간 (밀리초)
const REVIEW_SELECTOR = 'DIV[class="shopee-product-comment-list"] > div'; // 리뷰 목록 셀렉터
const PAGINATION_SELECTOR = 'nav.shopee-page-controller'; // 페이지네이션 영역 셀렉터
// ========== 메인 코드 ==========
(async function() {
// 수집 컨테이너 생성 (body 마지막 자식으로 추가)
const collectionContainer = document.createElement('div');
collectionContainer.id = 'review-collection-container';
collectionContainer.style.cssText = 'position: fixed; bottom: 200px; right: 10px; background: #fff; border: 2px solid #333; padding: 10px; font-size: 14px; z-index: 9999; min-width: 200px; max-height: 150px; overflow-y: auto;';
document.body.appendChild(collectionContainer);
// 상태 표시 영역
const statusDiv = document.createElement('div');
statusDiv.id = 'collection-status';
collectionContainer.appendChild(statusDiv);
// 수집 데이터 저장 영역 (숨김)
const dataContainer = document.createElement('div');
dataContainer.id = 'collected-data-container';
dataContainer.style.display = 'none';
collectionContainer.appendChild(dataContainer);
let collectedCount = 0;
let totalReviewCount = 0;
// 상태 표시 업데이트 함수
function updateStatus(message) {
statusDiv.innerHTML = `
<div style="font-weight: bold; margin-bottom: 5px;">리뷰 수집 중...</div>
<div>수집된 페이지: ${collectedCount} / ${MAX_PAGE_COUNT}</div>
<div>수집된 리뷰: ${totalReviewCount}개</div>
<div style="margin-top: 5px; color: #666;">${message}</div>
`;
}
// 리뷰 수집 함수
function collectReviews() {
const reviews = document.querySelectorAll(REVIEW_SELECTOR);
// 각 리뷰를 dataContainer에 직접 추가 (같은 레벨의 sibling으로)
reviews.forEach(review => {
dataContainer.appendChild(review.cloneNode(true));
});
collectedCount++;
totalReviewCount += reviews.length;
console.log(`페이지 ${collectedCount} 수집 완료 (이번 페이지: ${reviews.length}개, 총: ${totalReviewCount}개)`);
return reviews.length;
}
// 대기 함수
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 페이지네이션 영역으로 스크롤
async function scrollToPagination() {
const pagination = document.querySelector(PAGINATION_SELECTOR);
if (pagination) {
pagination.scrollIntoView({ behavior: 'smooth', block: 'center' });
console.log('페이지네이션 영역으로 스크롤 이동');
await wait(SCROLL_WAIT_TIME);
} else {
console.warn('페이지네이션 영역을 찾을 수 없습니다.');
}
}
// 다음 페이지 버튼 찾기 및 클릭 함수
async function clickNextPage() {
// 페이지네이션 영역으로 스크롤 이동
await scrollToPagination();
// 현재 활성화된 페이지 번호 찾기 (shopee-button-solid--primary 클래스)
const currentPageBtn = document.querySelector('button.shopee-button-solid.shopee-button-solid--primary');
if (!currentPageBtn) {
console.error('현재 페이지 버튼을 찾을 수 없습니다.');
return false;
}
console.log('현재 페이지:', currentPageBtn.textContent);
// 다음 페이지 번호 버튼 찾기 (shopee-button-no-outline 클래스, ... 버튼 제외)
let nextPageBtn = currentPageBtn.nextElementSibling;
// ... 버튼은 건너뛰기
while (nextPageBtn && nextPageBtn.classList.contains('shopee-button-no-outline--non-click')) {
nextPageBtn = nextPageBtn.nextElementSibling;
}
if (nextPageBtn && nextPageBtn.classList.contains('shopee-button-no-outline') && !nextPageBtn.disabled) {
console.log('다음 페이지로 이동:', nextPageBtn.textContent);
nextPageBtn.click();
return true;
} else {
console.log('다음 페이지가 없습니다.');
return false;
}
}
// 메인 수집 루프
try {
updateStatus('첫 번째 페이지 수집 중...');
// 첫 번째 페이지 수집
collectReviews();
updateStatus(`완료! 다음 페이지로 이동 중...`);
// 나머지 페이지들 수집
for (let i = 1; i < MAX_PAGE_COUNT; i++) {
// 다음 페이지로 이동 (스크롤 + 대기 포함)
if (!await clickNextPage()) {
updateStatus('수집 완료 (마지막 페이지 도달)');
break;
}
// 페이지 로딩 대기
await wait(WAIT_TIME);
// 리뷰 수집
updateStatus(`페이지 ${i + 1} 수집 중...`);
collectReviews();
updateStatus(`페이지 ${i + 1} 완료!`);
}
// 최종 결과
statusDiv.innerHTML = `
<div style="font-weight: bold; color: green; margin-bottom: 10px;">✓ 수집 완료!</div>
<div>수집된 페이지: ${collectedCount}개</div>
<div>수집된 리뷰: ${totalReviewCount}개</div>
<div style="margin-top: 10px; font-size: 12px; color: #666;">
수집된 데이터는 이 div 내부<br>
'collected-data-container'에 저장됨
</div>
`;
console.log('=== 수집 완료 ===');
console.log(`총 ${collectedCount}개 페이지, ${totalReviewCount}개 리뷰 수집 완료`);
console.log('수집된 HTML은 document.querySelector("#collected-data-container") 내부에 저장됨');
} catch (error) {
console.error('수집 중 오류 발생:', error);
statusDiv.innerHTML = `
<div style="color: red; font-weight: bold;">오류 발생!</div>
<div style="font-size: 12px;">${error.message}</div>
`;
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment