apple-app-store-scraper-lite 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apple_app_store_scraper_lite-0.1.0/LICENSE +18 -0
- apple_app_store_scraper_lite-0.1.0/PKG-INFO +213 -0
- apple_app_store_scraper_lite-0.1.0/README.md +188 -0
- apple_app_store_scraper_lite-0.1.0/pyproject.toml +40 -0
- apple_app_store_scraper_lite-0.1.0/setup.cfg +4 -0
- apple_app_store_scraper_lite-0.1.0/src/apple_app_store_scraper_lite/__init__.py +20 -0
- apple_app_store_scraper_lite-0.1.0/src/apple_app_store_scraper_lite/pagination.py +116 -0
- apple_app_store_scraper_lite-0.1.0/src/apple_app_store_scraper_lite/parsers.py +364 -0
- apple_app_store_scraper_lite-0.1.0/src/apple_app_store_scraper_lite/urls.py +134 -0
- apple_app_store_scraper_lite-0.1.0/src/apple_app_store_scraper_lite.egg-info/PKG-INFO +213 -0
- apple_app_store_scraper_lite-0.1.0/src/apple_app_store_scraper_lite.egg-info/SOURCES.txt +16 -0
- apple_app_store_scraper_lite-0.1.0/src/apple_app_store_scraper_lite.egg-info/dependency_links.txt +1 -0
- apple_app_store_scraper_lite-0.1.0/src/apple_app_store_scraper_lite.egg-info/requires.txt +4 -0
- apple_app_store_scraper_lite-0.1.0/src/apple_app_store_scraper_lite.egg-info/top_level.txt +1 -0
- apple_app_store_scraper_lite-0.1.0/tests/test_acceptance.py +221 -0
- apple_app_store_scraper_lite-0.1.0/tests/test_pagination.py +134 -0
- apple_app_store_scraper_lite-0.1.0/tests/test_parsers.py +286 -0
- apple_app_store_scraper_lite-0.1.0/tests/test_urls.py +115 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agents Kit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
6
|
+
and associated documentation files (the "Software"), to deal in the Software without
|
|
7
|
+
restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
9
|
+
Software is furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
12
|
+
substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
15
|
+
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
17
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apple-app-store-scraper-lite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, pure-Python library for scraping Apple App Store data via iTunes API
|
|
5
|
+
Author: Apple App Store Scraper Lite Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://apify.com/Redbaron/apple-app-store-scraper-lite
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Apple App Store Scraper Lite
|
|
27
|
+
|
|
28
|
+
> **Run it hosted, no setup:** [Apple App Store Scraper Lite on Apify](https://apify.com/Redbaron/apple-app-store-scraper-lite) — pay per result, nothing to install or maintain.
|
|
29
|
+
|
|
30
|
+
A lightweight, pure-Python library for scraping Apple App Store data via the iTunes API. Supports searching for apps, getting app details, extracting reviews, and discovering top charts — all without a browser, using only standard library HTTP requests.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Search apps** by keyword with full metadata (name, developer, price, rating, description, screenshots, supported devices)
|
|
35
|
+
- **Get app details** by App Store ID or URL
|
|
36
|
+
- **Extract reviews** with ratings, titles, content, author, date, and app version
|
|
37
|
+
- **Discover top charts** (free, paid, grossing, new releases) by country
|
|
38
|
+
- **Pure Python** — no external dependencies, no browser automation
|
|
39
|
+
- **Pagination support** — automatically follows next-page links
|
|
40
|
+
- **Privacy-safe** — never returns personal data (emails, phones, usernames beyond public review author names)
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install apple-app-store-scraper-lite-0.1.0-py3-none-any.whl
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or install from source:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requires Python 3.9+.
|
|
55
|
+
|
|
56
|
+
## Quickstart
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from apple_app_store_scraper_lite import start_urls, process, next_urls
|
|
60
|
+
import urllib.request
|
|
61
|
+
import json
|
|
62
|
+
|
|
63
|
+
# 1. Build start URLs from your parameters
|
|
64
|
+
params = {
|
|
65
|
+
"mode": "search",
|
|
66
|
+
"searchTerm": "photo editor",
|
|
67
|
+
"country": "us"
|
|
68
|
+
}
|
|
69
|
+
urls = start_urls(params)
|
|
70
|
+
print(f"Fetching: {urls[0]}")
|
|
71
|
+
|
|
72
|
+
# 2. Fetch the response (using stdlib urllib)
|
|
73
|
+
with urllib.request.urlopen(urls[0]) as response:
|
|
74
|
+
text = response.read().decode("utf-8")
|
|
75
|
+
|
|
76
|
+
# 3. Parse into structured records
|
|
77
|
+
records = process(text, urls[0])
|
|
78
|
+
print(f"Found {len(records)} apps")
|
|
79
|
+
for app in records[:3]:
|
|
80
|
+
print(f" - {app['appName']} ({app['appId']}) by {app['developer']} — {app['price']['formatted']} — ⭐ {app['rating']['value']}")
|
|
81
|
+
|
|
82
|
+
# 4. Follow pagination if needed
|
|
83
|
+
for next_url in next_urls(text, urls[0]):
|
|
84
|
+
with urllib.request.urlopen(next_url) as response:
|
|
85
|
+
more_text = response.read().decode("utf-8")
|
|
86
|
+
more_records = process(more_text, next_url)
|
|
87
|
+
print(f"Next page: {len(more_records)} more apps")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Modes
|
|
91
|
+
|
|
92
|
+
| Mode | Description | Required Params | Optional Params |
|
|
93
|
+
|------|-------------|-----------------|-----------------|
|
|
94
|
+
| `search` | Search apps by keyword | `mode`, `searchTerm` | `country` (default: `us`) |
|
|
95
|
+
| `details` | Get full app metadata | `mode`, `appIds` | `country` (default: `us`) |
|
|
96
|
+
| `reviews` | Extract user reviews | `mode`, `appIds` | `country` (default: `us`) |
|
|
97
|
+
| `charts` | Top charts by category | `mode` | `chartType` (default: `top-free`), `country` (default: `us`) |
|
|
98
|
+
|
|
99
|
+
### Chart Types
|
|
100
|
+
- `top-free` — Top Free Apps
|
|
101
|
+
- `top-paid` — Top Paid Apps
|
|
102
|
+
- `top-grossing` — Top Grossing Apps
|
|
103
|
+
- `new-free` — New Free Apps
|
|
104
|
+
- `new-paid` — New Paid Apps
|
|
105
|
+
|
|
106
|
+
## API Reference
|
|
107
|
+
|
|
108
|
+
### `start_urls(params: dict) -> list[str]`
|
|
109
|
+
|
|
110
|
+
Build initial URL(s) to fetch from user parameters.
|
|
111
|
+
|
|
112
|
+
**Parameters:**
|
|
113
|
+
- `params` (dict): Configuration dictionary with keys:
|
|
114
|
+
- `mode` (str, required): One of `"search"`, `"details"`, `"reviews"`, `"charts"`
|
|
115
|
+
- `searchTerm` (str, optional): Keyword for search mode (default: `"photo editor"`)
|
|
116
|
+
- `appIds` (list[str], optional): App IDs or App Store URLs for details/reviews modes (default: `["389801252"]`)
|
|
117
|
+
- `chartType` (str, optional): Chart type for charts mode (default: `"top-free"`)
|
|
118
|
+
- `country` (str, optional): Two-letter country code (default: `"us"`)
|
|
119
|
+
|
|
120
|
+
**Returns:** List of URLs to fetch initially.
|
|
121
|
+
|
|
122
|
+
**Example:**
|
|
123
|
+
```python
|
|
124
|
+
start_urls({"mode": "search", "searchTerm": "fitness", "country": "gb"})
|
|
125
|
+
# ["https://itunes.apple.com/search?term=fitness&country=gb&entity=software&limit=200"]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### `process(text: str, url: str) -> list[dict]`
|
|
131
|
+
|
|
132
|
+
Parse a single fetched response into structured records.
|
|
133
|
+
|
|
134
|
+
**Parameters:**
|
|
135
|
+
- `text` (str): Raw response body (JSON)
|
|
136
|
+
- `url` (str): The URL that was fetched (used to determine response type)
|
|
137
|
+
|
|
138
|
+
**Returns:** List of record dictionaries. Each record has exactly these fields:
|
|
139
|
+
|
|
140
|
+
**App Records (search, details, charts):**
|
|
141
|
+
| Field | Type | Description |
|
|
142
|
+
|-------|------|-------------|
|
|
143
|
+
| `appId` | str | Numeric App Store ID |
|
|
144
|
+
| `appName` | str | App display name |
|
|
145
|
+
| `developer` | str | Developer/seller name |
|
|
146
|
+
| `price` | dict | `{"amount": float, "currency": str, "formatted": str}` |
|
|
147
|
+
| `rating` | dict | `{"value": float, "count": int}` |
|
|
148
|
+
| `description` | str | App description |
|
|
149
|
+
| `screenshotUrls` | list[str] | Screenshot image URLs |
|
|
150
|
+
| `supportedDevices` | list[str] | e.g., `["iphone", "ipad", "mac"]` |
|
|
151
|
+
| `country` | str | Two-letter country code |
|
|
152
|
+
|
|
153
|
+
**Review Records (reviews mode):**
|
|
154
|
+
| Field | Type | Description |
|
|
155
|
+
|-------|------|-------------|
|
|
156
|
+
| `reviewId` | str | Unique review identifier |
|
|
157
|
+
| `title` | str | Review title |
|
|
158
|
+
| `content` | str | Review text |
|
|
159
|
+
| `rating` | int | 1–5 star rating |
|
|
160
|
+
| `author` | str | Review author name (public) |
|
|
161
|
+
| `date` | str | ISO 8601 timestamp |
|
|
162
|
+
| `appVersion` | str | App version reviewed |
|
|
163
|
+
|
|
164
|
+
**Example:**
|
|
165
|
+
```python
|
|
166
|
+
records = process(json_text, "https://itunes.apple.com/search?term=photo+editor&country=us")
|
|
167
|
+
# [{
|
|
168
|
+
# "appId": "389801252",
|
|
169
|
+
# "appName": "Photo Editor Pro",
|
|
170
|
+
# "developer": "Example Inc.",
|
|
171
|
+
# "price": {"amount": 0.0, "currency": "USD", "formatted": "Free"},
|
|
172
|
+
# "rating": {"value": 4.7, "count": 12345},
|
|
173
|
+
# "description": "Edit photos like a pro...",
|
|
174
|
+
# "screenshotUrls": ["https://.../1.jpg", "https://.../2.jpg"],
|
|
175
|
+
# "supportedDevices": ["iphone", "ipad"],
|
|
176
|
+
# "country": "us"
|
|
177
|
+
# }]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### `next_urls(text: str, url: str) -> list[str]`
|
|
183
|
+
|
|
184
|
+
Extract further URLs to fetch for pagination.
|
|
185
|
+
|
|
186
|
+
**Parameters:**
|
|
187
|
+
- `text` (str): Raw response body (JSON)
|
|
188
|
+
- `url` (str): The URL that was fetched
|
|
189
|
+
|
|
190
|
+
**Returns:** List of next-page URLs, empty if no more pages.
|
|
191
|
+
|
|
192
|
+
**Behavior by mode:**
|
|
193
|
+
- **search**: Uses `offset`/`limit` pagination until `resultCount` is reached
|
|
194
|
+
- **details**: No pagination (single request per app ID)
|
|
195
|
+
- **reviews**: Follows RSS `link[rel="next"]` for paginated review feeds
|
|
196
|
+
- **charts**: No pagination (fixed limit feed)
|
|
197
|
+
|
|
198
|
+
**Example:**
|
|
199
|
+
```python
|
|
200
|
+
next_pages = next_urls(search_response_text, search_url)
|
|
201
|
+
# ["https://itunes.apple.com/search?term=photo+editor&country=us&limit=200&offset=200"]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Running Tests
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
pip install -e ".[dev]"
|
|
208
|
+
pytest -v
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT License — see LICENSE for details.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Apple App Store Scraper Lite
|
|
2
|
+
|
|
3
|
+
> **Run it hosted, no setup:** [Apple App Store Scraper Lite on Apify](https://apify.com/Redbaron/apple-app-store-scraper-lite) — pay per result, nothing to install or maintain.
|
|
4
|
+
|
|
5
|
+
A lightweight, pure-Python library for scraping Apple App Store data via the iTunes API. Supports searching for apps, getting app details, extracting reviews, and discovering top charts — all without a browser, using only standard library HTTP requests.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Search apps** by keyword with full metadata (name, developer, price, rating, description, screenshots, supported devices)
|
|
10
|
+
- **Get app details** by App Store ID or URL
|
|
11
|
+
- **Extract reviews** with ratings, titles, content, author, date, and app version
|
|
12
|
+
- **Discover top charts** (free, paid, grossing, new releases) by country
|
|
13
|
+
- **Pure Python** — no external dependencies, no browser automation
|
|
14
|
+
- **Pagination support** — automatically follows next-page links
|
|
15
|
+
- **Privacy-safe** — never returns personal data (emails, phones, usernames beyond public review author names)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install apple-app-store-scraper-lite-0.1.0-py3-none-any.whl
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install from source:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Python 3.9+.
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from apple_app_store_scraper_lite import start_urls, process, next_urls
|
|
35
|
+
import urllib.request
|
|
36
|
+
import json
|
|
37
|
+
|
|
38
|
+
# 1. Build start URLs from your parameters
|
|
39
|
+
params = {
|
|
40
|
+
"mode": "search",
|
|
41
|
+
"searchTerm": "photo editor",
|
|
42
|
+
"country": "us"
|
|
43
|
+
}
|
|
44
|
+
urls = start_urls(params)
|
|
45
|
+
print(f"Fetching: {urls[0]}")
|
|
46
|
+
|
|
47
|
+
# 2. Fetch the response (using stdlib urllib)
|
|
48
|
+
with urllib.request.urlopen(urls[0]) as response:
|
|
49
|
+
text = response.read().decode("utf-8")
|
|
50
|
+
|
|
51
|
+
# 3. Parse into structured records
|
|
52
|
+
records = process(text, urls[0])
|
|
53
|
+
print(f"Found {len(records)} apps")
|
|
54
|
+
for app in records[:3]:
|
|
55
|
+
print(f" - {app['appName']} ({app['appId']}) by {app['developer']} — {app['price']['formatted']} — ⭐ {app['rating']['value']}")
|
|
56
|
+
|
|
57
|
+
# 4. Follow pagination if needed
|
|
58
|
+
for next_url in next_urls(text, urls[0]):
|
|
59
|
+
with urllib.request.urlopen(next_url) as response:
|
|
60
|
+
more_text = response.read().decode("utf-8")
|
|
61
|
+
more_records = process(more_text, next_url)
|
|
62
|
+
print(f"Next page: {len(more_records)} more apps")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Modes
|
|
66
|
+
|
|
67
|
+
| Mode | Description | Required Params | Optional Params |
|
|
68
|
+
|------|-------------|-----------------|-----------------|
|
|
69
|
+
| `search` | Search apps by keyword | `mode`, `searchTerm` | `country` (default: `us`) |
|
|
70
|
+
| `details` | Get full app metadata | `mode`, `appIds` | `country` (default: `us`) |
|
|
71
|
+
| `reviews` | Extract user reviews | `mode`, `appIds` | `country` (default: `us`) |
|
|
72
|
+
| `charts` | Top charts by category | `mode` | `chartType` (default: `top-free`), `country` (default: `us`) |
|
|
73
|
+
|
|
74
|
+
### Chart Types
|
|
75
|
+
- `top-free` — Top Free Apps
|
|
76
|
+
- `top-paid` — Top Paid Apps
|
|
77
|
+
- `top-grossing` — Top Grossing Apps
|
|
78
|
+
- `new-free` — New Free Apps
|
|
79
|
+
- `new-paid` — New Paid Apps
|
|
80
|
+
|
|
81
|
+
## API Reference
|
|
82
|
+
|
|
83
|
+
### `start_urls(params: dict) -> list[str]`
|
|
84
|
+
|
|
85
|
+
Build initial URL(s) to fetch from user parameters.
|
|
86
|
+
|
|
87
|
+
**Parameters:**
|
|
88
|
+
- `params` (dict): Configuration dictionary with keys:
|
|
89
|
+
- `mode` (str, required): One of `"search"`, `"details"`, `"reviews"`, `"charts"`
|
|
90
|
+
- `searchTerm` (str, optional): Keyword for search mode (default: `"photo editor"`)
|
|
91
|
+
- `appIds` (list[str], optional): App IDs or App Store URLs for details/reviews modes (default: `["389801252"]`)
|
|
92
|
+
- `chartType` (str, optional): Chart type for charts mode (default: `"top-free"`)
|
|
93
|
+
- `country` (str, optional): Two-letter country code (default: `"us"`)
|
|
94
|
+
|
|
95
|
+
**Returns:** List of URLs to fetch initially.
|
|
96
|
+
|
|
97
|
+
**Example:**
|
|
98
|
+
```python
|
|
99
|
+
start_urls({"mode": "search", "searchTerm": "fitness", "country": "gb"})
|
|
100
|
+
# ["https://itunes.apple.com/search?term=fitness&country=gb&entity=software&limit=200"]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### `process(text: str, url: str) -> list[dict]`
|
|
106
|
+
|
|
107
|
+
Parse a single fetched response into structured records.
|
|
108
|
+
|
|
109
|
+
**Parameters:**
|
|
110
|
+
- `text` (str): Raw response body (JSON)
|
|
111
|
+
- `url` (str): The URL that was fetched (used to determine response type)
|
|
112
|
+
|
|
113
|
+
**Returns:** List of record dictionaries. Each record has exactly these fields:
|
|
114
|
+
|
|
115
|
+
**App Records (search, details, charts):**
|
|
116
|
+
| Field | Type | Description |
|
|
117
|
+
|-------|------|-------------|
|
|
118
|
+
| `appId` | str | Numeric App Store ID |
|
|
119
|
+
| `appName` | str | App display name |
|
|
120
|
+
| `developer` | str | Developer/seller name |
|
|
121
|
+
| `price` | dict | `{"amount": float, "currency": str, "formatted": str}` |
|
|
122
|
+
| `rating` | dict | `{"value": float, "count": int}` |
|
|
123
|
+
| `description` | str | App description |
|
|
124
|
+
| `screenshotUrls` | list[str] | Screenshot image URLs |
|
|
125
|
+
| `supportedDevices` | list[str] | e.g., `["iphone", "ipad", "mac"]` |
|
|
126
|
+
| `country` | str | Two-letter country code |
|
|
127
|
+
|
|
128
|
+
**Review Records (reviews mode):**
|
|
129
|
+
| Field | Type | Description |
|
|
130
|
+
|-------|------|-------------|
|
|
131
|
+
| `reviewId` | str | Unique review identifier |
|
|
132
|
+
| `title` | str | Review title |
|
|
133
|
+
| `content` | str | Review text |
|
|
134
|
+
| `rating` | int | 1–5 star rating |
|
|
135
|
+
| `author` | str | Review author name (public) |
|
|
136
|
+
| `date` | str | ISO 8601 timestamp |
|
|
137
|
+
| `appVersion` | str | App version reviewed |
|
|
138
|
+
|
|
139
|
+
**Example:**
|
|
140
|
+
```python
|
|
141
|
+
records = process(json_text, "https://itunes.apple.com/search?term=photo+editor&country=us")
|
|
142
|
+
# [{
|
|
143
|
+
# "appId": "389801252",
|
|
144
|
+
# "appName": "Photo Editor Pro",
|
|
145
|
+
# "developer": "Example Inc.",
|
|
146
|
+
# "price": {"amount": 0.0, "currency": "USD", "formatted": "Free"},
|
|
147
|
+
# "rating": {"value": 4.7, "count": 12345},
|
|
148
|
+
# "description": "Edit photos like a pro...",
|
|
149
|
+
# "screenshotUrls": ["https://.../1.jpg", "https://.../2.jpg"],
|
|
150
|
+
# "supportedDevices": ["iphone", "ipad"],
|
|
151
|
+
# "country": "us"
|
|
152
|
+
# }]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### `next_urls(text: str, url: str) -> list[str]`
|
|
158
|
+
|
|
159
|
+
Extract further URLs to fetch for pagination.
|
|
160
|
+
|
|
161
|
+
**Parameters:**
|
|
162
|
+
- `text` (str): Raw response body (JSON)
|
|
163
|
+
- `url` (str): The URL that was fetched
|
|
164
|
+
|
|
165
|
+
**Returns:** List of next-page URLs, empty if no more pages.
|
|
166
|
+
|
|
167
|
+
**Behavior by mode:**
|
|
168
|
+
- **search**: Uses `offset`/`limit` pagination until `resultCount` is reached
|
|
169
|
+
- **details**: No pagination (single request per app ID)
|
|
170
|
+
- **reviews**: Follows RSS `link[rel="next"]` for paginated review feeds
|
|
171
|
+
- **charts**: No pagination (fixed limit feed)
|
|
172
|
+
|
|
173
|
+
**Example:**
|
|
174
|
+
```python
|
|
175
|
+
next_pages = next_urls(search_response_text, search_url)
|
|
176
|
+
# ["https://itunes.apple.com/search?term=photo+editor&country=us&limit=200&offset=200"]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Running Tests
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pip install -e ".[dev]"
|
|
183
|
+
pytest -v
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT License — see LICENSE for details.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "apple-app-store-scraper-lite"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A lightweight, pure-Python library for scraping Apple App Store data via iTunes API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Apple App Store Scraper Lite Contributors"}
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
include = ["apple_app_store_scraper_lite*"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
"*" = ["fixtures/*.json"]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = ["pytest>=7.0", "pytest-cov>=4.0"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://apify.com/Redbaron/apple-app-store-scraper-lite"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Apple App Store Scraper Lite
|
|
3
|
+
|
|
4
|
+
A lightweight, pure-Python library for scraping Apple App Store data via the iTunes API.
|
|
5
|
+
Supports searching for apps, getting app details, extracting reviews, and discovering top charts.
|
|
6
|
+
|
|
7
|
+
Public API:
|
|
8
|
+
- start_urls: Build initial URLs from user parameters
|
|
9
|
+
- process: Parse a fetched response into structured records
|
|
10
|
+
- next_urls: Get pagination URLs for the next page of results
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .parsers import process
|
|
16
|
+
from .pagination import next_urls
|
|
17
|
+
from .urls import start_urls
|
|
18
|
+
|
|
19
|
+
__all__ = ["start_urls", "process", "next_urls"]
|
|
20
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pagination handling for Apple App Store iTunes API responses.
|
|
3
|
+
|
|
4
|
+
Pure functions that extract next-page URLs from responses.
|
|
5
|
+
No network I/O, no side effects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import urllib.parse
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _extract_next_from_search(text: str, url: str) -> list[str]:
|
|
16
|
+
"""Extract next page URL from search API response."""
|
|
17
|
+
try:
|
|
18
|
+
data = json.loads(text)
|
|
19
|
+
except json.JSONDecodeError:
|
|
20
|
+
return []
|
|
21
|
+
|
|
22
|
+
# iTunes search API doesn't have standard pagination in response
|
|
23
|
+
# But we can paginate by offset if resultCount > limit
|
|
24
|
+
result_count = data.get("resultCount", 0)
|
|
25
|
+
results = data.get("results", [])
|
|
26
|
+
|
|
27
|
+
if not isinstance(results, list) or len(results) == 0:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
# If we got fewer results than limit, we're done
|
|
31
|
+
# Default limit is 200, but could be overridden
|
|
32
|
+
parsed = urllib.parse.urlparse(url)
|
|
33
|
+
query = urllib.parse.parse_qs(parsed.query)
|
|
34
|
+
limit = int(query.get("limit", ["200"])[0])
|
|
35
|
+
|
|
36
|
+
if len(results) < limit:
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
# Calculate next offset
|
|
40
|
+
offset = int(query.get("offset", ["0"])[0])
|
|
41
|
+
next_offset = offset + limit
|
|
42
|
+
|
|
43
|
+
# Don't paginate beyond resultCount
|
|
44
|
+
if next_offset >= result_count:
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
# Build next URL with updated offset
|
|
48
|
+
next_query = dict(query)
|
|
49
|
+
next_query["offset"] = [str(next_offset)]
|
|
50
|
+
next_url = urllib.parse.urlunparse((
|
|
51
|
+
parsed.scheme,
|
|
52
|
+
parsed.netloc,
|
|
53
|
+
parsed.path,
|
|
54
|
+
parsed.params,
|
|
55
|
+
urllib.parse.urlencode(next_query, doseq=True),
|
|
56
|
+
parsed.fragment
|
|
57
|
+
))
|
|
58
|
+
return [next_url]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _extract_next_from_lookup(text: str, url: str) -> list[str]:
|
|
62
|
+
"""Lookup API doesn't paginate - single request per app ID."""
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _extract_next_from_reviews(text: str, url: str) -> list[str]:
|
|
67
|
+
"""Extract next page URL from reviews RSS feed."""
|
|
68
|
+
try:
|
|
69
|
+
data = json.loads(text)
|
|
70
|
+
except json.JSONDecodeError:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
feed = data.get("feed", {})
|
|
74
|
+
links = feed.get("link", [])
|
|
75
|
+
if not isinstance(links, list):
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
# Find the 'next' link
|
|
79
|
+
for link in links:
|
|
80
|
+
if isinstance(link, dict):
|
|
81
|
+
rel = link.get("rel")
|
|
82
|
+
href = link.get("href")
|
|
83
|
+
if rel == "next" and href:
|
|
84
|
+
return [href]
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _extract_next_from_charts(text: str, url: str) -> list[str]:
|
|
89
|
+
"""Charts RSS feed doesn't typically paginate (fixed limit)."""
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def next_urls(text: str, url: str) -> list[str]:
|
|
94
|
+
"""
|
|
95
|
+
Extract further URLs to fetch for pagination.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
text: Raw response text (JSON)
|
|
99
|
+
url: The URL that was fetched
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
List of next page URLs, empty if no more pages.
|
|
103
|
+
"""
|
|
104
|
+
if not text or not isinstance(text, str):
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
if "/search?" in url:
|
|
108
|
+
return _extract_next_from_search(text, url)
|
|
109
|
+
elif "/lookup?" in url:
|
|
110
|
+
return _extract_next_from_lookup(text, url)
|
|
111
|
+
elif "/customerreviews/" in url:
|
|
112
|
+
return _extract_next_from_reviews(text, url)
|
|
113
|
+
elif "/rss/" in url:
|
|
114
|
+
return _extract_next_from_charts(text, url)
|
|
115
|
+
else:
|
|
116
|
+
return []
|