Export Google Maps Place Photos with Python
Fetch photo pages for selected places, inspect the response, and export image URLs to a CSV you can trace.
Fetch photo pages for selected places, inspect the response, and export image URLs to a CSV you can trace.
The catalog needs an entrance photo for one branch of a chain, and the other branches share its name. Before choosing an image, match the full address and keep every photo link tied to that branch's place reference.
The script below saves pages from the GMapsScraper.AI photos scraper as JSON, then exports the photoUrl field to a CSV. That field name comes from the public Photos response example on the API page; some items also carry videoUrl.
The request budget
The script has two commands. fetch makes API calls and saves the full JSON; extract reads those files locally. If the CSV ends up with the wrong field, run extract again into a new CSV without spending another request.
This script uses API request quota. Web app credits are separate: photo credits count photos, and search credits count businesses. Each API key also has a rate limit.
Each page that fetch requests is one API call, so a batch costs at most the number of places times the page cap, unless you rerun something. The GMapsScraper.AI pricing details cover the web app's credit plans.
Match the photo request to the branch
Keep Name, Fulladdress, Fid, and Google Maps URL together when making a shortlist from search results. The name helps you find the row; the address tells you which branch you're about to inspect.
The photo call takes fid or url. Copy the actual Fid or the place's Google Maps URL from that row. If all you have is a business name, run a search first and match the address before copying a reference.
The GMapsScraper.AI API guide covers POST /api/v1/photos. The body takes the place reference and page. The response contains photos, next, total, url, params, and place.
Fetch a page and keep it
Get an API key from Dashboard > Settings > Create API Key, then set it in your terminal:
export GMAPS_API_KEY='YOUR_API_KEY'
Save the complete block as export_photos.py. You'll only need Python's standard library.
import argparse
import csv
import json
import os
from pathlib import Path
from urllib.parse import urlsplit
from urllib.request import Request, urlopen
ENDPOINT = "https://gmapsscraper.ai/api/v1/photos"
def page_count(value):
number = int(value)
if number < 1:
raise argparse.ArgumentTypeError("Page numbers and caps must be at least 1")
return number
def fetch_pages(args):
key = os.environ.get("GMAPS_API_KEY", "").strip()
if not key or key == "YOUR_API_KEY":
raise ValueError("Set GMAPS_API_KEY to your real API key")
target = {"fid": args.fid} if args.fid else {"url": args.url}
if not next(iter(target.values())).strip():
raise ValueError("The place reference cannot be blank")
folder = Path(args.out)
folder.mkdir(parents=True, exist_ok=False)
for page in range(args.page, args.page + args.max_pages):
body = {**target, "page": page}
request = Request(
ENDPOINT,
data=json.dumps(body).encode("utf-8"),
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
},
method="POST",
)
with urlopen(request, timeout=60) as response:
payload = json.load(response)
path = folder / f"page-{page:04d}.json"
path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2),
encoding="utf-8",
)
if "photos" not in payload:
raise ValueError(f"Missing photos in {path}")
if not isinstance(payload.get("next"), bool):
raise ValueError(f"Photo pagination needs a boolean next: {path}")
print(f"Saved {path}; next={payload['next']}")
if not payload["next"]:
break
def extract_urls(args):
files = sorted(Path(args.folder).glob("page-*.json"))
if not files:
raise ValueError("No saved photo pages found")
rows = []
seen = set()
for path in files:
payload = json.loads(path.read_text(encoding="utf-8"))
photos = payload["photos"]
if not isinstance(photos, list):
raise ValueError("Inspect photos; adapt the local parser")
for photo in photos:
if not isinstance(photo, dict):
raise ValueError("Expected records; inspect photos first")
value = photo.get(args.url_field)
if not isinstance(value, str):
raise ValueError("URL field missing or not a string")
parts = urlsplit(value)
if parts.scheme not in ("http", "https") or not parts.netloc:
raise ValueError("Selected field is not an HTTP URL")
if value not in seen:
seen.add(value)
rows.append({"source_page": path.name,
"photo_url": value})
with open(args.csv, "x", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(
handle, fieldnames=["source_page", "photo_url"]
)
writer.writeheader()
writer.writerows(rows)
print(f"Saved {len(rows)} distinct URLs to {args.csv}")
def main():
parser = argparse.ArgumentParser()
commands = parser.add_subparsers(dest="command", required=True)
fetch = commands.add_parser("fetch")
target = fetch.add_mutually_exclusive_group(required=True)
target.add_argument("--fid")
target.add_argument("--url")
fetch.add_argument("--page", type=page_count, default=1)
fetch.add_argument("--max-pages", type=page_count, default=1)
fetch.add_argument("--out", required=True)
fetch.set_defaults(run=fetch_pages)
extract = commands.add_parser("extract")
extract.add_argument("--folder", required=True)
extract.add_argument("--url-field", required=True)
extract.add_argument("--csv", required=True)
extract.set_defaults(run=extract_urls)
args = parser.parse_args()
args.run(args)
if __name__ == "__main__":
main()
Start with a real Google Maps place URL and a new output directory:
python3 export_photos.py fetch \
--url 'YOUR_GOOGLE_MAPS_PLACE_URL' \
--max-pages 1 --out photo-check-01
Use --fid 'YOUR_FID' instead of --url if that's what your shortlist contains. Give each place its own folder and write the folder name in the business row.
Is the export finished?
The loop requests pages in order until next is false or it reaches --max-pages. Don't use total to guess a page size. If the last saved page has next: true, more pages are available and this export is partial.
To keep going, run fetch again with --page 2 and a new folder. --max-pages counts from that page. Raising only --max-pages starts back at page one, and those calls count again.
HTTP and network failures stop fetching, but earlier pages stay saved. Check the last saved page and its next value before choosing where to resume.
Find the URL field, then export locally
Open the first saved page (for example page-0001.json). Compare place and params with the business row, then look inside photos. On the public API example, each item is an object with photoUrl; a video item may also include videoUrl:
{
"photos": [
{ "photoUrl": "https://lh3.googleusercontent.com/p/AF1QipEXAMPLE" },
{
"photoUrl": "https://lh3.googleusercontent.com/gps-cs-s/AHVAweEXAMPLE",
"videoUrl": "https://lh3.googleusercontent.com/gps-cs-s/AHVAweEXAMPLE=m18"
}
],
"next": true,
"total": 20,
"url": "https://www.google.com/maps/uv?pb=!1sEXAMPLE",
"params": { "fid": "0x89b7b0fbf994f971:0xccd1567c352422cf", "page": 1 }
}
extract_urls reads photos as a list of objects and pulls one http(s) field per item. For stills, pass photoUrl. The extractor only checks that a value is an http(s) URL, so open one candidate link in a browser to confirm it's a photo of the place. If your saved page nests the link deeper or uses another key, change extract_urls before exporting.
python3 export_photos.py extract \
--folder photo-check-01 \
--url-field photoUrl \
--csv photo-check-01/photo-urls.csv
source_page and photo_url are headers this script chooses. There's no place column, and every place's first file is page-0001.json, so a CSV on its own can't say which branch a link came from. That's why the command above writes it into the place's folder. Hand over the whole folder: the CSV, the pages it was built from, and the place and params saved in each page. If the CSV has to travel alone, add a column with the Fid or URL you passed to fetch before sending it.
In an MCP client, scrape_google_maps_photos accepts the same fid or url and page, and shares REST quota, rate limits, and response shapes. Pages fetched that way don't land in your place folders, so the folder handover above doesn't cover them.
The extractor only removes exact duplicate links, so the same picture can appear twice under different URL strings; you'll catch those when you open the images. It also removes duplicates only within one extraction. Pages fetched later into another folder get their own CSV, so compare the CSVs before combining them.
Put the links back into the catalog task
Open each candidate image and ask two things: is it the right branch, and does it show the entrance? A short label like 'check entrance image' goes in a notes column, separate from the URL, so someone else can open the source and judge it. Photos can be old, so they don't prove how the place looks today.
Having the URL doesn't give you the right to reuse the image. Copyright usually sits with whoever took the photo, and on Google Maps that may be a customer rather than the business. Get the owner's permission before an image goes into the catalog or anywhere public; Google's terms apply on top of that.
Jim Miller, Growth at GMapsScraper.AI
About the author
- Growth, GMapsScraper.AI — https://gmapsscraper.ai
Comments
Join the discussionNo comments yet. Be the first to comment.
