Google Maps Reviews Scraper & Exporter

Scrapes reviews from Google Maps with a floating interface and improved functionality

 // ==UserScript==
// @name         Google Maps Reviews Scraper & Exporter
// @namespace    http://tampermonkey.net/
// @version      0.13
// @description  Scrapes reviews from Google Maps with a floating interface and improved functionality
// @author       sharmanhall
// @match        https://www.google.com/maps/place/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant        GM_addStyle
// @license      MIT 
// ==/UserScript==

(function() {
    'use strict';

    // Add styles for floating panel
    GM_addStyle(`
        #review-scraper-panel {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background-color: #fff;
            border: 1px solid #ccc;
            border-radius: 8px;
            padding: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 1000;
            font-family: Arial, sans-serif;
        }
        #review-scraper-panel button {
            background-color: #4285f4;
            color: white;
            border: none;
            padding: 8px 12px;
            margin: 5px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        #review-scraper-panel button:hover {
            background-color: #3367d6;
        }
        #scraper-status {
            margin-top: 10px;
            font-size: 14px;
        }
    `);

    // Create floating panel
    const panel = document.createElement('div');
    panel.id = 'review-scraper-panel';
    panel.innerHTML = `
        <button id="scrape-reviews">Scrape Reviews</button>
        <button id="copy-to-clipboard">Copy to Clipboard</button>
        <div id="scraper-status"></div>
    `;
    document.body.appendChild(panel);

    const setStatus = (message) => {
        document.getElementById('scraper-status').textContent = message;
    };

    const autoScroll = async () => {
        setStatus('Scrolling to load reviews...');
        let lastScrollHeight = 0, currentScrollHeight = document.documentElement.scrollHeight;
        do {
            lastScrollHeight = currentScrollHeight;
            window.scrollTo(0, currentScrollHeight);
            await new Promise(r => setTimeout(r, 2000));
            currentScrollHeight = document.documentElement.scrollHeight;
        } while(currentScrollHeight > lastScrollHeight);
        window.scrollTo(0, 0);
    };

    const expandReviews = async () => {
        setStatus('Expanding truncated reviews...');
        const moreButtons = document.querySelectorAll("button.w8nwRe.kyuRq");
        for (const button of moreButtons) {
            button.click();
            await new Promise(r => setTimeout(r, 500));
        }
    };

    const scrapeReviews = async () => {
        await autoScroll();
        await expandReviews();

        setStatus('Scraping reviews...');
        const reviewDivs = document.querySelectorAll("div[data-review-id]");
        const reviews = [];
        const scrapedReviewIds = new Set();

        for (const reviewDiv of reviewDivs) {
            const reviewId = reviewDiv.getAttribute("data-review-id");
            if (scrapedReviewIds.has(reviewId)) continue;

            const review = {
                reviewer_name: reviewDiv.querySelector("div.d4r55")?.textContent.trim() || '',
                img_url: reviewDiv.querySelector("img.NBa7we")?.src.replace('=w36-h36-p-rp-mo-br100', '=s0') || '',
                review_date: reviewDiv.querySelector("span.rsqaWe")?.textContent.trim() || '',
                star_rating: parseInt(reviewDiv.querySelector("span.kvMYJc[role='img']")?.getAttribute("aria-label").match(/(\d+)/)?.[0] || '0', 10),
                review_url: reviewDiv.querySelector("button[data-href]")?.getAttribute("data-href") || '',
                review_content: reviewDiv.querySelector("span.wiI7pd")?.textContent.trim() || ''
            };

            scrapedReviewIds.add(reviewId);
            reviews.push(review);
        }

        setStatus(`Scraped ${reviews.length} reviews.`);
        return reviews;
    };

    const copyToClipboard = async () => {
        const reviews = await scrapeReviews();
        const contentToCopy = JSON.stringify(reviews, null, 2);
        navigator.clipboard.writeText(contentToCopy).then(() => {
            setStatus('Content copied to clipboard!');
        }).catch(err => {
            setStatus('Error copying to clipboard. See console for details.');
            console.error("Could not copy content to clipboard: ", err);
        });
    };

    document.getElementById('scrape-reviews').addEventListener('click', scrapeReviews);
    document.getElementById('copy-to-clipboard').addEventListener('click', copyToClipboard);
})();