snapchat-scraper-api 0.0.1__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.
- snapchat_scraper_api-0.0.1/LICENSE +21 -0
- snapchat_scraper_api-0.0.1/MANIFEST.in +2 -0
- snapchat_scraper_api-0.0.1/PKG-INFO +228 -0
- snapchat_scraper_api-0.0.1/README.md +198 -0
- snapchat_scraper_api-0.0.1/pyproject.toml +38 -0
- snapchat_scraper_api-0.0.1/setup.cfg +4 -0
- snapchat_scraper_api-0.0.1/src/snapchat_scraper_api/__init__.py +6 -0
- snapchat_scraper_api-0.0.1/src/snapchat_scraper_api/client.py +265 -0
- snapchat_scraper_api-0.0.1/src/snapchat_scraper_api.egg-info/PKG-INFO +228 -0
- snapchat_scraper_api-0.0.1/src/snapchat_scraper_api.egg-info/SOURCES.txt +11 -0
- snapchat_scraper_api-0.0.1/src/snapchat_scraper_api.egg-info/dependency_links.txt +1 -0
- snapchat_scraper_api-0.0.1/src/snapchat_scraper_api.egg-info/requires.txt +1 -0
- snapchat_scraper_api-0.0.1/src/snapchat_scraper_api.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wordstotech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snapchat-scraper-api
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Snapchat scraper API client: public profile data, follower counts and Spotlight videos via ScrapingBee.
|
|
5
|
+
Author: wordstotech
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ScrapingBee/snapchat-scraper-api
|
|
8
|
+
Project-URL: Repository, https://github.com/ScrapingBee/snapchat-scraper-api
|
|
9
|
+
Project-URL: Documentation, https://www.scrapingbee.com/documentation/
|
|
10
|
+
Keywords: snapchat scraper,snapchat api,snapchat profiles scraper,snapchat data scraper,web scraping,scrapingbee
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: requests>=2.25.0
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# snapchat-scraper-api
|
|
32
|
+
|
|
33
|
+
A Python client for public Snapchat profile data through ScrapingBee. Built around the fact that most selectors on a Snapchat profile are guaranteed to break, and the ones that are not live somewhere unobvious.
|
|
34
|
+
|
|
35
|
+
**Verified against `snapchat.com/add/teamsnapchat` and `snapchat.com/add/mrbeast` on 2026-09-10.** Three findings below only surface on a live run: there are two incompatible page layouts, the follower count is not in the visible text at all, and the page language depends on which country the request left from.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install snapchat-scraper-api
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.8 or newer and `requests`.
|
|
42
|
+
|
|
43
|
+
## The cheapest target in the series
|
|
44
|
+
|
|
45
|
+
One credit per profile. Snapchat serves these pages as delivered HTML, so `mode=auto` settles on the plain rung and never opens a browser. Do not add `render_js` out of habit, it buys nothing here and costs five times more.
|
|
46
|
+
|
|
47
|
+
## What is in scope
|
|
48
|
+
|
|
49
|
+
Public profile pages, read anonymously: display name, username, subtitle, Snapcode, preview image, canonical URL, follower count, profile creation and last modified dates, Snapchat's family friendly flag, and public Spotlight or highlight videos.
|
|
50
|
+
|
|
51
|
+
Out of scope: private accounts, friend lists, Snap Map location data, direct messages and story view data. Those need a signed in session, and scraping under login credentials is prohibited by ScrapingBee's terms of service.
|
|
52
|
+
|
|
53
|
+
## Authentication
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from snapchat_scraper_api import SnapchatScraper
|
|
57
|
+
|
|
58
|
+
bee = SnapchatScraper("YOUR_API_KEY")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Sent as `Authorization: Bearer YOUR_API_KEY`. Key and 1,000 free credits: [ScrapingBee](https://www.scrapingbee.com/). Landing page: [Snapchat scraper API](https://www.scrapingbee.com/scrapers/snapchat-scraper-api/).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Method reference
|
|
66
|
+
|
|
67
|
+
### `profile(username, country=None)`
|
|
68
|
+
|
|
69
|
+
**1 credit** unpinned, **25 credits** with a country pinned.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
bee.profile("teamsnapchat")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
{'display_name': 'Team Snapchat',
|
|
77
|
+
'username': 'teamsnapchat',
|
|
78
|
+
'subtitle': 'Add me on Snapchat!',
|
|
79
|
+
'snapcode': 'https://app.snapchat.com/web/deeplink/snapcode?username=teamsnapchat&type=SVG&bitmoji=enable',
|
|
80
|
+
'profile_image': 'https://www.snapchat.com/web-capture/www.snapchat.com/@teamsnapchat/preview/square.jpeg?xp_id=1',
|
|
81
|
+
'og_title': 'Team Snapchat on Snapchat',
|
|
82
|
+
'og_description': 'Team Snapchat is on Snapchat!',
|
|
83
|
+
'canonical': 'https://www.snapchat.com/@teamsnapchat'}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The same call works on a creator profile, which matters more than it sounds.
|
|
87
|
+
|
|
88
|
+
**Snapchat serves two incompatible layouts.** Both handles below were fetched with one rule set:
|
|
89
|
+
|
|
90
|
+
| Selector | `teamsnapchat` (basic card) | `mrbeast` (creator profile) |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `h1 span` | empty | `MrBeast` |
|
|
93
|
+
| `h4 span` | `Team Snapchat` | empty |
|
|
94
|
+
| `h5 span` | `teamsnapchat` | wrong node, the page has ten `h5` elements |
|
|
95
|
+
| `[data-testid="snapCodeImage"]` | the Snapcode URL | empty |
|
|
96
|
+
|
|
97
|
+
They are mutually exclusive. This client requests both headings and returns whichever is populated, so one call covers both page types. A scraper written against only `h4 span` works on half of Snapchat and returns empty strings on the other half, with a 200 status and no error.
|
|
98
|
+
|
|
99
|
+
Three more decisions the client makes for you:
|
|
100
|
+
|
|
101
|
+
- **The username comes from the canonical URL**, not from a heading. The canonical is byte identical across layouts and does not change with locale, and it always reads `https://www.snapchat.com/@<username>`.
|
|
102
|
+
- **The Snapcode is rebuilt when the selector is empty.** The endpoint is parameterised by username, and the constructed string matches the live selector value exactly, verified on both handles. No second request.
|
|
103
|
+
- **An empty dict means the handle has no public page.** More on that below.
|
|
104
|
+
|
|
105
|
+
**Never select on CSS module classes.** Snapchat ships class names like `UserDetailsCard_title__K9Awz`, `Heading_h400Emphasis__SQXxl` and `DesktopUserProfile_desktopContainer__UwOc_`. The suffix after the double underscore is a build hash. It rotates on deploy, and a scraper keyed on it silently returns empty strings afterwards.
|
|
106
|
+
|
|
107
|
+
### `stats(username, country=None)`
|
|
108
|
+
|
|
109
|
+
**1 credit.** Follower count and account dates, from the `application/ld+json` block.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
bee.stats("mrbeast")
|
|
113
|
+
# {'name': 'MrBeast',
|
|
114
|
+
# 'username': 'mrbeast',
|
|
115
|
+
# 'url': 'https://www.snapchat.com/@mrbeast',
|
|
116
|
+
# 'image': 'https://cf-st.sc-cdn.net/aps/bolt/...',
|
|
117
|
+
# 'followers': 1463400,
|
|
118
|
+
# 'created': '2019-05-16T14:46:37.345Z',
|
|
119
|
+
# 'modified': '2026-08-13T13:01:39.438Z',
|
|
120
|
+
# 'locale': 'en-US',
|
|
121
|
+
# 'family_friendly': True}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**The follower count is not in the visible text.** The DOM carries the i18n template placeholder, literally `{subscriberCount} suscriptores`, so a text selector returns the template rather than a number. The real value sits in `interactionStatistic`, in the counter whose `interactionType` is `FollowAction`. This client digs it out.
|
|
125
|
+
|
|
126
|
+
`created` and `modified` are the other two fields available nowhere else. `modified` is how you separate an active creator from a dormant handle without fetching a single post.
|
|
127
|
+
|
|
128
|
+
Returns `None` when there is no public profile page.
|
|
129
|
+
|
|
130
|
+
Note this parses the fetched HTML rather than using `extract_rules`, because **`extract_rules` cannot read script tag contents**. A rule selecting `script[type="application/ld+json"]` returns `None`, tested directly.
|
|
131
|
+
|
|
132
|
+
### `spotlight(username, country=None)`
|
|
133
|
+
|
|
134
|
+
**1 credit.** Public Spotlight and highlight videos, from the `ItemList` block.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
bee.spotlight("mrbeast")
|
|
138
|
+
# [{'url': 'https://www.snapchat.com/@mrbeast/highlight/91542d31-...',
|
|
139
|
+
# 'name': 'A Snapchat video by MrBeast',
|
|
140
|
+
# 'description': 'A Snapchat video by MrBeast',
|
|
141
|
+
# 'thumbnail': 'https://cf-st.sc-cdn.net/d/...'}, ...]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Five entries on the creator page tested. **An empty list is normal, not a failure.** The same block came back as an empty array on `teamsnapchat`.
|
|
145
|
+
|
|
146
|
+
### `exists(username)`
|
|
147
|
+
|
|
148
|
+
**1 credit.** Whether the handle has a public profile page.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
bee.exists("teamsnapchat") # True
|
|
152
|
+
bee.exists("dailymail") # False
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A missing handle answers in two different ways, and both were observed on the same handle at different times. One capture returned HTTP 200 with a 5,973 byte body, a bare `Snapchat` title and no `ProfilePage` block. A later capture returned a real **HTTP 404**, forwarded straight through because 404 is one of the few statuses ScrapingBee does not rewrite.
|
|
156
|
+
|
|
157
|
+
This client treats both as "no page" rather than raising, so a batch run does not die on one bad handle. Decide on the presence of the `ProfilePage` block, not on the status code.
|
|
158
|
+
|
|
159
|
+
### `snapcode_url(username, svg=True, bitmoji=True)`
|
|
160
|
+
|
|
161
|
+
A static method. **0 credits, no request.**
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
SnapchatScraper.snapcode_url("teamsnapchat")
|
|
165
|
+
# 'https://app.snapchat.com/web/deeplink/snapcode?username=teamsnapchat&type=SVG&bitmoji=enable'
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `usage()`
|
|
169
|
+
|
|
170
|
+
Free. Account credits, concurrency and renewal date.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## The locale trap
|
|
175
|
+
|
|
176
|
+
Snapchat localises by proxy exit IP, and without a country parameter whichever region the request left from decides the language of every title, subtitle and label you parse.
|
|
177
|
+
|
|
178
|
+
Two captures of the same creator URL, same parameters, no change in between:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
capture 1 title: MrBeast (@mrbeast) | Historias de Snapchat, Spotlight y Lentes
|
|
182
|
+
capture 2 og_title: MrBeast pe Snapchat og_description: MrBeast este pe Snapchat!
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Spanish, then Romanian. The structured data tells you it happened, through `inLanguage`, which `stats()` returns as `locale`.
|
|
186
|
+
|
|
187
|
+
What moves and what does not:
|
|
188
|
+
|
|
189
|
+
| Field | Locale dependent |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `og_title`, `og_description`, page title, visible labels | yes |
|
|
192
|
+
| `canonical`, `username`, `profile_image`, `snapcode` | no |
|
|
193
|
+
| `followers`, `created`, `modified` | no |
|
|
194
|
+
|
|
195
|
+
So pin the country only when you actually read the strings:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
bee.profile("mrbeast", country="us") # 25 credits, English guaranteed
|
|
199
|
+
bee.stats("mrbeast") # 1 credit, the number is the same either way
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Pinning gave an identical `followers` value on both runs. Geotargeting requires the premium tier, which is a twenty five times cost increase, so do not turn it on for numeric work.
|
|
203
|
+
|
|
204
|
+
## Credit cost
|
|
205
|
+
|
|
206
|
+
Measured from `spb-cost` headers. Available on `bee.last_cost`.
|
|
207
|
+
|
|
208
|
+
| Configuration | Credits |
|
|
209
|
+
|---|---|
|
|
210
|
+
| `mode=auto`, settled on plain HTML | 1 |
|
|
211
|
+
| `premium_proxy` with `country_code` | 25 |
|
|
212
|
+
| Validation error | 0 |
|
|
213
|
+
|
|
214
|
+
`mode=auto` bills only the rung that worked and nothing if every rung fails. It is incompatible with `render_js`, `premium_proxy` and `stealth_proxy`, and sending both returns HTTP 400 while billing nothing.
|
|
215
|
+
|
|
216
|
+
At 1 credit per profile, 250,000 credits covers 250,000 profile checks. Plan tiers: [ScrapingBee pricing](https://www.scrapingbee.com/pricing).
|
|
217
|
+
|
|
218
|
+
## Related
|
|
219
|
+
|
|
220
|
+
Adjacent social and creator landing pages: [Patreon scraper API](https://www.scrapingbee.com/scrapers/patreon-api/), [TikTok API](https://www.scrapingbee.com/scrapers/tiktok-api/), [TikTok search API](https://www.scrapingbee.com/scrapers/tiktok-search-api/), [TikTok follower API](https://www.scrapingbee.com/scrapers/tiktok-follower/), [Twitch API](https://www.scrapingbee.com/scrapers/twitch-api/), [Substack scraper API](https://www.scrapingbee.com/scrapers/substack-scraper-api/), [YouTube shorts API](https://www.scrapingbee.com/scrapers/youtube-shorts-api/), [YouTube video scraper API](https://www.scrapingbee.com/scrapers/youtube-video-scraper-api/), [YouTube title scraper API](https://www.scrapingbee.com/scrapers/youtube-title-scraper-api/).
|
|
221
|
+
|
|
222
|
+
Features: [data extraction](https://www.scrapingbee.com/features/data-extraction/), [AI web scraping](https://www.scrapingbee.com/features/ai-web-scraping-api/), [screenshots](https://www.scrapingbee.com/features/screenshot/), [markdown scraper](https://www.scrapingbee.com/features/markdown-scraper/), [JavaScript scenario](https://www.scrapingbee.com/features/javascript-scenario/), [n8n integration](https://www.scrapingbee.com/features/n8n/).
|
|
223
|
+
|
|
224
|
+
Reference: [extraction rules documentation](https://www.scrapingbee.com/documentation/data-extraction/). The selector durability walkthrough is at [github.com/ScrapingBee/snapchat-scraper-api](https://github.com/ScrapingBee/snapchat-scraper-api).
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# snapchat-scraper-api
|
|
2
|
+
|
|
3
|
+
A Python client for public Snapchat profile data through ScrapingBee. Built around the fact that most selectors on a Snapchat profile are guaranteed to break, and the ones that are not live somewhere unobvious.
|
|
4
|
+
|
|
5
|
+
**Verified against `snapchat.com/add/teamsnapchat` and `snapchat.com/add/mrbeast` on 2026-09-10.** Three findings below only surface on a live run: there are two incompatible page layouts, the follower count is not in the visible text at all, and the page language depends on which country the request left from.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install snapchat-scraper-api
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.8 or newer and `requests`.
|
|
12
|
+
|
|
13
|
+
## The cheapest target in the series
|
|
14
|
+
|
|
15
|
+
One credit per profile. Snapchat serves these pages as delivered HTML, so `mode=auto` settles on the plain rung and never opens a browser. Do not add `render_js` out of habit, it buys nothing here and costs five times more.
|
|
16
|
+
|
|
17
|
+
## What is in scope
|
|
18
|
+
|
|
19
|
+
Public profile pages, read anonymously: display name, username, subtitle, Snapcode, preview image, canonical URL, follower count, profile creation and last modified dates, Snapchat's family friendly flag, and public Spotlight or highlight videos.
|
|
20
|
+
|
|
21
|
+
Out of scope: private accounts, friend lists, Snap Map location data, direct messages and story view data. Those need a signed in session, and scraping under login credentials is prohibited by ScrapingBee's terms of service.
|
|
22
|
+
|
|
23
|
+
## Authentication
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from snapchat_scraper_api import SnapchatScraper
|
|
27
|
+
|
|
28
|
+
bee = SnapchatScraper("YOUR_API_KEY")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Sent as `Authorization: Bearer YOUR_API_KEY`. Key and 1,000 free credits: [ScrapingBee](https://www.scrapingbee.com/). Landing page: [Snapchat scraper API](https://www.scrapingbee.com/scrapers/snapchat-scraper-api/).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Method reference
|
|
36
|
+
|
|
37
|
+
### `profile(username, country=None)`
|
|
38
|
+
|
|
39
|
+
**1 credit** unpinned, **25 credits** with a country pinned.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
bee.profile("teamsnapchat")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
{'display_name': 'Team Snapchat',
|
|
47
|
+
'username': 'teamsnapchat',
|
|
48
|
+
'subtitle': 'Add me on Snapchat!',
|
|
49
|
+
'snapcode': 'https://app.snapchat.com/web/deeplink/snapcode?username=teamsnapchat&type=SVG&bitmoji=enable',
|
|
50
|
+
'profile_image': 'https://www.snapchat.com/web-capture/www.snapchat.com/@teamsnapchat/preview/square.jpeg?xp_id=1',
|
|
51
|
+
'og_title': 'Team Snapchat on Snapchat',
|
|
52
|
+
'og_description': 'Team Snapchat is on Snapchat!',
|
|
53
|
+
'canonical': 'https://www.snapchat.com/@teamsnapchat'}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The same call works on a creator profile, which matters more than it sounds.
|
|
57
|
+
|
|
58
|
+
**Snapchat serves two incompatible layouts.** Both handles below were fetched with one rule set:
|
|
59
|
+
|
|
60
|
+
| Selector | `teamsnapchat` (basic card) | `mrbeast` (creator profile) |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `h1 span` | empty | `MrBeast` |
|
|
63
|
+
| `h4 span` | `Team Snapchat` | empty |
|
|
64
|
+
| `h5 span` | `teamsnapchat` | wrong node, the page has ten `h5` elements |
|
|
65
|
+
| `[data-testid="snapCodeImage"]` | the Snapcode URL | empty |
|
|
66
|
+
|
|
67
|
+
They are mutually exclusive. This client requests both headings and returns whichever is populated, so one call covers both page types. A scraper written against only `h4 span` works on half of Snapchat and returns empty strings on the other half, with a 200 status and no error.
|
|
68
|
+
|
|
69
|
+
Three more decisions the client makes for you:
|
|
70
|
+
|
|
71
|
+
- **The username comes from the canonical URL**, not from a heading. The canonical is byte identical across layouts and does not change with locale, and it always reads `https://www.snapchat.com/@<username>`.
|
|
72
|
+
- **The Snapcode is rebuilt when the selector is empty.** The endpoint is parameterised by username, and the constructed string matches the live selector value exactly, verified on both handles. No second request.
|
|
73
|
+
- **An empty dict means the handle has no public page.** More on that below.
|
|
74
|
+
|
|
75
|
+
**Never select on CSS module classes.** Snapchat ships class names like `UserDetailsCard_title__K9Awz`, `Heading_h400Emphasis__SQXxl` and `DesktopUserProfile_desktopContainer__UwOc_`. The suffix after the double underscore is a build hash. It rotates on deploy, and a scraper keyed on it silently returns empty strings afterwards.
|
|
76
|
+
|
|
77
|
+
### `stats(username, country=None)`
|
|
78
|
+
|
|
79
|
+
**1 credit.** Follower count and account dates, from the `application/ld+json` block.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
bee.stats("mrbeast")
|
|
83
|
+
# {'name': 'MrBeast',
|
|
84
|
+
# 'username': 'mrbeast',
|
|
85
|
+
# 'url': 'https://www.snapchat.com/@mrbeast',
|
|
86
|
+
# 'image': 'https://cf-st.sc-cdn.net/aps/bolt/...',
|
|
87
|
+
# 'followers': 1463400,
|
|
88
|
+
# 'created': '2019-05-16T14:46:37.345Z',
|
|
89
|
+
# 'modified': '2026-08-13T13:01:39.438Z',
|
|
90
|
+
# 'locale': 'en-US',
|
|
91
|
+
# 'family_friendly': True}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**The follower count is not in the visible text.** The DOM carries the i18n template placeholder, literally `{subscriberCount} suscriptores`, so a text selector returns the template rather than a number. The real value sits in `interactionStatistic`, in the counter whose `interactionType` is `FollowAction`. This client digs it out.
|
|
95
|
+
|
|
96
|
+
`created` and `modified` are the other two fields available nowhere else. `modified` is how you separate an active creator from a dormant handle without fetching a single post.
|
|
97
|
+
|
|
98
|
+
Returns `None` when there is no public profile page.
|
|
99
|
+
|
|
100
|
+
Note this parses the fetched HTML rather than using `extract_rules`, because **`extract_rules` cannot read script tag contents**. A rule selecting `script[type="application/ld+json"]` returns `None`, tested directly.
|
|
101
|
+
|
|
102
|
+
### `spotlight(username, country=None)`
|
|
103
|
+
|
|
104
|
+
**1 credit.** Public Spotlight and highlight videos, from the `ItemList` block.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
bee.spotlight("mrbeast")
|
|
108
|
+
# [{'url': 'https://www.snapchat.com/@mrbeast/highlight/91542d31-...',
|
|
109
|
+
# 'name': 'A Snapchat video by MrBeast',
|
|
110
|
+
# 'description': 'A Snapchat video by MrBeast',
|
|
111
|
+
# 'thumbnail': 'https://cf-st.sc-cdn.net/d/...'}, ...]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Five entries on the creator page tested. **An empty list is normal, not a failure.** The same block came back as an empty array on `teamsnapchat`.
|
|
115
|
+
|
|
116
|
+
### `exists(username)`
|
|
117
|
+
|
|
118
|
+
**1 credit.** Whether the handle has a public profile page.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
bee.exists("teamsnapchat") # True
|
|
122
|
+
bee.exists("dailymail") # False
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A missing handle answers in two different ways, and both were observed on the same handle at different times. One capture returned HTTP 200 with a 5,973 byte body, a bare `Snapchat` title and no `ProfilePage` block. A later capture returned a real **HTTP 404**, forwarded straight through because 404 is one of the few statuses ScrapingBee does not rewrite.
|
|
126
|
+
|
|
127
|
+
This client treats both as "no page" rather than raising, so a batch run does not die on one bad handle. Decide on the presence of the `ProfilePage` block, not on the status code.
|
|
128
|
+
|
|
129
|
+
### `snapcode_url(username, svg=True, bitmoji=True)`
|
|
130
|
+
|
|
131
|
+
A static method. **0 credits, no request.**
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
SnapchatScraper.snapcode_url("teamsnapchat")
|
|
135
|
+
# 'https://app.snapchat.com/web/deeplink/snapcode?username=teamsnapchat&type=SVG&bitmoji=enable'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `usage()`
|
|
139
|
+
|
|
140
|
+
Free. Account credits, concurrency and renewal date.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## The locale trap
|
|
145
|
+
|
|
146
|
+
Snapchat localises by proxy exit IP, and without a country parameter whichever region the request left from decides the language of every title, subtitle and label you parse.
|
|
147
|
+
|
|
148
|
+
Two captures of the same creator URL, same parameters, no change in between:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
capture 1 title: MrBeast (@mrbeast) | Historias de Snapchat, Spotlight y Lentes
|
|
152
|
+
capture 2 og_title: MrBeast pe Snapchat og_description: MrBeast este pe Snapchat!
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Spanish, then Romanian. The structured data tells you it happened, through `inLanguage`, which `stats()` returns as `locale`.
|
|
156
|
+
|
|
157
|
+
What moves and what does not:
|
|
158
|
+
|
|
159
|
+
| Field | Locale dependent |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `og_title`, `og_description`, page title, visible labels | yes |
|
|
162
|
+
| `canonical`, `username`, `profile_image`, `snapcode` | no |
|
|
163
|
+
| `followers`, `created`, `modified` | no |
|
|
164
|
+
|
|
165
|
+
So pin the country only when you actually read the strings:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
bee.profile("mrbeast", country="us") # 25 credits, English guaranteed
|
|
169
|
+
bee.stats("mrbeast") # 1 credit, the number is the same either way
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Pinning gave an identical `followers` value on both runs. Geotargeting requires the premium tier, which is a twenty five times cost increase, so do not turn it on for numeric work.
|
|
173
|
+
|
|
174
|
+
## Credit cost
|
|
175
|
+
|
|
176
|
+
Measured from `spb-cost` headers. Available on `bee.last_cost`.
|
|
177
|
+
|
|
178
|
+
| Configuration | Credits |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `mode=auto`, settled on plain HTML | 1 |
|
|
181
|
+
| `premium_proxy` with `country_code` | 25 |
|
|
182
|
+
| Validation error | 0 |
|
|
183
|
+
|
|
184
|
+
`mode=auto` bills only the rung that worked and nothing if every rung fails. It is incompatible with `render_js`, `premium_proxy` and `stealth_proxy`, and sending both returns HTTP 400 while billing nothing.
|
|
185
|
+
|
|
186
|
+
At 1 credit per profile, 250,000 credits covers 250,000 profile checks. Plan tiers: [ScrapingBee pricing](https://www.scrapingbee.com/pricing).
|
|
187
|
+
|
|
188
|
+
## Related
|
|
189
|
+
|
|
190
|
+
Adjacent social and creator landing pages: [Patreon scraper API](https://www.scrapingbee.com/scrapers/patreon-api/), [TikTok API](https://www.scrapingbee.com/scrapers/tiktok-api/), [TikTok search API](https://www.scrapingbee.com/scrapers/tiktok-search-api/), [TikTok follower API](https://www.scrapingbee.com/scrapers/tiktok-follower/), [Twitch API](https://www.scrapingbee.com/scrapers/twitch-api/), [Substack scraper API](https://www.scrapingbee.com/scrapers/substack-scraper-api/), [YouTube shorts API](https://www.scrapingbee.com/scrapers/youtube-shorts-api/), [YouTube video scraper API](https://www.scrapingbee.com/scrapers/youtube-video-scraper-api/), [YouTube title scraper API](https://www.scrapingbee.com/scrapers/youtube-title-scraper-api/).
|
|
191
|
+
|
|
192
|
+
Features: [data extraction](https://www.scrapingbee.com/features/data-extraction/), [AI web scraping](https://www.scrapingbee.com/features/ai-web-scraping-api/), [screenshots](https://www.scrapingbee.com/features/screenshot/), [markdown scraper](https://www.scrapingbee.com/features/markdown-scraper/), [JavaScript scenario](https://www.scrapingbee.com/features/javascript-scenario/), [n8n integration](https://www.scrapingbee.com/features/n8n/).
|
|
193
|
+
|
|
194
|
+
Reference: [extraction rules documentation](https://www.scrapingbee.com/documentation/data-extraction/). The selector durability walkthrough is at [github.com/ScrapingBee/snapchat-scraper-api](https://github.com/ScrapingBee/snapchat-scraper-api).
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "snapchat-scraper-api"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Snapchat scraper API client: public profile data, follower counts and Spotlight videos via ScrapingBee."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "wordstotech" }]
|
|
13
|
+
keywords = ["snapchat scraper", "snapchat api", "snapchat profiles scraper", "snapchat data scraper", "web scraping", "scrapingbee"]
|
|
14
|
+
dependencies = ["requests>=2.25.0"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
"Topic :: Text Processing :: Markup :: HTML",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/ScrapingBee/snapchat-scraper-api"
|
|
34
|
+
Repository = "https://github.com/ScrapingBee/snapchat-scraper-api"
|
|
35
|
+
Documentation = "https://www.scrapingbee.com/documentation/"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""ScrapingBee based Snapchat scraper for public profile pages.
|
|
2
|
+
|
|
3
|
+
Verified against snapchat.com/add/teamsnapchat and /add/mrbeast on
|
|
4
|
+
2026-09-10.
|
|
5
|
+
|
|
6
|
+
Scope: public profile pages only. Private accounts, friend lists, Snap Map
|
|
7
|
+
location data, direct messages and story views are out of reach, and
|
|
8
|
+
scraping under login credentials is prohibited by ScrapingBee's terms of
|
|
9
|
+
service.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
__all__ = ["SnapchatScraper", "ScrapingBeeError"]
|
|
21
|
+
|
|
22
|
+
BASE_URL = "https://app.scrapingbee.com/api/v1/"
|
|
23
|
+
|
|
24
|
+
# Durable selectors only. Snapchat ships CSS module class names such as
|
|
25
|
+
# UserDetailsCard_title__K9Awz, where the suffix is a build hash that
|
|
26
|
+
# rotates on deploy. Never select on those: they return empty strings with
|
|
27
|
+
# a 200 status after the next release.
|
|
28
|
+
#
|
|
29
|
+
# There are TWO public profile layouts, and they disagree on headings:
|
|
30
|
+
# basic user card (for example teamsnapchat): name in h4 span, no h1
|
|
31
|
+
# creator profile (for example mrbeast): name in h1 span, no h4
|
|
32
|
+
# They are mutually exclusive, so both are requested and coalesced.
|
|
33
|
+
#
|
|
34
|
+
# Username comes from the canonical URL rather than a heading, because the
|
|
35
|
+
# canonical is identical on both layouts and is locale independent. og:title
|
|
36
|
+
# and og:description are NOT: live captures came back in Spanish and in
|
|
37
|
+
# Romanian depending on the proxy exit.
|
|
38
|
+
_PROFILE_RULES = {
|
|
39
|
+
"name_h1": {"selector": "h1 span", "output": "text"},
|
|
40
|
+
"name_h4": {"selector": "h4 span", "output": "text"},
|
|
41
|
+
"subtitle": {"selector": "h5 + div", "output": "text"},
|
|
42
|
+
"snapcode": {"selector": '[data-testid="snapCodeImage"]', "output": "@src"},
|
|
43
|
+
"profile_image": {"selector": 'meta[property="og:image"]', "output": "@content"},
|
|
44
|
+
"og_title": {"selector": 'meta[property="og:title"]', "output": "@content"},
|
|
45
|
+
"og_description": {"selector": 'meta[property="og:description"]', "output": "@content"},
|
|
46
|
+
"canonical": {"selector": 'link[rel="canonical"]', "output": "@href"},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_LD_BLOCK = re.compile(
|
|
50
|
+
r"<script[^>]*application/ld\+json[^>]*>(.*?)</script>", re.S
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ScrapingBeeError(RuntimeError):
|
|
55
|
+
def __init__(self, message: str, status: Optional[int] = None,
|
|
56
|
+
payload: Any = None) -> None:
|
|
57
|
+
super().__init__(message)
|
|
58
|
+
self.status = status
|
|
59
|
+
self.payload = payload
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SnapchatScraper:
|
|
63
|
+
"""Public Snapchat profile data.
|
|
64
|
+
|
|
65
|
+
>>> bee = SnapchatScraper("YOUR_API_KEY")
|
|
66
|
+
>>> bee.profile("teamsnapchat")["username"]
|
|
67
|
+
'teamsnapchat'
|
|
68
|
+
|
|
69
|
+
Credit cost, measured live: 1 credit for an unpinned request, because
|
|
70
|
+
Snapchat serves these pages as delivered HTML and auto mode settles on
|
|
71
|
+
the plain rung. 25 credits with a country pinned, which requires the
|
|
72
|
+
premium tier. Do not reach for ``render_js`` here, it buys nothing.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, api_key: str, base_url: str = BASE_URL,
|
|
76
|
+
timeout: int = 120,
|
|
77
|
+
session: Optional[requests.Session] = None) -> None:
|
|
78
|
+
if not api_key:
|
|
79
|
+
raise ValueError("api_key is required")
|
|
80
|
+
self.api_key = api_key
|
|
81
|
+
self.base_url = base_url
|
|
82
|
+
self.timeout = timeout
|
|
83
|
+
self._session = session or requests.Session()
|
|
84
|
+
self.last_cost: Optional[int] = None
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------ core
|
|
87
|
+
|
|
88
|
+
def _fetch(self, username: str, *, country: Optional[str] = None,
|
|
89
|
+
**params: Any) -> requests.Response:
|
|
90
|
+
url = (
|
|
91
|
+
username if username.startswith("http")
|
|
92
|
+
else f"https://www.snapchat.com/add/{username}"
|
|
93
|
+
)
|
|
94
|
+
clean = {k: v for k, v in params.items() if v is not None}
|
|
95
|
+
if country:
|
|
96
|
+
# Geotargeting requires the premium tier.
|
|
97
|
+
clean.update(premium_proxy="true", country_code=country)
|
|
98
|
+
else:
|
|
99
|
+
clean.setdefault("mode", "auto")
|
|
100
|
+
|
|
101
|
+
response = self._session.get(
|
|
102
|
+
self.base_url,
|
|
103
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
104
|
+
params={"url": url, **clean},
|
|
105
|
+
timeout=self.timeout,
|
|
106
|
+
)
|
|
107
|
+
cost = response.headers.get("spb-cost")
|
|
108
|
+
self.last_cost = int(cost) if cost and cost.isdigit() else None
|
|
109
|
+
|
|
110
|
+
if response.status_code == 400:
|
|
111
|
+
raise ScrapingBeeError(
|
|
112
|
+
"API rejected the request", 400, response.text[:500]
|
|
113
|
+
)
|
|
114
|
+
# ScrapingBee passes 404 through from the target rather than
|
|
115
|
+
# rewriting it. A handle with no public profile page can answer
|
|
116
|
+
# either a 404 or a 200 with a small body and no ProfilePage block,
|
|
117
|
+
# so both have to read as "no page" instead of as a failure.
|
|
118
|
+
if response.status_code == 404:
|
|
119
|
+
return response
|
|
120
|
+
response.raise_for_status()
|
|
121
|
+
return response
|
|
122
|
+
|
|
123
|
+
def _blocks(self, username: str, country: Optional[str] = None) -> List[Any]:
|
|
124
|
+
response = self._fetch(username, country=country)
|
|
125
|
+
if response.status_code == 404:
|
|
126
|
+
return []
|
|
127
|
+
html = response.text
|
|
128
|
+
out = []
|
|
129
|
+
for raw in _LD_BLOCK.findall(html):
|
|
130
|
+
try:
|
|
131
|
+
out.append(json.loads(raw))
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
continue
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
# --------------------------------------------------------------- methods
|
|
137
|
+
|
|
138
|
+
def profile(self, username: str, country: Optional[str] = None) -> Dict[str, Any]:
|
|
139
|
+
"""Display name, username, Snapcode, preview image and canonical URL.
|
|
140
|
+
|
|
141
|
+
Handles both public layouts. The display name sits in an ``h1`` on
|
|
142
|
+
creator profiles and in an ``h4`` on basic user cards, so both are
|
|
143
|
+
requested and whichever is populated wins. The username is taken
|
|
144
|
+
from the canonical URL, which is identical across layouts and does
|
|
145
|
+
not change with locale.
|
|
146
|
+
|
|
147
|
+
The Snapcode ``data-testid`` is present only on basic cards, so it
|
|
148
|
+
is reconstructed from the username when the selector comes back
|
|
149
|
+
empty. The endpoint is parameterised by username, verified against
|
|
150
|
+
the live value.
|
|
151
|
+
|
|
152
|
+
Pass ``country`` whenever you rely on ``og_title`` or
|
|
153
|
+
``og_description``. Snapchat localises by proxy exit IP: live
|
|
154
|
+
captures of the same page returned Spanish and Romanian titles on
|
|
155
|
+
different requests. Everything else here is locale independent.
|
|
156
|
+
|
|
157
|
+
Returns an empty dict when the handle has no public profile page.
|
|
158
|
+
"""
|
|
159
|
+
response = self._fetch(
|
|
160
|
+
username, country=country,
|
|
161
|
+
extract_rules=json.dumps(_PROFILE_RULES),
|
|
162
|
+
)
|
|
163
|
+
if response.status_code == 404:
|
|
164
|
+
return {}
|
|
165
|
+
data = response.json()
|
|
166
|
+
|
|
167
|
+
data["display_name"] = (
|
|
168
|
+
data.pop("name_h1", "") or data.pop("name_h4", "") or None
|
|
169
|
+
)
|
|
170
|
+
data.pop("name_h1", None)
|
|
171
|
+
data.pop("name_h4", None)
|
|
172
|
+
|
|
173
|
+
canonical = data.get("canonical") or ""
|
|
174
|
+
handle = canonical.rsplit("/@", 1)[-1] if "/@" in canonical else None
|
|
175
|
+
data["username"] = handle
|
|
176
|
+
|
|
177
|
+
if not data.get("snapcode") and handle:
|
|
178
|
+
data["snapcode"] = self.snapcode_url(handle)
|
|
179
|
+
|
|
180
|
+
if not data.get("display_name"):
|
|
181
|
+
return {}
|
|
182
|
+
return data
|
|
183
|
+
|
|
184
|
+
def stats(self, username: str,
|
|
185
|
+
country: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
186
|
+
"""Follower count and account dates from structured data.
|
|
187
|
+
|
|
188
|
+
The follower number is not in the visible text. The DOM carries the
|
|
189
|
+
i18n template placeholder ``{subscriberCount}`` instead, so a text
|
|
190
|
+
selector returns the template rather than a number. The real value
|
|
191
|
+
is in ``interactionStatistic``, keyed by a FollowAction counter.
|
|
192
|
+
|
|
193
|
+
Returns None when the handle has no public profile page. That case
|
|
194
|
+
looks like a 200 status with a small body and no ProfilePage block,
|
|
195
|
+
so test for None rather than for a status code.
|
|
196
|
+
"""
|
|
197
|
+
for obj in self._blocks(username, country):
|
|
198
|
+
if isinstance(obj, dict) and obj.get("@type") == "ProfilePage":
|
|
199
|
+
person = obj.get("mainEntity") or {}
|
|
200
|
+
followers = None
|
|
201
|
+
for counter in person.get("interactionStatistic") or []:
|
|
202
|
+
kind = (counter.get("interactionType") or {}).get("@type")
|
|
203
|
+
if kind == "FollowAction":
|
|
204
|
+
followers = counter.get("userInteractionCount")
|
|
205
|
+
return {
|
|
206
|
+
"name": person.get("name"),
|
|
207
|
+
"username": person.get("alternateName"),
|
|
208
|
+
"url": person.get("url"),
|
|
209
|
+
"image": person.get("image"),
|
|
210
|
+
"followers": followers,
|
|
211
|
+
"created": obj.get("dateCreated"),
|
|
212
|
+
"modified": obj.get("dateModified"),
|
|
213
|
+
"locale": obj.get("inLanguage"),
|
|
214
|
+
"family_friendly": obj.get("isFamilyFriendly"),
|
|
215
|
+
}
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
def spotlight(self, username: str,
|
|
219
|
+
country: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
220
|
+
"""Public Spotlight and highlight videos.
|
|
221
|
+
|
|
222
|
+
An empty list is normal rather than an error. A creator page
|
|
223
|
+
returned five VideoObject entries, and a non creator account
|
|
224
|
+
returned an empty ItemList on the same call.
|
|
225
|
+
"""
|
|
226
|
+
for obj in self._blocks(username, country):
|
|
227
|
+
if isinstance(obj, dict) and obj.get("@type") == "ItemList":
|
|
228
|
+
return [
|
|
229
|
+
{
|
|
230
|
+
"url": item.get("url"),
|
|
231
|
+
"name": item.get("name"),
|
|
232
|
+
"description": item.get("description"),
|
|
233
|
+
"thumbnail": item.get("thumbnailUrl"),
|
|
234
|
+
}
|
|
235
|
+
for item in obj.get("itemListElement") or []
|
|
236
|
+
]
|
|
237
|
+
return []
|
|
238
|
+
|
|
239
|
+
def exists(self, username: str) -> bool:
|
|
240
|
+
"""Whether the handle has a public profile page at all."""
|
|
241
|
+
return self.stats(username) is not None
|
|
242
|
+
|
|
243
|
+
def usage(self) -> Dict[str, Any]:
|
|
244
|
+
"""Account credit usage. Free."""
|
|
245
|
+
response = self._session.get(
|
|
246
|
+
"https://app.scrapingbee.com/api/v1/usage",
|
|
247
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
248
|
+
timeout=self.timeout,
|
|
249
|
+
)
|
|
250
|
+
response.raise_for_status()
|
|
251
|
+
return response.json()
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def snapcode_url(username: str, *, svg: bool = True,
|
|
255
|
+
bitmoji: bool = True) -> str:
|
|
256
|
+
"""Build a Snapcode URL without a request.
|
|
257
|
+
|
|
258
|
+
The Snapcode endpoint is parameterised by username, so once you
|
|
259
|
+
know the pattern you can construct it for any handle.
|
|
260
|
+
"""
|
|
261
|
+
return (
|
|
262
|
+
"https://app.snapchat.com/web/deeplink/snapcode"
|
|
263
|
+
f"?username={username}&type={'SVG' if svg else 'PNG'}"
|
|
264
|
+
f"&bitmoji={'enable' if bitmoji else 'disable'}"
|
|
265
|
+
)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snapchat-scraper-api
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Snapchat scraper API client: public profile data, follower counts and Spotlight videos via ScrapingBee.
|
|
5
|
+
Author: wordstotech
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ScrapingBee/snapchat-scraper-api
|
|
8
|
+
Project-URL: Repository, https://github.com/ScrapingBee/snapchat-scraper-api
|
|
9
|
+
Project-URL: Documentation, https://www.scrapingbee.com/documentation/
|
|
10
|
+
Keywords: snapchat scraper,snapchat api,snapchat profiles scraper,snapchat data scraper,web scraping,scrapingbee
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: requests>=2.25.0
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# snapchat-scraper-api
|
|
32
|
+
|
|
33
|
+
A Python client for public Snapchat profile data through ScrapingBee. Built around the fact that most selectors on a Snapchat profile are guaranteed to break, and the ones that are not live somewhere unobvious.
|
|
34
|
+
|
|
35
|
+
**Verified against `snapchat.com/add/teamsnapchat` and `snapchat.com/add/mrbeast` on 2026-09-10.** Three findings below only surface on a live run: there are two incompatible page layouts, the follower count is not in the visible text at all, and the page language depends on which country the request left from.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install snapchat-scraper-api
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.8 or newer and `requests`.
|
|
42
|
+
|
|
43
|
+
## The cheapest target in the series
|
|
44
|
+
|
|
45
|
+
One credit per profile. Snapchat serves these pages as delivered HTML, so `mode=auto` settles on the plain rung and never opens a browser. Do not add `render_js` out of habit, it buys nothing here and costs five times more.
|
|
46
|
+
|
|
47
|
+
## What is in scope
|
|
48
|
+
|
|
49
|
+
Public profile pages, read anonymously: display name, username, subtitle, Snapcode, preview image, canonical URL, follower count, profile creation and last modified dates, Snapchat's family friendly flag, and public Spotlight or highlight videos.
|
|
50
|
+
|
|
51
|
+
Out of scope: private accounts, friend lists, Snap Map location data, direct messages and story view data. Those need a signed in session, and scraping under login credentials is prohibited by ScrapingBee's terms of service.
|
|
52
|
+
|
|
53
|
+
## Authentication
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from snapchat_scraper_api import SnapchatScraper
|
|
57
|
+
|
|
58
|
+
bee = SnapchatScraper("YOUR_API_KEY")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Sent as `Authorization: Bearer YOUR_API_KEY`. Key and 1,000 free credits: [ScrapingBee](https://www.scrapingbee.com/). Landing page: [Snapchat scraper API](https://www.scrapingbee.com/scrapers/snapchat-scraper-api/).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Method reference
|
|
66
|
+
|
|
67
|
+
### `profile(username, country=None)`
|
|
68
|
+
|
|
69
|
+
**1 credit** unpinned, **25 credits** with a country pinned.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
bee.profile("teamsnapchat")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
{'display_name': 'Team Snapchat',
|
|
77
|
+
'username': 'teamsnapchat',
|
|
78
|
+
'subtitle': 'Add me on Snapchat!',
|
|
79
|
+
'snapcode': 'https://app.snapchat.com/web/deeplink/snapcode?username=teamsnapchat&type=SVG&bitmoji=enable',
|
|
80
|
+
'profile_image': 'https://www.snapchat.com/web-capture/www.snapchat.com/@teamsnapchat/preview/square.jpeg?xp_id=1',
|
|
81
|
+
'og_title': 'Team Snapchat on Snapchat',
|
|
82
|
+
'og_description': 'Team Snapchat is on Snapchat!',
|
|
83
|
+
'canonical': 'https://www.snapchat.com/@teamsnapchat'}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The same call works on a creator profile, which matters more than it sounds.
|
|
87
|
+
|
|
88
|
+
**Snapchat serves two incompatible layouts.** Both handles below were fetched with one rule set:
|
|
89
|
+
|
|
90
|
+
| Selector | `teamsnapchat` (basic card) | `mrbeast` (creator profile) |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `h1 span` | empty | `MrBeast` |
|
|
93
|
+
| `h4 span` | `Team Snapchat` | empty |
|
|
94
|
+
| `h5 span` | `teamsnapchat` | wrong node, the page has ten `h5` elements |
|
|
95
|
+
| `[data-testid="snapCodeImage"]` | the Snapcode URL | empty |
|
|
96
|
+
|
|
97
|
+
They are mutually exclusive. This client requests both headings and returns whichever is populated, so one call covers both page types. A scraper written against only `h4 span` works on half of Snapchat and returns empty strings on the other half, with a 200 status and no error.
|
|
98
|
+
|
|
99
|
+
Three more decisions the client makes for you:
|
|
100
|
+
|
|
101
|
+
- **The username comes from the canonical URL**, not from a heading. The canonical is byte identical across layouts and does not change with locale, and it always reads `https://www.snapchat.com/@<username>`.
|
|
102
|
+
- **The Snapcode is rebuilt when the selector is empty.** The endpoint is parameterised by username, and the constructed string matches the live selector value exactly, verified on both handles. No second request.
|
|
103
|
+
- **An empty dict means the handle has no public page.** More on that below.
|
|
104
|
+
|
|
105
|
+
**Never select on CSS module classes.** Snapchat ships class names like `UserDetailsCard_title__K9Awz`, `Heading_h400Emphasis__SQXxl` and `DesktopUserProfile_desktopContainer__UwOc_`. The suffix after the double underscore is a build hash. It rotates on deploy, and a scraper keyed on it silently returns empty strings afterwards.
|
|
106
|
+
|
|
107
|
+
### `stats(username, country=None)`
|
|
108
|
+
|
|
109
|
+
**1 credit.** Follower count and account dates, from the `application/ld+json` block.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
bee.stats("mrbeast")
|
|
113
|
+
# {'name': 'MrBeast',
|
|
114
|
+
# 'username': 'mrbeast',
|
|
115
|
+
# 'url': 'https://www.snapchat.com/@mrbeast',
|
|
116
|
+
# 'image': 'https://cf-st.sc-cdn.net/aps/bolt/...',
|
|
117
|
+
# 'followers': 1463400,
|
|
118
|
+
# 'created': '2019-05-16T14:46:37.345Z',
|
|
119
|
+
# 'modified': '2026-08-13T13:01:39.438Z',
|
|
120
|
+
# 'locale': 'en-US',
|
|
121
|
+
# 'family_friendly': True}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**The follower count is not in the visible text.** The DOM carries the i18n template placeholder, literally `{subscriberCount} suscriptores`, so a text selector returns the template rather than a number. The real value sits in `interactionStatistic`, in the counter whose `interactionType` is `FollowAction`. This client digs it out.
|
|
125
|
+
|
|
126
|
+
`created` and `modified` are the other two fields available nowhere else. `modified` is how you separate an active creator from a dormant handle without fetching a single post.
|
|
127
|
+
|
|
128
|
+
Returns `None` when there is no public profile page.
|
|
129
|
+
|
|
130
|
+
Note this parses the fetched HTML rather than using `extract_rules`, because **`extract_rules` cannot read script tag contents**. A rule selecting `script[type="application/ld+json"]` returns `None`, tested directly.
|
|
131
|
+
|
|
132
|
+
### `spotlight(username, country=None)`
|
|
133
|
+
|
|
134
|
+
**1 credit.** Public Spotlight and highlight videos, from the `ItemList` block.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
bee.spotlight("mrbeast")
|
|
138
|
+
# [{'url': 'https://www.snapchat.com/@mrbeast/highlight/91542d31-...',
|
|
139
|
+
# 'name': 'A Snapchat video by MrBeast',
|
|
140
|
+
# 'description': 'A Snapchat video by MrBeast',
|
|
141
|
+
# 'thumbnail': 'https://cf-st.sc-cdn.net/d/...'}, ...]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Five entries on the creator page tested. **An empty list is normal, not a failure.** The same block came back as an empty array on `teamsnapchat`.
|
|
145
|
+
|
|
146
|
+
### `exists(username)`
|
|
147
|
+
|
|
148
|
+
**1 credit.** Whether the handle has a public profile page.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
bee.exists("teamsnapchat") # True
|
|
152
|
+
bee.exists("dailymail") # False
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A missing handle answers in two different ways, and both were observed on the same handle at different times. One capture returned HTTP 200 with a 5,973 byte body, a bare `Snapchat` title and no `ProfilePage` block. A later capture returned a real **HTTP 404**, forwarded straight through because 404 is one of the few statuses ScrapingBee does not rewrite.
|
|
156
|
+
|
|
157
|
+
This client treats both as "no page" rather than raising, so a batch run does not die on one bad handle. Decide on the presence of the `ProfilePage` block, not on the status code.
|
|
158
|
+
|
|
159
|
+
### `snapcode_url(username, svg=True, bitmoji=True)`
|
|
160
|
+
|
|
161
|
+
A static method. **0 credits, no request.**
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
SnapchatScraper.snapcode_url("teamsnapchat")
|
|
165
|
+
# 'https://app.snapchat.com/web/deeplink/snapcode?username=teamsnapchat&type=SVG&bitmoji=enable'
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `usage()`
|
|
169
|
+
|
|
170
|
+
Free. Account credits, concurrency and renewal date.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## The locale trap
|
|
175
|
+
|
|
176
|
+
Snapchat localises by proxy exit IP, and without a country parameter whichever region the request left from decides the language of every title, subtitle and label you parse.
|
|
177
|
+
|
|
178
|
+
Two captures of the same creator URL, same parameters, no change in between:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
capture 1 title: MrBeast (@mrbeast) | Historias de Snapchat, Spotlight y Lentes
|
|
182
|
+
capture 2 og_title: MrBeast pe Snapchat og_description: MrBeast este pe Snapchat!
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Spanish, then Romanian. The structured data tells you it happened, through `inLanguage`, which `stats()` returns as `locale`.
|
|
186
|
+
|
|
187
|
+
What moves and what does not:
|
|
188
|
+
|
|
189
|
+
| Field | Locale dependent |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `og_title`, `og_description`, page title, visible labels | yes |
|
|
192
|
+
| `canonical`, `username`, `profile_image`, `snapcode` | no |
|
|
193
|
+
| `followers`, `created`, `modified` | no |
|
|
194
|
+
|
|
195
|
+
So pin the country only when you actually read the strings:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
bee.profile("mrbeast", country="us") # 25 credits, English guaranteed
|
|
199
|
+
bee.stats("mrbeast") # 1 credit, the number is the same either way
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Pinning gave an identical `followers` value on both runs. Geotargeting requires the premium tier, which is a twenty five times cost increase, so do not turn it on for numeric work.
|
|
203
|
+
|
|
204
|
+
## Credit cost
|
|
205
|
+
|
|
206
|
+
Measured from `spb-cost` headers. Available on `bee.last_cost`.
|
|
207
|
+
|
|
208
|
+
| Configuration | Credits |
|
|
209
|
+
|---|---|
|
|
210
|
+
| `mode=auto`, settled on plain HTML | 1 |
|
|
211
|
+
| `premium_proxy` with `country_code` | 25 |
|
|
212
|
+
| Validation error | 0 |
|
|
213
|
+
|
|
214
|
+
`mode=auto` bills only the rung that worked and nothing if every rung fails. It is incompatible with `render_js`, `premium_proxy` and `stealth_proxy`, and sending both returns HTTP 400 while billing nothing.
|
|
215
|
+
|
|
216
|
+
At 1 credit per profile, 250,000 credits covers 250,000 profile checks. Plan tiers: [ScrapingBee pricing](https://www.scrapingbee.com/pricing).
|
|
217
|
+
|
|
218
|
+
## Related
|
|
219
|
+
|
|
220
|
+
Adjacent social and creator landing pages: [Patreon scraper API](https://www.scrapingbee.com/scrapers/patreon-api/), [TikTok API](https://www.scrapingbee.com/scrapers/tiktok-api/), [TikTok search API](https://www.scrapingbee.com/scrapers/tiktok-search-api/), [TikTok follower API](https://www.scrapingbee.com/scrapers/tiktok-follower/), [Twitch API](https://www.scrapingbee.com/scrapers/twitch-api/), [Substack scraper API](https://www.scrapingbee.com/scrapers/substack-scraper-api/), [YouTube shorts API](https://www.scrapingbee.com/scrapers/youtube-shorts-api/), [YouTube video scraper API](https://www.scrapingbee.com/scrapers/youtube-video-scraper-api/), [YouTube title scraper API](https://www.scrapingbee.com/scrapers/youtube-title-scraper-api/).
|
|
221
|
+
|
|
222
|
+
Features: [data extraction](https://www.scrapingbee.com/features/data-extraction/), [AI web scraping](https://www.scrapingbee.com/features/ai-web-scraping-api/), [screenshots](https://www.scrapingbee.com/features/screenshot/), [markdown scraper](https://www.scrapingbee.com/features/markdown-scraper/), [JavaScript scenario](https://www.scrapingbee.com/features/javascript-scenario/), [n8n integration](https://www.scrapingbee.com/features/n8n/).
|
|
223
|
+
|
|
224
|
+
Reference: [extraction rules documentation](https://www.scrapingbee.com/documentation/data-extraction/). The selector durability walkthrough is at [github.com/ScrapingBee/snapchat-scraper-api](https://github.com/ScrapingBee/snapchat-scraper-api).
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/snapchat_scraper_api/__init__.py
|
|
6
|
+
src/snapchat_scraper_api/client.py
|
|
7
|
+
src/snapchat_scraper_api.egg-info/PKG-INFO
|
|
8
|
+
src/snapchat_scraper_api.egg-info/SOURCES.txt
|
|
9
|
+
src/snapchat_scraper_api.egg-info/dependency_links.txt
|
|
10
|
+
src/snapchat_scraper_api.egg-info/requires.txt
|
|
11
|
+
src/snapchat_scraper_api.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.25.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
snapchat_scraper_api
|