Scrape Booksy reviews and copy to clipboard in JSON format with UI button(s)
// ==UserScript== // @name Booksy Reviews Scraper to JSON // @namespace http://tampermonkey.net/ // @version 2.1 // @description Scrape Booksy reviews and copy to clipboard in JSON format with UI button(s) // @author sharmanhall // @match https://booksy.com/en-us/* // @match *://booksy.com/en-us/* // @icon https://www.google.com/s2/favicons?sz=64&domain=booksy.com // @license MIT // @compatible chrome // @compatible edge // @compatible firefox // @compatible opera // @compatible safari // @grant none // ==/UserScript== (function() { 'use strict'; function scrapeReviews() { return new Promise((resolve) => { const reviews = []; const reviewsSection = document.querySelector("#reviews-section"); if (!reviewsSection) { console.log("Reviews section not found."); resolve(reviews); return; } const reviewItems = reviewsSection.querySelectorAll('[data-testid="review-item"]'); reviewItems.forEach((reviewElement, index) => { try { const reviewerNameElement = reviewElement.querySelector('div[class*="purify_E9KQOPWS1B+V9XvpZovy8A"] span[class*="purify_HRlYZ9s5U73L+xgNWotErg"]'); const reviewDateElement = reviewElement.querySelector('div[class*="purify_E9KQOPWS1B+V9XvpZovy8A"] span[class*="purify_VsjLagY8Ojq+9Ze+lWDQGQ"]'); const reviewContentElement = reviewElement.querySelector('div[class*="purify_1KV69VGQK1FO5206zDXt6w"] span'); const serviceElements = reviewElement.querySelectorAll('[data-testid="review-service"]'); const stafferElement = reviewElement.querySelector('[data-testid="review-staffer"]'); const starRatingElements = reviewElement.querySelectorAll('div[class*="purify_Qoav5NuXv0ym9cfxyao4Ig"]'); const replyElement = reviewElement.querySelector('[data-testid="review-reply"]'); const verifiedElement = reviewElement.querySelector('[data-testid="verified-badge"]'); if (!reviewerNameElement || !reviewDateElement || !reviewContentElement || !starRatingElements) { console.log(`Missing data in review ${index + 1}`); return; } const reviewerName = reviewerNameElement.innerText.trim().replace(' •', ''); const reviewDate = reviewDateElement.innerText.trim(); const starRating = starRatingElements.length; const reviewContent = reviewContentElement.innerText.trim(); const verified = verifiedElement !== null; let services = []; serviceElements.forEach(serviceElement => { services.push(serviceElement.innerText.trim()); }); let reply = null; if (replyElement) { const replyDateElement = replyElement.querySelector('span[class*="purify_FD6WbUSz3rkoW+C9e7kZ8A"]'); const replyContentElement = replyElement.querySelector('div[class*="purify_XhJpX0ckn22M3YvadX5CmQ"]'); const replyFromElement = replyElement.querySelector('div[class*="purify_GPF4-5C5H8PSMkYNQiwwzA"]'); if (replyDateElement && replyContentElement && replyFromElement) { const replyDate = replyDateElement.innerText.trim(); const replyContent = replyContentElement.innerText.trim(); const replyFrom = replyFromElement.innerText.trim(); reply = { reply_date: replyDate, reply_content: replyContent, reply_from: replyFrom }; } else { console.log(`Missing reply data in review ${index + 1}`); } } const review = { reviewer_name: reviewerName, img_url: "", // Could not find image URL in the given structure review_date: reviewDate, star_rating: starRating, review_url: "", // No review URL in the given structure review_content: reviewContent, services: services.join(', '), staffer: stafferElement ? stafferElement.innerText.trim() : '', verified: verified, reply: reply }; reviews.push(review); } catch (error) { console.log(`Error processing review ${index + 1}:`, error); } }); resolve(reviews); }); } async function scrapeAllReviews() { let allReviews = []; let currentPage = 1; let totalPages = document.querySelectorAll('#reviews-section > div.purify_LmECaSkpXh8qi\\+Leh32tdw\\=\\=.purify_JUl\\+sl0GnJSkGuqya\\+I4uQ\\=\\= > ul > li').length - 2; // Subtracting 2 for previous and next buttons while (currentPage <= totalPages) { const pageReviews = await scrapeReviews(); allReviews = allReviews.concat(pageReviews); if (currentPage < totalPages) { const nextPageButton = document.querySelector(`#reviews-section > div.purify_LmECaSkpXh8qi\\+Leh32tdw\\=\\=.purify_JUl\\+sl0GnJSkGuqya\\+I4uQ\\=\\= > ul > li:nth-child(${currentPage + 2})`); nextPageButton.click(); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for 2 seconds for the next page to load } currentPage++; } const reviewsJson = JSON.stringify({ Reviews: allReviews }, null, 2); console.log(reviewsJson); displayReviews(reviewsJson); copyToClipboard(reviewsJson, allReviews.length); } function displayReviews(reviewsJson) { const container = document.createElement('div'); container.style.position = 'fixed'; container.style.bottom = '80px'; container.style.left = '20px'; container.style.backgroundColor = '#fff'; container.style.padding = '10px'; container.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; container.style.maxHeight = '50vh'; container.style.overflowY = 'scroll'; container.style.zIndex = '1000'; container.innerText = reviewsJson; document.body.appendChild(container); } function copyToClipboard(text, reviewCount) { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); alert(`${reviewCount} reviews copied to clipboard`); } function addButton(text, bottom, onClick) { const button = document.createElement('button'); button.innerHTML = `<span style="font-size: 16px; font-weight: bold; color: #fff;">${text}</span>`; button.type = 'button'; button.style.position = 'fixed'; button.style.bottom = bottom; button.style.left = '10px'; button.style.zIndex = '1000'; button.style.backgroundColor = '#00A3AD'; button.style.border = 'none'; button.style.borderRadius = '4px'; button.style.padding = '10px 20px'; button.style.cursor = 'pointer'; button.onclick = onClick; document.body.appendChild(button); } // Wait for the page to load completely window.addEventListener('load', () => { console.log("Page loaded. Adding buttons."); addButton('Copy Reviews', '50px', async () => { const reviews = await scrapeReviews(); const reviewsJson = JSON.stringify({ Reviews: reviews }, null, 2); displayReviews(reviewsJson); copyToClipboard(reviewsJson, reviews.length); }); addButton('Copy All Reviews (TODO FIX)', '10px', scrapeAllReviews); // Changed button text to indicate TODO }); // TODO: Fix pagination issue to scrape all pages of reviews correctly. })();