~ / guides / How to Scrape App Stores (Python Guide)

How to Scrape App Stores (Python Guide)

MR
Marcus Reed
Founder & lead tester · about the author
the short version
  • Google Play app pages serve a JSON-LD block in the static HTML, so requests + BeautifulSoup parses name, category, rating, and install count without a browser. I confirmed this on June 12, 2026: HTTP 200, WhatsApp at 4.66 from 236.7M ratings.
  • Apple App Store HTML is the opposite: the visible page ships a useless Organization stub in JSON-LD and loads real data over JavaScript. The fix is Apple's own iTunes Lookup and Search API, public JSON, which I tested: WhatsApp at 4.69 from 18.06M ratings.
  • Both stores rate-limit a single IP fast and Play swaps field positions without notice, so DIY breaks at volume.
  • For search results, top charts, and reviews at scale, use a scraper API like ChocoData that rotates IPs and returns JSON.

App store data (ratings, install counts, category rank, reviews) drives ASO tools, competitor tracking, and market research. This guide builds a real Python scraper for both Google Play and the Apple App Store, tests it against live apps, and shows exactly where a plain requests call works and where it returns nothing. I ran the code on June 12, 2026 and pasted the real output below. The headline: the two stores behave in opposite ways, and knowing which is which saves you from shipping a parser that returns an empty stub. For the fundamentals, see my web scraping guide and the Python walkthrough.

You can fetch public app-listing data with a script, and the legality depends on the method and what you keep. Both store fronts serve public pages that anyone can load without logging in, which sits on the safer side of US computer-access law. In hiQ Labs v. LinkedIn, the Ninth Circuit held that scraping public data does not violate the Computer Fraud and Abuse Act, and the Supreme Court’s Van Buren decision narrowed the CFAA toward traditional hacking. Querying a public app page with a bot is not, by itself, a federal computer crime.

Contract law is the live risk. Google’s Play Terms of Service and Apple’s Media Services Terms restrict automated access. hiQ is the cautionary tale: it won the CFAA question and still paid a $500,000 judgment for breaching LinkedIn’s user agreement. Three rules keep you on the lower-risk path: stay on public listings, never script a logged-in account, and treat app and developer metadata as your target rather than scraping personal data out of review text, which keeps you clear of the GDPR. I am a tester, not a lawyer, so get advice before any commercial scrape. My scraping legality coverage goes deeper.

One more honest note specific to Apple. The robots.txt at apps.apple.com disallows */search?* and /v1/*, so the web search path is off-limits to compliant crawlers. The iTunes Search and Lookup API I use below is a separate, documented JSON service that Apple publishes for affiliates, which is why it is the right tool rather than scraping the web search page.

What app data can you extract?

A store listing carries most of the commercially useful fields, but they live in different places per store. Knowing the structure tells you what to target and what an API should hand back as JSON.

FieldGoogle Play (JSON-LD in HTML)Apple (iTunes Lookup API)
App nameYesYes
Developer / sellerYesYes
CategoryYesYes
Average ratingYesYes
Rating countYesYes
PriceYesYes
Install countYesNo (Apple hides it)
VersionStatic HTML, separate nodeYes
App sizeNoYes (fileSizeBytes)
DescriptionYesYes (description)
ScreenshotsStatic HTMLYes (screenshotUrls)
Full review textSeparate endpointSeparate RSS feed

The split that matters: Google Play exposes installs but no clean file size, and Apple exposes file size and version cleanly but never publishes install counts. Reviews are a separate fetch on both stores.

How do app stores block scrapers?

The two stores fail a naive scraper in opposite ways, and that shapes the whole approach. Here is what I hit during testing.

StoreWhat a plain GET returnsReal blocker
Google PlayHTTP 200, full HTML with JSON-LDIP rate-limits and shifting field positions at volume
Apple App Store (web)HTTP 200, but JSON-LD is an empty Organization stubReal data is JavaScript-rendered, not in the HTML
iTunes APIHTTP 200, clean JSONUndocumented per-IP rate cap, region-locked by country

Google Play hands you usable structured data on the first request, so the block there is purely volume: hammer it from one IP and you get throttled, and Google reshuffles the JSON-LD and embedded arrays without notice. Apple’s web page looks like it works (you get a 200 and a JSON-LD block) and then the block is the data itself: the listing hydrates over JavaScript, so BeautifulSoup parses a shell. I proved both below.

Method 1: DIY scraper for Google Play (Python)

For Google Play, requests + BeautifulSoup works because the app page embeds a application/ld+json block with the core fields. I ran this on June 12, 2026 and pasted the real output.

import requests, json
from bs4 import BeautifulSoup

UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
      "(KHTML, like Gecko) Chrome/126.0 Safari/537.36")
headers = {"User-Agent": UA, "Accept-Language": "en-US,en;q=0.9"}

def scrape_play(app_id, country="US"):
    url = f"https://play.google.com/store/apps/details?id={app_id}&hl=en&gl={country}"
    r = requests.get(url, headers=headers, timeout=30)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    ld = json.loads(soup.find("script", type="application/ld+json").string)
    agg = ld.get("aggregateRating", {})
    offers = ld.get("offers")
    if isinstance(offers, list):
        offers = offers[0]
    return {
        "name": ld.get("name"),
        "developer": (ld.get("author") or {}).get("name"),
        "category": ld.get("applicationCategory"),
        "rating": round(float(agg.get("ratingValue", 0)), 3),
        "rating_count": agg.get("ratingCount"),
        "price": (offers or {}).get("price"),
        "currency": (offers or {}).get("priceCurrency"),
    }

for app_id in ["com.whatsapp", "com.spotify.music"]:
    print(scrape_play(app_id))

Real output:

{'name': 'WhatsApp Messenger', 'developer': None, 'category': 'COMMUNICATION', 'rating': 4.663, 'rating_count': 236742590, 'price': '0', 'currency': 'USD'}
{'name': 'Spotify: Music and Podcasts', 'developer': 'Spotify AB', 'category': None, 'rating': 4.335, 'rating_count': 35747376, 'price': None, 'currency': None}

Two things in that real output prove the brittleness I warned about. WhatsApp returns a category but no developer in JSON-LD; Spotify returns a developer but no category and no price. The same parser, two apps, different gaps. Google does not guarantee any field is present in the JSON-LD, so production code has to fall back to the embedded AF_dataServiceRequests arrays for the missing pieces, and those positions move. The install count also lives in a separate node, not JSON-LD, so I keep that fetch separate.

Method 1b: DIY scraper for the Apple App Store

For Apple, do not parse the web page; call the official iTunes API. I tested the web HTML first to show why, then switched to the API.

import requests, json
from bs4 import BeautifulSoup

UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
      "(KHTML, like Gecko) Chrome/126.0 Safari/537.36")
headers = {"User-Agent": UA}

# 1) The web page: JSON-LD is a useless stub
html = requests.get("https://apps.apple.com/us/app/whatsapp-messenger/id310633997",
                    headers=headers, timeout=30)
ld = json.loads(BeautifulSoup(html.text, "html.parser")
                .find("script", type="application/ld+json").string)
print("WEB JSON-LD @type:", ld.get("@type"), "| name:", ld.get("name"))

# 2) The iTunes Lookup API: clean JSON by app ID
look = requests.get("https://itunes.apple.com/lookup",
                    params={"id": "310633997", "country": "us"},
                    headers=headers, timeout=30).json()["results"][0]
print({
    "name": look["trackName"],
    "seller": look["sellerName"],
    "rating": round(look["averageUserRating"], 3),
    "rating_count": look["userRatingCount"],
    "price": look["formattedPrice"],
    "genre": look["primaryGenreName"],
    "version": look["version"],
    "size_mb": round(look["fileSizeBytes"] / 1048576, 1),
})

Real output:

WEB JSON-LD @type: Organization | name: App Store
{'name': 'WhatsApp Messenger', 'seller': 'WhatsApp Inc.', 'rating': 4.687, 'rating_count': 18064538, 'price': 'Free', 'genre': 'Social Networking', 'version': '26.22.76', 'size_mb': 353.1}

The first line is the proof: scraping the Apple web page gives you @type: Organization and name: App Store, the site’s own header, not the app. The real app data only comes from the API call. The same itunes.apple.com/search endpoint does keyword search and returns resultCount plus an array of apps, so you can build a full ASO crawler from those two documented calls without ever touching the HTML.

Method 2: scraper API for search, charts, and scale

The moment you need search rankings, top charts, or review volume across many apps, a scraper API replaces the brittle parts. ChocoData is the service I use for this category. It exposes a universal endpoint (one URL for any site) plus 453 dedicated endpoints for common targets. For a Google Play app or search page, the universal endpoint takes the full URL and returns the rendered page or parsed JSON:

import requests

API_KEY = "cd_live_YOUR_KEY"  # free tier: 1,000 requests/month, no card
target = "https://play.google.com/store/apps/details?id=com.whatsapp&hl=en&gl=US"

endpoint = "https://api.chocodata.com/api/v1/universal/get"
params = {
    "api_key": API_KEY,
    "url": target,
    "parse": "html",   # rendered HTML back, then parse JSON-LD as in Method 1
    "country": "us",   # US IP so region-gated ratings and price resolve
}

resp = requests.get(endpoint, params=params, timeout=90)
print("STATUS:", resp.status_code)
# Parse the same application/ld+json selector as Method 1, now on a rotated IP.

The difference from Method 1 is what happens behind that one call. ChocoData fetches through a country-matched residential IP that rotates automatically and retries through a fresh IP when a block appears, and it can run JavaScript rendering for the dynamic parts. That rotation is what lets you loop through hundreds of app IDs or paginate a category chart without the single-IP throttle I would hit running Method 1 in a tight loop. Billing applies only to successful (2xx) responses, so a throttled empty response does not quietly drain budget.

I did not paste live API output here because that requires a paid key, and I will not fake numbers. The endpoint path, parameters, and free-tier limits above come from the ChocoData docs. The honest claim is narrow: the universal endpoint takes a store URL and returns the page through a rotated IP, which removes the single-IP rate limit that breaks Method 1 at volume.

Should you scrape app stores yourself or use an API?

Use DIY for a handful of known apps; use a scraper API the moment you need search rankings, charts, or reviews at scale. The decision comes down to volume and which surface you need.

ScenarioDIY (Python)Scraper API
A few known apps, core fieldsWorks, free (Play JSON-LD, iTunes API)Overkill
Apple app detail by ID or keywordiTunes API, clean and freeOverkill
Google Play keyword search resultsInternal endpoint, brittleReturns ranked JSON
Top charts by categoryHard, JS-renderedBuilt for it
Thousands of apps, scheduledSingle-IP throttle stops youRotates IPs
Full review corpusSeparate paginated endpoint per storeHandles pagination

My rule after testing both stores: for Apple, the free iTunes Lookup and Search API covers detail and keyword search cleanly, so DIY wins until you need charts. For Google Play, the JSON-LD parse is genuinely useful for known app IDs, but search results, charts, and review pagination push you to an API fast because Google reshuffles the internal arrays and throttles a single IP. Start with the free code above, measure how many apps you actually need, and switch to a scraper API when the IP block or the moving JSON becomes your bottleneck. For the broader toolkit, see my Beautiful Soup and Scrapy guides.

FAQ

Is there an official Google Play data API?

No. Google publishes no public read API for Play Store listings. The Play Developer API only manages apps you own. Third parties read the public store pages, which is why the JSON-LD parsing approach below exists.

Can I scrape app reviews the same way?

Partly. Google Play reviews load through a separate batchexecute endpoint that returns nested arrays, not the listing HTML. Apple reviews come from a public RSS feed (itunes.apple.com/.../customerreviews) capped at roughly 500 entries. Neither shows in the app-detail parse.

What is the difference between scraping by app ID and by keyword?

App ID gives you one known app's full detail. Keyword search returns a ranked list of app IDs for a query, which you then loop through for detail. The iTunes Search API does keyword search directly; Google Play needs its internal search endpoint or a scraper API.

MR
Marcus Reed
I've built and run web scrapers for the better part of a decade. On this site I put scraper APIs and scraping tools through real jobs against real targets, then write up what actually holds up.