🏠 Home 

Booksy Reviews Scraper to JSON

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.
})();