Created
January 31, 2025 16:12
-
-
Save RobinDev/09368def5c9743d48c427477d2967fac to your computer and use it in GitHub Desktop.
GoogleMaps Reviews Scrapper -
This file contains 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
/** | |
* GoogleMaps Reviews Scrapper | |
* Working with gMaps in french | |
* | |
* To execute from browser console after loading a reviews page ➜ | |
* eg : https://www.google.fr/maps/place/Tour+Eiffel/@48.8583701,2.2780018,15z/data=!4m7!3m6!1s0x47e66e2964e34e2d:0x8ddca9ee380ef7e0!8m2!3d48.8583701!4d2.2944813!15sCgt0b3VyIGVpZmZlbFoNIgt0b3VyIGVpZmZlbJIBE2hpc3RvcmljYWxfbGFuZG1hcmvgAQA!16zL20vMDJqODE?entry=tts&g_ep=EgoyMDI1MDEyOS4xIPu8ASoASAFQAw%3D%3D | |
* | |
* Note : Google will automatically prevents you to load the 2xx xxx reviews from a page. | |
* To extract last one, prefiltered the list. | |
*/ | |
const extractShareLink = false; | |
const mainWrapper = document.querySelector('div[role="main"]'); | |
const firstScrollable = findFirstScrollableElement(mainWrapper); | |
if (firstScrollable) await scrollToBottomUntilNoMoreScroll(firstScrollable); | |
console.log(await extractReviewDetails(extractShareLink)); | |
function findFirstScrollableElement(element) { | |
const elements = element.querySelectorAll("*"); | |
for (let element of elements) { | |
const style = window.getComputedStyle(element); | |
if (element.scrollHeight > element.clientHeight) { | |
return element; | |
} | |
} | |
return null; // Return null if no scrollable element is found | |
} | |
async function waitForElement(selector, timeout = 5000) { | |
const startTime = Date.now(); | |
while (Date.now() - startTime < timeout) { | |
const element = document.querySelector(selector); | |
if (element) return element; | |
await new Promise((resolve) => setTimeout(resolve, 100)); | |
} | |
return null; | |
} | |
async function scrollToBottomUntilNoMoreScroll(element, delay = 500) { | |
let lastScrollHeight = element.scrollHeight; | |
async function scrollAndCheck() { | |
// Scroll to the bottom | |
element.scrollTop = element.scrollHeight; | |
await new Promise((resolve) => setTimeout(resolve, delay)); | |
// Check if scrollHeight is the same after scrolling | |
if (element.scrollHeight === lastScrollHeight) { | |
console.log("No more scrolling possible"); | |
return; // No more scroll | |
} else { | |
lastScrollHeight = element.scrollHeight; | |
await scrollAndCheck(); // Continue scrolling | |
} | |
} | |
await scrollAndCheck(); | |
} | |
async function extractReviewDetails(extractShareLink = true) { | |
const reviewsData = []; | |
const reviews = document.querySelectorAll("div[data-review-id]"); | |
if (!reviews.length) { | |
console.log("No reviews found."); | |
return; | |
} | |
for (const reviewContainer of reviews) { | |
// Extract Name: Find the first button with data-href starting with "https://www.google.com/maps/contrib/" | |
const nameElement = reviewContainer.querySelector( | |
'[data-href^="https://www.google.com/maps/contrib/"] div' | |
); | |
const name = nameElement ? nameElement.textContent : ""; | |
console.log(!name ? "No name found !!!" : "Name:", name); | |
// Check for and click the "Plus" button if it exists | |
const expandButton = reviewContainer.querySelector( | |
'button[aria-expanded="false"]' | |
); | |
if (expandButton) { | |
expandButton.click(); | |
await new Promise((resolve) => setTimeout(resolve, 500)); // Wait for expansion | |
} | |
// Extract Review Body: Look for the span containing the review text | |
const reviewBodyElement = reviewContainer.querySelector("div[id]"); | |
const reviewBody = reviewBodyElement | |
? reviewBodyElement.textContent.trim() | |
: ""; | |
console.log( | |
!reviewBodyElement ? "reviewBody not found" : "Review Body:", | |
reviewBody | |
); | |
// Extract Rating: Count the stars inside the rating container | |
const ratingElement = reviewContainer.querySelector('span[role="img"]'); | |
const rating = ratingElement | |
? ratingElement.querySelectorAll(".hCCjke").length | |
: 0; | |
console.log(!ratingElement ? "Rating not found" : "Rating:", rating); | |
// Click on "Partager" button: Find button with aria-label containing "Partager" | |
let shareLink = ""; // Declare shareLink outside the if block | |
const shareButton = reviewContainer.querySelector( | |
'[aria-label*="Partager"]' | |
); | |
if (shareButton && extractShareLink) { | |
shareButton.click(); | |
console.log('Clicked on "Partager"'); | |
const inputElement = await waitForElement("input[readonly]"); | |
shareLink = inputElement ? inputElement.value : ""; // Assign value here | |
console.log("Share link:", shareLink); | |
const closeButton = document.querySelector('button[aria-label="Fermer"]'); | |
if (closeButton) { | |
closeButton.click(); | |
console.log("Closed the popup"); | |
} else { | |
console.log("Close button not found"); | |
} | |
} else { | |
console.log("Partager button not found"); | |
} | |
reviewsData.push({ | |
name: name, | |
reviewBody: reviewBody, | |
rating: rating, | |
source: shareLink, | |
}); | |
} | |
return reviewsData; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment