Download every review of a place on Google Maps (yours, a competitor’s, a hotel or restaurant) to a spreadsheet: author, stars, date, full text, owner’s reply and number of photos. Free, in your browser, no API key.
Open the place, click its Reviews tab (sort by Newest or Lowest rating first if you like), then run the script.
// Responsive Muse · Google Maps Reviews Exporter · v1.0 · 2026-10-22
// Saves the reviews of any place on Google Maps (your business, a competitor, a hotel you're
// comparing) to CSV or Markdown: author, stars, date, text, the owner's reply and photos count.
// Open the place, click its Reviews tab (sort them first if you like) and run the script.
// It scrolls the list and expands long reviews. It makes no network requests of its own and
// never reads cookies, passwords or tokens.
(async () => {
const TOOL = 'Export Google Maps reviews';
const FORMAT = 'csv'; // 'csv' (Excel, Sheets) or 'md' (readable document)
const LIMIT = 500; // stop after this many reviews (0 = all; big places have thousands)
if (!/^(www\.)?google\.[a-z.]+$/.test(location.hostname) || !location.pathname.startsWith('/maps')) {
alert('Open a place on Google Maps first and click its Reviews tab:\nhttps://www.google.com/maps');
return;
}
const reviewsOnPage = () => [...document.querySelectorAll('div[data-review-id][aria-label]')];
if (!reviewsOnPage().length) {
alert('Click the Reviews tab of the place first, then run the script again.');
return;
}
// A small box on the page shows progress and asks before changing anything,
// so you never need to read or edit the console.
const many = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`; // 1 video, 2 videos
const panel = (() => {
const box = document.createElement('div');
box.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
const head = document.createElement('strong');
head.textContent = TOOL;
const msg = document.createElement('p');
msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
const row = document.createElement('div');
row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
box.append(head, msg, row);
document.body.append(box);
const button = (label, primary, onClick) => {
const b = document.createElement('button');
b.textContent = label;
b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
(primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
b.onclick = onClick;
row.append(b);
};
return {
text(t) { msg.textContent = t; },
ask(t, yes) {
msg.textContent = t;
row.replaceChildren();
return new Promise(r => {
button('Cancel', false, () => { row.replaceChildren(); r(false); });
button(yes, true, () => { row.replaceChildren(); r(true); });
});
},
stoppable(onStop) { row.replaceChildren(); button('Stop', false, onStop); },
actions(t, list) {
msg.textContent = t;
row.replaceChildren();
list.forEach(([label, primary, run]) => button(label, primary, run));
},
finish(t) { this.actions(t, [['Close', true, () => box.remove()]]); },
close() { box.remove(); },
};
})();
// Offer the result as a downloaded file or as clipboard text (a real click, so it always works)
const deliver = (summary, body, file) => {
const save = () => {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([/\.csv$/.test(file) ? '\ufeff' + body : body], { type: 'text/plain;charset=utf-8' }));
a.download = file;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
panel.text(`${summary}\nSaved as ${file} (check your Downloads).`);
};
const copy = () => navigator.clipboard.writeText(body)
.then(() => panel.text(`${summary}\nCopied. Paste it into a spreadsheet or a note.`))
.catch(() => panel.text(`${summary}\nThe browser blocked the clipboard: use Download instead.`));
panel.actions(summary, [['Close', false, () => panel.close()], ['Copy', false, copy], ['Download', true, save]]);
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
const reviews = new Map();
// The reviews live in a scrolling side panel: find it from the first review
let scroller = reviewsOnPage()[0];
while (scroller && !(scroller.scrollHeight > scroller.clientHeight + 50 && /auto|scroll/.test(getComputedStyle(scroller).overflowY))) {
scroller = scroller.parentElement;
}
scroller = scroller || document.scrollingElement;
// Read the reviews currently loaded (long ones are expanded first)
const MORE = 'button[jsaction*="review.expandReview"][aria-expanded="false"], ' +
'button[jsaction*="review.expandOwnerResponse"][aria-expanded="false"]';
const waited = new Set();
const collect = async () => {
const more = [...document.querySelectorAll(MORE)];
more.forEach(b => b.click());
if (more.length) await sleep(300);
for (const el of reviewsOnPage()) {
const id = el.dataset.reviewId;
if (reviews.has(id)) continue;
// Loaded after the clicks above: read it on the next pass, once expanded
if (el.querySelector(MORE) && !waited.has(id)) { waited.add(id); continue; }
// Review text and the owner's reply (inside .CDe7pd) use the same text class
const text = [...el.querySelectorAll('.wiI7pd')].filter(t => !t.closest('.CDe7pd'))
.map(t => t.innerText.trim()).filter(Boolean).join('\n');
const reply = (el.querySelector('.CDe7pd .wiI7pd')?.innerText || '').trim();
// Stars: an image labelled "5 stars"; hotels also show scores from other sites ("10/10")
const label = el.querySelector('[role="img"][aria-label]')?.getAttribute('aria-label');
const stars = label ? (label.match(/\d+([.,]\d+)?/) || [''])[0] : (el.querySelector('.fzvQIb')?.textContent || '').trim();
reviews.set(id, {
author: el.getAttribute('aria-label') || '',
stars,
date: (el.querySelector('.rsqaWe, .xRkPPb')?.textContent || '').trim(),
text,
reply,
photos: el.querySelectorAll('button[jsaction*="openPhoto"], button.Tya61d').length,
});
if (LIMIT && reviews.size >= LIMIT) return;
}
};
// 1. Scroll the panel, collecting as we go, until nothing new appears five times in a row
let stop = false;
panel.stoppable(() => { stop = true; });
let idle = 0;
while (idle < 5 && !stop && !(LIMIT && reviews.size >= LIMIT)) {
const before = reviews.size;
await collect();
scroller.scrollTo(0, scroller.scrollHeight);
await sleep(1500);
idle = reviews.size === before ? idle + 1 : 0;
panel.text(`Collecting reviews… ${reviews.size}${LIMIT ? ' of up to ' + LIMIT : ''}`);
}
const list = [...reviews.values()];
if (!list.length) {
panel.finish('No reviews found. Google Maps may have changed its layout: please report it on responsive-muse.com.');
return 0;
}
// 2. Build the file and offer it
const place = document.title.replace(/ [-–] Google Maps$/, '').trim() || 'place';
const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
const body = FORMAT === 'md'
? `# Reviews of ${place}\n\n` + list.map(r => `**${r.author}** · ${r.stars}★ · ${r.date}\n\n${r.text}` +
(r.reply ? `\n\n> Owner: ${r.reply.replace(/\n/g, '\n> ')}` : '')).join('\n\n---\n\n')
: ['Author,Stars,Date,Review,Owner reply,Photos', ...list.map(r =>
[r.author, r.stars, r.date, r.text, r.reply, r.photos].map(cell).join(','))].join('\n');
const name = ('reviews-' + place).replace(/[\\/:*?"<>|]+/g, '_');
deliver(`${stop ? 'Stopped early. ' : ''}Found ${many(list.length, 'review')}.`, body, `${name}.${FORMAT}`);
return list.length;
})();
Bookmarklet: drag the middle button to your bookmarks bar, open www.google.com and click the bookmark. It runs the same script with the options above.
- Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is www.google.com itself loading your list.
- Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
- Only runs on www.google.com. Anywhere else it stops before doing anything.
- Short and readable. 151 commented lines, not minified or obfuscated.
- Shows its progress on the page. A small box tells you what it's doing and offers Download or Copy at the end.
- Same code for everyone. SHA-256 of the default version:
ca636b2ef4db613a…
Verify it yourself in 30 seconds
- Read it: every line is above, with comments.
- Search the code for
fetch,XMLHttpRequest,cookie,localStorageoreval. None of them appear. - Open the Network tab before running it: you'll only see requests to www.google.com.
How to use it
- Open the place on Google Maps and click its Reviews tab. Use Sort first if you want the newest or the lowest-rated reviews at the top.
- Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type
allow pastingand press Enter. - Pick a format and a limit, copy the script, paste it in the console and press Enter. It scrolls the reviews, expands the long ones and shows a box: click Download (or Copy).
What you get
One row per review: Author, Stars, Date, Review, Owner reply, Photos. Hotels also show scores imported from other sites, which are kept as they appear (for example 10/10). The Markdown format gives you a readable document to share or to paste into an AI chat for a summary.
Why not the Google API?
The Places API only returns the five “most relevant” reviews of a place, and paid scrapers charge per review. This script reads the same list you scroll on screen, so it gets all of them, at the speed Google Maps loads them.
FAQ
Can I export the reviews of any business, not just mine?
Yes. Reviews on Google Maps are public, so the script works on any place you can open: your business, a competitor or a hotel you're comparing.
How many reviews can it export?
As many as Google Maps loads, about 10 every second or two. The default stops at 500 so big places don't take forever; set the limit to 0 to get all of them and keep the tab open.
Does it include the owner's replies?
Yes. Long reviews and replies are expanded first, so the text is complete.
Why are the dates like "a month ago"?
That is the only date Google Maps shows on the page. Sort by Newest if you need the most recent ones first.
Do I need to be signed in?
Usually not, but if Google shows a "limited view" of Maps without the Reviews tab, sign in to your Google account and try again.
More free tools
YouTube
8 tools- Clear Watch Later
- Unsubscribe from all channels
- Unlike all videos
- Export Watch Later
- Export saved posts
- Delete all comments
TikTok
1 tools- Export profile videos
Amazon
2 tools- Export order history
- Export wishlist
X / Twitter
3 tools- Export bookmarks
- Unlike all posts
- Remove all bookmarks
AI chats
4 tools- Export ChatGPT chat
- Export Claude chat
- Export Gemini chat
- Export Perplexity thread
Google Maps
1 tools- Export Google Maps reviews
Steam
1 tools- Export Steam wishlist
Apple Music
1 tools- Export Apple Music playlist
