Disclosure: I built the reviews scraper used in this post and earn money when people run it. The pandas part works with reviews from any source.
App reviews are the cheapest user research you'll ever get. The problem is getting them out of the stores:
- App Store Connect and Google Play Console only show reviews for apps you own, and exporting them is clunky.
- Competitors' reviews are only on the public store pages.
- Apple and Google have different formats, so comparing iPhone and Android users means two pipelines.
This post pulls 100 recent Spotify reviews from both stores into one Excel file, tags the 1-2 star ones with complaint themes, and pivots them by store. It takes about a minute and costs about 3 cents.
Free options first
-
Google Play only: the open-source
google-play-scraperPython package works well and is free. -
App Store only: Apple has a public RSS/JSON feed of recent reviews per country (
https://itunes.apple.com/us/rss/customerreviews/id=<APP_ID>/sortby=mostrecent/json). It returns at most 500 reviews per country, 50 per page, and some pages come back empty, so you need retries.
If you only need one store and you're happy to maintain the code, those are fine. I wanted both stores, lookup by app name, and a per-app summary in one call, so I packaged it as a hosted scraper on Apify.
Setup
pip install apify-client pandas openpyxl
export APIFY_TOKEN=your_token_here # Apify Console > Settings > API & Integrations
The code
import os
import pandas as pd
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run_input = {
"apps": ["Spotify"], # a name, store URL, App Store ID or package name
"stores": ["apple", "google"],
"countries": ["us"],
"maxReviewsPerApp": 50, # per store and country -> 100 reviews
"sort": "newest",
}
run = client.actor("zahidthani/app-store-google-play-reviews").call(
run_input=run_input, max_total_charge_usd=0.10
)
# apify-client 3.x returns a Run object; 1.x and 2.x return a dict
dataset_id = getattr(run, "default_dataset_id", None) or run["defaultDatasetId"]
items = list(client.dataset(dataset_id).iterate_items())
reviews = pd.DataFrame([i for i in items if i["type"] == "review"])
summaries = [i for i in items if i["type"] == "appSummary"]
print(len(reviews), "reviews;", reviews.groupby("store")["rating"].agg(["count", "mean"]).round(2).to_dict("index"))
# 1. Tag 1-2 star reviews with complaint themes (simple keyword rules you can edit)
THEMES = {
"Ads": r"\bads?\b|\badds\b|advert|commercial",
"Price / Premium": r"premium|subscription|price|pay|paid|money|expensive",
"Bugs / crashes": r"crash|bug|glitch|freez|\blag|delay|broken|not working|doesn.t work",
"Shuffle / playback": r"shuffle|skip|playback|repeat|queue|stops? playing",
"UI / update": r"update|interface|\bui\b|layout|design|redesign",
"AI / recommendations": r"\bai\b|\bdj\b|recommend|algorithm",
}
neg = reviews[reviews["rating"] <= 2].copy()
blob = (neg["title"].fillna("") + " " + neg["text"].fillna("")).str.lower()
for theme, pattern in THEMES.items():
neg[theme] = blob.str.contains(pattern, regex=True)
pivot = neg.groupby("store")[list(THEMES)].sum().T
pivot["total"] = pivot.sum(axis=1)
pivot = pivot.sort_values("total", ascending=False)
print(f"\n{len(neg)} reviews with 1-2 stars. Complaint themes by store:")
print(pivot)
# 2. One Excel file: all reviews + tagged complaints + the pivot
cols = ["store", "rating", "date", "appVersion", "title", "text", "thumbsUp", "replyText"]
with pd.ExcelWriter("spotify_reviews.xlsx") as xl:
reviews[cols].to_excel(xl, sheet_name="reviews", index=False)
neg[cols + list(THEMES)].to_excel(xl, sheet_name="complaints", index=False)
pivot.to_excel(xl, sheet_name="complaint themes")
# 3. The Actor's own summary per store (rating, complaint themes, phrases)
for s in summaries:
themes = ", ".join(f'{t["theme"]} ({t["reviews"]})' for t in s["complaintThemes"][:3])
print(f'\n{s["store"]}: store rating {s["rating"]:.2f} from {s["ratingsCount"]:,} ratings; '
f'{s["negativeShare"]:.0%} of fetched reviews are 1-2 stars')
print(" top complaint themes:", themes)
print(" complaint phrases:", ", ".join(p["phrase"] for p in s["topComplaintPhrases"][:5]))
Tested with Python 3.12, apify-client 3.2.1 and pandas 2.3.
Real output
From a run on 6 October 2026. 100 reviews came back (50 per store). The average rating of these recent reviews was 4.24 on iPhone and 3.88 on Android, and 21 of the 100 had 1-2 stars.
The pivot of those 21 complaints (a review can match several themes):
| Complaint theme | App Store | Google Play | Total |
|---|---|---|---|
| Price / Premium | 2 | 7 | 9 |
| Ads | 2 | 6 | 8 |
| Shuffle / playback | 0 | 6 | 6 |
| Bugs / crashes | 0 | 4 | 4 |
| AI / recommendations | 1 | 1 | 2 |
| UI / update | 0 | 1 | 1 |
The per-store summary the scraper adds to every run:
| Store | Store rating | Ratings | 1-2★ share of fetched | Top complaint themes |
|---|---|---|---|---|
| App Store | 4.77 | 42.4M | 14% | Price & subscription (2), Ads (1), Login & account (1) |
| Google Play | 4.35 | 36.4M | 28% | Price & subscription (7), Ads (6), Bugs & crashes (3) |
Reading the actual complaint rows tells the story the numbers hint at: free-tier users on Android are unhappy about ad frequency and length, skip limits and forced shuffle, and several mention switching to YouTube Music. One paid user reports the app freezing during playback. You'd want more than 100 reviews before making a product decision, but that's the shape of it.
The Excel file has three sheets: reviews (all 100), complaints (the 21 negative ones with a True/False column per theme, easy to filter) and complaint themes (the pivot above).
Adapting it
-
Your own app, weekly: add
"reviewsSince": "7 days"and run it on a schedule. You only pay for new reviews. -
Complaints only:
"maxRating": 2skips the 3-5 star reviews before you're charged for them. -
Competitors side by side:
"apps": ["Spotify", "com.google.android.apps.youtube.music", "Pandora"]. Every row carriesappNameandstore, sopivot_table(index="appName", columns="store", ...)works directly. -
Better tagging: keyword rules are crude but transparent. Edit
THEMESfor your app's vocabulary, or send thecomplaintssheet to an LLM for classification once you know which themes matter.
What it costs
$0.12 per 1,000 reviews + $0.005 per app summary + $0.005 per run. This example: 100 reviews + 2 summaries + 1 run = about $0.03. 10,000 Google Play reviews of one app is about $1.21. Apify's free plan includes $5 of credit a month. max_total_charge_usd caps any single run.
Limitations
-
Apple's feed is capped at 500 most-recent reviews per country. Add countries (
gb,ca,au...) for more. Google Play has no fixed cap. - Apple's feed lags. In this run the newest App Store reviews were about two days old, while Google Play's were minutes old.
-
50 "newest" reviews of a huge app is a few hours of reviews. For a popular app, use
reviewsSinceand a largermaxReviewsPerApp. - Small samples make phrase lists noisy. The App Store summary's top complaint phrases here were generic words ("music", "songs"); with 7 negative reviews there's not much to work with.
- Name lookup takes the top store match. Paste the store URL or package name when the name is ambiguous.
- Apple rows have no developer replies or helpful votes (the feed doesn't include them).
- Reviews include public display names. If you store them, treat them as personal data (GDPR/CCPA). The script above doesn't export them.
Try it
App Store & Google Play Reviews Scraper on the Apify Store: https://apify.com/zahidthani/app-store-google-play-reviews
Runnable Python, Node.js and n8n examples: https://github.com/zahidthani/apify-data-tools-examples
Top comments (0)