๐Ÿ“ฑ App Store Scraper

Architecture & Data-Flow Reference

Google Play + Apple App Store ยท discovery โ†’ scraping โ†’ combined CSV ยท scaled across 2 PCs

Self-contained document โ€” open in any browser

0Big picture

There is no "all apps" endpoint in either store. So the system works in two phases: first it discovers a big list of app IDs, then it scrapes the details for every app on that list into one CSV. Everything is free โ€” no API keys, no paid services.

2
stores covered
(Play + App Store)
~30
apps / Google Play query
(hard server cap)
~180
apps / iTunes search term
(the volume engine)
100k+
target catalog size
(App Store driven)
Two verbs, two phases
FIND apps โ†’ discover.py writes apps.json. GET data โ†’ scraper.py reads apps.json and writes output/app_data.csv.

1The pipeline

End-to-end, data flows left to right. The same flow runs on each PC; only the final merge is shared.

discover.py
FIND apps
โ†’
apps.json
list of app IDs
โ†’
scraper.py
GET data
โ†’
app_data.csv
one row / app / country
โ†’
merge_csv.py
combine 2 PCs
process (script) data file output
StepScriptReadsWritesPurpose
Finddiscover.pysearch APIs + chartsapps.jsonbuild a big list of app IDs
Getscraper.pyapps.jsonoutput/app_data.csvfetch metadata + ratings for each app
Mergemerge_csv.pyshard CSVsoutput/app_data.csvcombine the two PCs' results, de-duped

2Files in the project

FileRole
discover.pyPhase 1 Finds app IDs and fills apps.json
scraper.pyPhase 2 Fetches per-app data into the CSV (threaded, shardable, resumable)
netutil.pyShared safety Error classification + adaptive throttle / circuit breaker
merge_csv.pyHelper Merges the two PCs' shard CSVs into one
apps.jsonData Settings + the list of app IDs (the contract between the two phases)
english_words.txtData ~9,868 common words used as deep search terms (path to 100k)
requirements.txtDependencies: google-play-scraper, requests
output/app_data.csvThe final dataset

3Phase 1 โ€” Find apps (discover.py)

Because no full catalog exists, discovery queries many sources with many terms and keeps only the unique app IDs. Three sources feed one de-duplicated set per store.

The three discovery sources

SourceStoreHowYield / callRole
Keyword searchGoogle Playgoogle_play_scraper.search(term)~30 (hard cap)breadth via many terms
iTunes Search APIApp Storeitunes.apple.com/search?term=~180main volume engine
RSS top chartsApp Store.../rss/{feed}/genre={id} (free/grossing/paid ร— 23 genres)up to 200popular apps, fast

How the search terms are built

The function build_terms() assembles the query list from up to four tiers:

~155 curated
topic keywords
+
aโ€“z
26 letters
+
aaโ€“zz
--deep (676)
+
wordlist
--terms-file (~9,868)
=
~9,943 terms
de-duped, ordered

More terms = more apps. The wordlist (--terms-file english_words.txt) is the single biggest lever toward 100k. --deeper (aaaโ€“zzz, 17,576 terms) exists but is very slow.

Discovery flow (per store)

  1. Build the term list and resolve the country list (from --countries or settings).
  2. For each country, loop every term; call the source through the safety layer (retry + backoff).
  3. Collect each result's app ID into a seen dictionary โ€” duplicates are ignored automatically.
  4. Print live progress: total unique = N / target.
  5. Stop as soon as --target is reached (or terms are exhausted).
  6. Merge into the existing apps.json (keeps your settings + anything already listed), then write the file.
Why Google Play can't reach 100k
Play search returns only ~30 apps per query no matter what โ€” that's Google's cap, not the code's. So Play contributes ~15โ€“30k; the App Store's ~180/term search is what carries you to 100k.
De-duplication is automatic and idempotent
Discovery keys on app ID, and merge() drops duplicates against what's already in apps.json. Re-running only adds new apps โ€” it never loses or doubles existing ones. If apps.json is missing, a fresh one is created automatically.

4apps.json โ€” the app list

This file is the contract between the two phases: discovery writes it, scraping reads it.

{
  "settings": {
    "country": "us",                          // default single country
    "countries": ["us","gb","in","ca","au"],  // scrape/discover across these
    "lang": "en",                             // language (Google Play)
    "review_count": 100,
    "delay_seconds": 1.0                       // polite pause; rate โ‰ˆ workers / delay
  },
  "google_play": [
    { "app_id": "com.whatsapp" }              // the id= value from the Play URL (string)
  ],
  "app_store": [
    { "app_id": 310633997, "name": "WhatsApp" } // numeric id from the App Store URL
  ]
}
StoreID typeWhere it comes from
Google Playstringplay.google.com/store/apps/details?id=com.whatsapp
App Storenumberapps.apple.com/us/app/.../id310633997

5Phase 2 โ€” Get data (scraper.py)

Scraping turns the ID list into real data. For every app ร— every country it makes one "detail" call and writes one CSV row. Two stores, two API calls, one unified row schema.

The two detail calls

Google Play

google_play_scraper.app(app_id, lang, country)

Scrapes the public listing page โ†’ title, developer, genre, price, score, ratings count, review count, last-updated, version, URL.

Apple App Store

itunes.apple.com/lookup?id=&country=

Official lookup API โ†’ trackName, seller, genre, price, average rating, rating count, release date, version, URL.

Output columns โ€” output/app_data.csv

ColumnMeaning
storegoogle_play or app_store
countrywhich region this row was fetched for
app_id, title, developer, categoryidentity
price, currency, freepricing
avg_ratingfinal average star rating
total_ratingstotal number of ratings
text_review_countnumber of written reviews (Play only)
last_updateddate the app was last updated
versionlatest version
urlstore listing link

CSV is UTF-8 with BOM โ†’ opens cleanly in Excel. The same columns apply to both stores.

How one job is processed

job
(store, id, country)
โ†’
throttle.wait()
honor cooldown
โ†’
fetch
Play / App Store
โ†’
row
flushed to CSV

6Safety layer (netutil.py)

The #1 rule: slow is fine โ€” getting the IP blocked is not.
Every request (in both scripts) passes through a shared throttle. The moment a store pushes back, all workers slow down together instead of hammering until blocked.

Error classification

SignalMeaningReaction
429 / 403 "too many requests", "quota"rate-limitedTrip circuit breaker, wait it out, retry generously
408 / 5xx, timeout, connectiontransient hiccupshort exponential backoff, a few retries
404 / not-foundhealthy answerskip immediately โ€” no wasted retries

The circuit breaker (a shared state machine)

RUNNING
requests flow freely
โ†’
TRIPPED
all workers pause
โ†’
COOLDOWN
30โ†’60โ†’120โ€ฆโ†’300s
โ†’
RECOVER
speed back up on success
Reading the logs
A line like THROTTLING: pausing all workers ~60s is the protection working, not an error. If it fires constantly, that IP is at its limit โ†’ lower --workers on that machine.

7Concurrency โ€” why threads

These are network-wait tasks (most time is spent waiting for the server, not computing), which is the ideal case for a thread pool. With --workers N, up to N requests are in flight at once.

job queue
all apps ร— countries
โ†’
worker 1
worker 2
โ€ฆ worker N
โ†’
CSV sink
thread-safe, flush per row

Effective request rate โ‰ˆ workers รท delay_seconds. The throttle sits above all workers, so one rate-limit signal pauses the whole pool.

8Two-PC architecture (2 CPUs, 2 IPs)

Two machines with separate IPs double throughput and the safe request budget (each store rate-limits per IP). You don't split the file by hand โ€” both PCs read the same apps.json and each takes a different shard.

How sharding splits the work

All jobs are flattened to a list, then each PC takes jobs[shard::shards] โ€” i.e. every Nth job. This interleaves Play/App-Store/country evenly, so neither PC gets a lopsided slice.

apps.json
jobs: 0,1,2,3,4,5,6,7โ€ฆ

PC #1 โ€” IP A

--shard 0 --shards 2

does jobs 0, 2, 4, 6 โ€ฆ

writes app_data_shard0of2.csv

PC #2 โ€” IP B

--shard 1 --shards 2

does jobs 1, 3, 5, 7 โ€ฆ

writes app_data_shard1of2.csv

shard0 CSV
โ†˜
merge_csv.py
de-duped by store+country+id
โ†™
shard1 CSV
โ†“
app_data.csv
complete combined dataset

The 3 steps end-to-end

  1. Discover once (either PC): run discover.py, then copy the resulting apps.json to both machines.
  2. Scrape in parallel: PC #1 runs shard 0, PC #2 runs shard 1 (each with its own IP and its own thread pool).
  3. Merge: copy both shard CSVs into one output/ folder and run merge_csv.py.
รท2
work per PC
(sharding)
ร—~10
per PC
(thread pool)
ร—2 IPs
safe rate budget
(per-IP limits)
28h โ†’ ~1h
rough wall-clock
for 100k records

9Resume & crash safety

A 100k run can't depend on a single perfect execution. Two mechanisms make interruptions harmless:

๐Ÿ’พ Flush-as-you-go

Each row is written and flushed to the CSV the instant it's fetched. A crash, block, or Ctrl-C never loses completed work.

โฏ๏ธ Resume on restart

On startup the scraper reads the existing CSV and skips apps already done (keyed by store + country + id). Just re-run the same command to continue.

Practical meaning
If a PC is interrupted at app 40,000 of 50,000, re-running the exact same command picks up at 40,001. Use --no-resume only if you deliberately want to re-scrape everything.

10Configuration & flags

discover.py

FlagDefaultPurpose
--target N0 (all terms)stop each store after N unique apps
--terms-file PATHโ€”extra search terms (best lever to 100k)
--deepoffadd 2-letter terms aaโ€“zz (676)
--deeperoffadd 3-letter terms aaaโ€“zzz (17,576; slow)
--countries a,b,csettingsregions to discover across
--skip-play / --skip-appstoreoffrun only one store
--search-limit / --chart-limit200 / 200results per iTunes term / per chart
--hits30Play results per term (server caps ~30)
--delay0.5polite pause between discovery requests

scraper.py

FlagDefaultPurpose
--workers N10concurrent requests (thread pool)
--shard i0this machine's slice index
--shards N1total machines splitting the work
--countries a,b,csettingsregions to scrape (one row each)
--no-resumeoffignore existing CSV, scrape all again
--out DIR / --config PATHoutput / apps.jsonoutput folder / input file

11Command cheat sheet

โ‘  Discover ~100k apps (App Store driven)

# fastest route to 1 lakh โ€” App Store only, using the wordlist
python discover.py --skip-play --target 100000 --terms-file english_words.txt --countries us

# include Google Play too (adds ~15โ€“30k; can't reach 100k on its own)
python discover.py --target 100000 --terms-file english_words.txt --countries us,gb

โ‘ก Scrape across 2 PCs

# --- PC #1 (IP A) ---
python scraper.py --shard 0 --shards 2 --workers 10

# --- PC #2 (IP B) ---
python scraper.py --shard 1 --shards 2 --workers 10

# ...interrupted? just run the same command again โ€” it resumes.

โ‘ข Merge the two PCs' output

# copy both app_data_shard*of2.csv into one output/ folder, then:
python merge_csv.py

Single-PC run (no sharding)

python scraper.py --workers 10

12Limits & ground truth

RealityDetail
No full catalogNeither store lists "all apps"; you only get what discovery finds.
Google Play search cap~30 results per query โ€” Play tops out ~15โ€“30k regardless of terms.
App Store is the volumeiTunes search ~180/term โ†’ this is what reaches 100k.
Per-IP rate limitsBoth stores throttle per IP; 2 PCs/IPs โ‰ˆ 2ร— safe budget. Proxies extend further.
Per-country dataRatings & pricing differ by country โ†’ one row per app per country.
Terms of ServiceBoth stores restrict automated scraping. Fine for internal research at modest scale; get sign-off before commercial redistribution.
Tuning rule of thumb
Start at --workers 10. If you see constant THROTTLING lines, lower workers (e.g. 6) โ€” fewer cooldowns beats pushing too hard. Back up apps.json after a big discovery run; it's the expensive artifact.