blinkit-scraper 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.
- blinkit_scraper-0.1.0/LICENSE +21 -0
- blinkit_scraper-0.1.0/MANIFEST.in +3 -0
- blinkit_scraper-0.1.0/PKG-INFO +135 -0
- blinkit_scraper-0.1.0/README.md +113 -0
- blinkit_scraper-0.1.0/examples/compare_cities.py +17 -0
- blinkit_scraper-0.1.0/pyproject.toml +34 -0
- blinkit_scraper-0.1.0/setup.cfg +4 -0
- blinkit_scraper-0.1.0/src/blinkit_scraper/__init__.py +33 -0
- blinkit_scraper-0.1.0/src/blinkit_scraper/cli.py +117 -0
- blinkit_scraper-0.1.0/src/blinkit_scraper/client.py +183 -0
- blinkit_scraper-0.1.0/src/blinkit_scraper/parser.py +254 -0
- blinkit_scraper-0.1.0/src/blinkit_scraper.egg-info/PKG-INFO +135 -0
- blinkit_scraper-0.1.0/src/blinkit_scraper.egg-info/SOURCES.txt +19 -0
- blinkit_scraper-0.1.0/src/blinkit_scraper.egg-info/dependency_links.txt +1 -0
- blinkit_scraper-0.1.0/src/blinkit_scraper.egg-info/entry_points.txt +2 -0
- blinkit_scraper-0.1.0/src/blinkit_scraper.egg-info/requires.txt +1 -0
- blinkit_scraper-0.1.0/src/blinkit_scraper.egg-info/top_level.txt +1 -0
- blinkit_scraper-0.1.0/tests/fixtures/category_page.html +4 -0
- blinkit_scraper-0.1.0/tests/fixtures/listing_api.json +115 -0
- blinkit_scraper-0.1.0/tests/fixtures/product_page.html +4 -0
- blinkit_scraper-0.1.0/tests/test_parser.py +164 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Buğrahan Saka
|
|
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,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blinkit-scraper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scrape Blinkit categories and product pages (price, MRP, stock per dark store, FSSAI licence, shelf life) with Python.
|
|
5
|
+
Author: Buğrahan Saka
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bugrahansss/blinkit-scraper
|
|
8
|
+
Project-URL: Source, https://github.com/bugrahansss/blinkit-scraper
|
|
9
|
+
Project-URL: Issues, https://github.com/bugrahansss/blinkit-scraper/issues
|
|
10
|
+
Project-URL: Scrape.do, https://scrape.do/?utm_source=pypi&utm_medium=package&utm_campaign=blinkit-scraper
|
|
11
|
+
Keywords: blinkit,web scraping,quick commerce,grocery,scraper,cloudflare,india
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: requests>=2.25
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# blinkit-scraper
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/blinkit-scraper/)
|
|
26
|
+
[](https://pypi.org/project/blinkit-scraper/)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
|
|
29
|
+
A Python scraper for [Blinkit](https://blinkit.com), India's 10-minute grocery app. Two jobs:
|
|
30
|
+
|
|
31
|
+
- **Category listings:** any category page (`/cn/chips-crisps/cid/1237/940`) → name, brand, pack size, price, MRP, discount, **units in stock**, rating, rating count, variants and URL, **for the dark store that serves your location**. Real pagination: 30 products on the first page, 15 per page after that, out-of-stock products kept.
|
|
32
|
+
- **Product pages:** price, MRP and stock plus the catalog sheet: **FSSAI licence, shelf life, country of origin, ingredients, manufacturer, return policy** and every other attribute Blinkit lists.
|
|
33
|
+
|
|
34
|
+
Output as Python dicts, JSON or CSV. One dependency (`requests`), no browser.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install blinkit-scraper
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Why a plain request fails
|
|
41
|
+
|
|
42
|
+
Blinkit sits behind Cloudflare. A script gets an error page, not products:
|
|
43
|
+
|
|
44
|
+
| Request | Result |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `requests`, default or Chrome user agent | HTTP 403, "blinkit \| Error Page" |
|
|
47
|
+
| Through Scrape.do (`super=true&geoCode=in`) | 200 in ~3 s, full page with the data, 5 of 5 tries |
|
|
48
|
+
|
|
49
|
+
Pages are fetched through the [Scrape.do](https://scrape.do/?utm_source=github&utm_medium=readme&utm_campaign=blinkit-scraper) API with residential proxies in India. Blinkit requests cost 10 credits each; the free plan includes 1,000 credits a month.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from blinkit_scraper import BlinkitScraper
|
|
55
|
+
|
|
56
|
+
scraper = BlinkitScraper(token="YOUR_SCRAPEDO_TOKEN", city="mumbai") # or lat=..., lon=...
|
|
57
|
+
|
|
58
|
+
# 3 pages = 30 + 15 + 15 = 60 products, 3 requests
|
|
59
|
+
rows = scraper.get_category("https://blinkit.com/cn/chips-crisps/cid/1237/940", max_pages=3)
|
|
60
|
+
for row in rows[:3]:
|
|
61
|
+
print(row["name"], row["pack_size"], row["price_inr"], row["mrp_inr"], row["inventory"], row["in_stock"])
|
|
62
|
+
|
|
63
|
+
product = scraper.get_product(rows[0]["url"])
|
|
64
|
+
print(product["fssai_license"], product["shelf_life"], product["country_of_origin"])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Command line
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export SCRAPEDO_TOKEN=your_token
|
|
71
|
+
|
|
72
|
+
# 3 pages of a category for a Mumbai address, CSV
|
|
73
|
+
blinkit-scraper category "https://blinkit.com/cn/chips-crisps/cid/1237/940" --pages 3 --city mumbai -f csv -o blinkit-chips.csv
|
|
74
|
+
|
|
75
|
+
# product sheets for the first 10 products of that file
|
|
76
|
+
blinkit-scraper product -i blinkit-chips.csv --limit 10 --city mumbai -f csv -o blinkit-products.csv
|
|
77
|
+
|
|
78
|
+
# any address: pass its coordinates
|
|
79
|
+
blinkit-scraper category "https://blinkit.com/cn/chips-crisps/cid/1237/940" --lat 12.9352 --lon 77.6245
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
City presets: `bengaluru`, `chennai`, `delhi`, `gurugram` (Blinkit's default), `hyderabad`, `kolkata`, `mumbai`, `pune`.
|
|
83
|
+
|
|
84
|
+
### Output
|
|
85
|
+
|
|
86
|
+
Listing row (from the fictional test page in [`tests/fixtures`](tests/fixtures/category_page.html)):
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{"product_id": 1001, "name": "Example Salted Chips", "brand": "Northwind Snacks", "pack_size": "60 g",
|
|
90
|
+
"price_inr": 43, "mrp_inr": 70, "discount_pct": 38, "in_stock": true, "inventory": 5,
|
|
91
|
+
"rating": 4.31, "rating_count": 120000, "variants": 2, "merchant_id": 90001,
|
|
92
|
+
"image": "https://cdn.example.com/p/1001.png", "url": "https://blinkit.com/prn/example-salted-chips/prid/1001"}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Product columns: `product_id, name, brand, pack_size, price_inr, mrp_inr, discount_pct, in_stock, inventory, shelf_life, fssai_license, country_of_origin, category_ids, merchant_id, url` (plus `ingredients`, `manufacturer`, `return_policy`, `images` and the full `attributes` dict in Python/JSON).
|
|
96
|
+
|
|
97
|
+
## How it works
|
|
98
|
+
|
|
99
|
+
**The data is one JSON object.** Blinkit renders every page on the server and puts the data in `window.grofers.PRELOADED_STATE = {...}`. Category pages keep the product cards in `ui.plpContainer.feedData.snippets`, product pages keep the catalog sheet in `ui.pdp.bffPdp.bffData.tracking.le_meta.custom_data.seo`. The scraper reads the object with `json.JSONDecoder.raw_decode`. No CSS selectors.
|
|
100
|
+
|
|
101
|
+
**Each card carries the stock count.** `inventory` is the number of units the dark store has (Blinkit stops counting at 50). `rating_count` arrives as text in Indian units ("7 lac" = 700,000) and is converted.
|
|
102
|
+
|
|
103
|
+
**Location picks the store.** Prices, assortment and stock belong to one dark store, chosen from the delivery address. Without one, Blinkit uses Gurugram. In our test the chips category listed 673 products in Gurugram (store 31719) and 524 in Mumbai (store 41649); a banana chips pack had 5 units in one and 1 in the other. Pages read the location from the `gr_1_lat` / `gr_1_lon` cookies, the listing API from `lat` / `lon` headers; the scraper sends both.
|
|
104
|
+
|
|
105
|
+
**Pagination is a POST API.** The page holds 30 products and a `next_url` for `/v1/layout/listing_widgets`. That endpoint returns 15 products per call (a larger `limit` is ignored) and the next `next_url`. It only answers through residential proxies, so every request uses `super=true&geoCode=in`. Reading a whole category of 673 products takes 44 requests.
|
|
106
|
+
|
|
107
|
+
## Limits
|
|
108
|
+
|
|
109
|
+
- **Credits.** 10 Scrape.do credits per request: page 1 of a category, each further page of 15, each product page.
|
|
110
|
+
- **Stock cap.** `inventory` tops out at 50.
|
|
111
|
+
- **Search.** `/s/` search pages are disallowed in Blinkit's robots.txt and not supported; use category pages.
|
|
112
|
+
- **Errors.** Non-200 responses raise `ScrapeError` with the status code; `from_target` tells a Blinkit status from a Scrape.do one (a wrong token is a Scrape.do 401). A 200 without Blinkit's data is retried. Server errors, 429s and empty pages are retried twice; client errors are not.
|
|
113
|
+
|
|
114
|
+
## Responsible use
|
|
115
|
+
|
|
116
|
+
The scraper reads category (`/cn/`) and product (`/prn/`) pages, which Blinkit's robots.txt allows, plus the listing API those pages call. It never touches search, cart, checkout or account paths. Product data is not personal data. Read Blinkit's terms for your use case and keep request rates modest. This is not legal advice.
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
git clone https://github.com/bugrahansss/blinkit-scraper
|
|
122
|
+
cd blinkit-scraper
|
|
123
|
+
pip install -e .
|
|
124
|
+
python -m unittest discover -s tests -v
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The tests run offline against fictional fixture pages.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
Built and tested with [Scrape.do](https://scrape.do/?utm_source=github&utm_medium=readme&utm_campaign=blinkit-scraper), November 2026.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# blinkit-scraper
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/blinkit-scraper/)
|
|
4
|
+
[](https://pypi.org/project/blinkit-scraper/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A Python scraper for [Blinkit](https://blinkit.com), India's 10-minute grocery app. Two jobs:
|
|
8
|
+
|
|
9
|
+
- **Category listings:** any category page (`/cn/chips-crisps/cid/1237/940`) → name, brand, pack size, price, MRP, discount, **units in stock**, rating, rating count, variants and URL, **for the dark store that serves your location**. Real pagination: 30 products on the first page, 15 per page after that, out-of-stock products kept.
|
|
10
|
+
- **Product pages:** price, MRP and stock plus the catalog sheet: **FSSAI licence, shelf life, country of origin, ingredients, manufacturer, return policy** and every other attribute Blinkit lists.
|
|
11
|
+
|
|
12
|
+
Output as Python dicts, JSON or CSV. One dependency (`requests`), no browser.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install blinkit-scraper
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Why a plain request fails
|
|
19
|
+
|
|
20
|
+
Blinkit sits behind Cloudflare. A script gets an error page, not products:
|
|
21
|
+
|
|
22
|
+
| Request | Result |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `requests`, default or Chrome user agent | HTTP 403, "blinkit \| Error Page" |
|
|
25
|
+
| Through Scrape.do (`super=true&geoCode=in`) | 200 in ~3 s, full page with the data, 5 of 5 tries |
|
|
26
|
+
|
|
27
|
+
Pages are fetched through the [Scrape.do](https://scrape.do/?utm_source=github&utm_medium=readme&utm_campaign=blinkit-scraper) API with residential proxies in India. Blinkit requests cost 10 credits each; the free plan includes 1,000 credits a month.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from blinkit_scraper import BlinkitScraper
|
|
33
|
+
|
|
34
|
+
scraper = BlinkitScraper(token="YOUR_SCRAPEDO_TOKEN", city="mumbai") # or lat=..., lon=...
|
|
35
|
+
|
|
36
|
+
# 3 pages = 30 + 15 + 15 = 60 products, 3 requests
|
|
37
|
+
rows = scraper.get_category("https://blinkit.com/cn/chips-crisps/cid/1237/940", max_pages=3)
|
|
38
|
+
for row in rows[:3]:
|
|
39
|
+
print(row["name"], row["pack_size"], row["price_inr"], row["mrp_inr"], row["inventory"], row["in_stock"])
|
|
40
|
+
|
|
41
|
+
product = scraper.get_product(rows[0]["url"])
|
|
42
|
+
print(product["fssai_license"], product["shelf_life"], product["country_of_origin"])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Command line
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
export SCRAPEDO_TOKEN=your_token
|
|
49
|
+
|
|
50
|
+
# 3 pages of a category for a Mumbai address, CSV
|
|
51
|
+
blinkit-scraper category "https://blinkit.com/cn/chips-crisps/cid/1237/940" --pages 3 --city mumbai -f csv -o blinkit-chips.csv
|
|
52
|
+
|
|
53
|
+
# product sheets for the first 10 products of that file
|
|
54
|
+
blinkit-scraper product -i blinkit-chips.csv --limit 10 --city mumbai -f csv -o blinkit-products.csv
|
|
55
|
+
|
|
56
|
+
# any address: pass its coordinates
|
|
57
|
+
blinkit-scraper category "https://blinkit.com/cn/chips-crisps/cid/1237/940" --lat 12.9352 --lon 77.6245
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
City presets: `bengaluru`, `chennai`, `delhi`, `gurugram` (Blinkit's default), `hyderabad`, `kolkata`, `mumbai`, `pune`.
|
|
61
|
+
|
|
62
|
+
### Output
|
|
63
|
+
|
|
64
|
+
Listing row (from the fictional test page in [`tests/fixtures`](tests/fixtures/category_page.html)):
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{"product_id": 1001, "name": "Example Salted Chips", "brand": "Northwind Snacks", "pack_size": "60 g",
|
|
68
|
+
"price_inr": 43, "mrp_inr": 70, "discount_pct": 38, "in_stock": true, "inventory": 5,
|
|
69
|
+
"rating": 4.31, "rating_count": 120000, "variants": 2, "merchant_id": 90001,
|
|
70
|
+
"image": "https://cdn.example.com/p/1001.png", "url": "https://blinkit.com/prn/example-salted-chips/prid/1001"}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Product columns: `product_id, name, brand, pack_size, price_inr, mrp_inr, discount_pct, in_stock, inventory, shelf_life, fssai_license, country_of_origin, category_ids, merchant_id, url` (plus `ingredients`, `manufacturer`, `return_policy`, `images` and the full `attributes` dict in Python/JSON).
|
|
74
|
+
|
|
75
|
+
## How it works
|
|
76
|
+
|
|
77
|
+
**The data is one JSON object.** Blinkit renders every page on the server and puts the data in `window.grofers.PRELOADED_STATE = {...}`. Category pages keep the product cards in `ui.plpContainer.feedData.snippets`, product pages keep the catalog sheet in `ui.pdp.bffPdp.bffData.tracking.le_meta.custom_data.seo`. The scraper reads the object with `json.JSONDecoder.raw_decode`. No CSS selectors.
|
|
78
|
+
|
|
79
|
+
**Each card carries the stock count.** `inventory` is the number of units the dark store has (Blinkit stops counting at 50). `rating_count` arrives as text in Indian units ("7 lac" = 700,000) and is converted.
|
|
80
|
+
|
|
81
|
+
**Location picks the store.** Prices, assortment and stock belong to one dark store, chosen from the delivery address. Without one, Blinkit uses Gurugram. In our test the chips category listed 673 products in Gurugram (store 31719) and 524 in Mumbai (store 41649); a banana chips pack had 5 units in one and 1 in the other. Pages read the location from the `gr_1_lat` / `gr_1_lon` cookies, the listing API from `lat` / `lon` headers; the scraper sends both.
|
|
82
|
+
|
|
83
|
+
**Pagination is a POST API.** The page holds 30 products and a `next_url` for `/v1/layout/listing_widgets`. That endpoint returns 15 products per call (a larger `limit` is ignored) and the next `next_url`. It only answers through residential proxies, so every request uses `super=true&geoCode=in`. Reading a whole category of 673 products takes 44 requests.
|
|
84
|
+
|
|
85
|
+
## Limits
|
|
86
|
+
|
|
87
|
+
- **Credits.** 10 Scrape.do credits per request: page 1 of a category, each further page of 15, each product page.
|
|
88
|
+
- **Stock cap.** `inventory` tops out at 50.
|
|
89
|
+
- **Search.** `/s/` search pages are disallowed in Blinkit's robots.txt and not supported; use category pages.
|
|
90
|
+
- **Errors.** Non-200 responses raise `ScrapeError` with the status code; `from_target` tells a Blinkit status from a Scrape.do one (a wrong token is a Scrape.do 401). A 200 without Blinkit's data is retried. Server errors, 429s and empty pages are retried twice; client errors are not.
|
|
91
|
+
|
|
92
|
+
## Responsible use
|
|
93
|
+
|
|
94
|
+
The scraper reads category (`/cn/`) and product (`/prn/`) pages, which Blinkit's robots.txt allows, plus the listing API those pages call. It never touches search, cart, checkout or account paths. Product data is not personal data. Read Blinkit's terms for your use case and keep request rates modest. This is not legal advice.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
git clone https://github.com/bugrahansss/blinkit-scraper
|
|
100
|
+
cd blinkit-scraper
|
|
101
|
+
pip install -e .
|
|
102
|
+
python -m unittest discover -s tests -v
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The tests run offline against fictional fixture pages.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
Built and tested with [Scrape.do](https://scrape.do/?utm_source=github&utm_medium=readme&utm_campaign=blinkit-scraper), November 2026.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Same category, two cities: which products and how much stock each dark store has."""
|
|
2
|
+
from blinkit_scraper import BlinkitScraper
|
|
3
|
+
|
|
4
|
+
URL = "https://blinkit.com/cn/chips-crisps/cid/1237/940"
|
|
5
|
+
|
|
6
|
+
stores = {}
|
|
7
|
+
for city in ("gurugram", "mumbai"):
|
|
8
|
+
scraper = BlinkitScraper(city=city) # token from SCRAPEDO_TOKEN
|
|
9
|
+
info = scraper.get_category(URL, max_pages=2, with_info=True)
|
|
10
|
+
stores[city] = {row["product_id"]: row for row in info["products"]}
|
|
11
|
+
print(f"{city}: store {info['merchant_id']}, {info['total']} products listed, {scraper.credits_used} credits")
|
|
12
|
+
|
|
13
|
+
both = stores["gurugram"].keys() & stores["mumbai"].keys()
|
|
14
|
+
print(f"{len(both)} products on page 1-2 in both cities")
|
|
15
|
+
for pid in list(both)[:10]:
|
|
16
|
+
g, m = stores["gurugram"][pid], stores["mumbai"][pid]
|
|
17
|
+
print(f"{g['name'][:40]:40} {g['price_inr']:>5} {m['price_inr']:>5} stock {g['inventory']:>3} / {m['inventory']:>3}")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "blinkit-scraper"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Scrape Blinkit categories and product pages (price, MRP, stock per dark store, FSSAI licence, shelf life) with Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Buğrahan Saka" }]
|
|
14
|
+
keywords = ["blinkit", "web scraping", "quick commerce", "grocery", "scraper", "cloudflare", "india"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Internet :: WWW/HTTP :: Indexing/Search",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["requests>=2.25"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/bugrahansss/blinkit-scraper"
|
|
26
|
+
Source = "https://github.com/bugrahansss/blinkit-scraper"
|
|
27
|
+
Issues = "https://github.com/bugrahansss/blinkit-scraper/issues"
|
|
28
|
+
"Scrape.do" = "https://scrape.do/?utm_source=pypi&utm_medium=package&utm_campaign=blinkit-scraper"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
blinkit-scraper = "blinkit_scraper.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Scrape Blinkit category listings and product pages (price, MRP, stock per dark store) with Python."""
|
|
2
|
+
|
|
3
|
+
from .client import CITIES, BlinkitScraper, ScrapeError
|
|
4
|
+
from .parser import (
|
|
5
|
+
LISTING_FIELDS,
|
|
6
|
+
PRODUCT_FIELDS,
|
|
7
|
+
extract_state,
|
|
8
|
+
parse_card,
|
|
9
|
+
parse_cards,
|
|
10
|
+
parse_category,
|
|
11
|
+
parse_count,
|
|
12
|
+
parse_listing_api,
|
|
13
|
+
parse_product,
|
|
14
|
+
product_url,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CITIES",
|
|
19
|
+
"LISTING_FIELDS",
|
|
20
|
+
"PRODUCT_FIELDS",
|
|
21
|
+
"BlinkitScraper",
|
|
22
|
+
"ScrapeError",
|
|
23
|
+
"extract_state",
|
|
24
|
+
"parse_card",
|
|
25
|
+
"parse_cards",
|
|
26
|
+
"parse_category",
|
|
27
|
+
"parse_count",
|
|
28
|
+
"parse_listing_api",
|
|
29
|
+
"parse_product",
|
|
30
|
+
"product_url",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Command line entry point: ``blinkit-scraper category URL`` / ``blinkit-scraper product URL ...``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import csv
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from . import __version__
|
|
12
|
+
from .client import CITIES, TOKEN_ENV, BlinkitScraper, ScrapeError
|
|
13
|
+
from .parser import LISTING_FIELDS, PRODUCT_FIELDS
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="blinkit-scraper",
|
|
19
|
+
description="Scrape Blinkit categories and product pages (price, MRP, stock per dark store) into JSON or CSV.",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
22
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
23
|
+
|
|
24
|
+
cat = sub.add_parser("category", help="Products of a category page (30 on page 1, 15 per further page)")
|
|
25
|
+
cat.add_argument("url", help="A Blinkit category URL, e.g. https://blinkit.com/cn/chips-crisps/cid/1237/940")
|
|
26
|
+
cat.add_argument("--pages", type=int, default=1, help="Pages to read (default 1)")
|
|
27
|
+
|
|
28
|
+
prod = sub.add_parser("product", help="Product pages: price, stock, FSSAI licence, shelf life, attributes")
|
|
29
|
+
prod.add_argument("urls", nargs="*", help="Blinkit product URLs or product ids")
|
|
30
|
+
prod.add_argument("-i", "--input", help="Text file with one URL per line, or a CSV with a 'url' column")
|
|
31
|
+
prod.add_argument("--limit", type=int, default=0, help="Stop after this many products (0 = all)")
|
|
32
|
+
|
|
33
|
+
for p in (cat, prod):
|
|
34
|
+
p.add_argument("-t", "--token", help=f"Scrape.do API token (default: ${TOKEN_ENV})")
|
|
35
|
+
p.add_argument("--city", choices=sorted(CITIES), help="Delivery location preset (default: Gurugram)")
|
|
36
|
+
p.add_argument("--lat", type=float, help="Delivery latitude (with --lon)")
|
|
37
|
+
p.add_argument("--lon", type=float, help="Delivery longitude (with --lat)")
|
|
38
|
+
p.add_argument("-f", "--format", choices=("json", "csv"), default="json")
|
|
39
|
+
p.add_argument("-o", "--output", help="Write to this file instead of stdout")
|
|
40
|
+
return parser
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_urls(args: argparse.Namespace) -> List[str]:
|
|
44
|
+
urls = list(args.urls)
|
|
45
|
+
if args.input:
|
|
46
|
+
with open(args.input, encoding="utf-8", newline="") as handle:
|
|
47
|
+
if args.input.endswith(".csv"):
|
|
48
|
+
urls.extend(row["url"] for row in csv.DictReader(handle) if row.get("url"))
|
|
49
|
+
else:
|
|
50
|
+
urls.extend(line.strip() for line in handle if line.strip() and not line.startswith("#"))
|
|
51
|
+
return urls[: args.limit] if args.limit else urls
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _write(rows: List[dict], fields: List[str], fmt: str, stream) -> None:
|
|
55
|
+
if fmt == "csv":
|
|
56
|
+
writer = csv.DictWriter(stream, fieldnames=fields, extrasaction="ignore")
|
|
57
|
+
writer.writeheader()
|
|
58
|
+
writer.writerows(rows)
|
|
59
|
+
else:
|
|
60
|
+
json.dump(rows, stream, indent=2, ensure_ascii=False)
|
|
61
|
+
stream.write("\n")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
65
|
+
args = build_parser().parse_args(argv)
|
|
66
|
+
if (args.lat is None) != (args.lon is None):
|
|
67
|
+
print("Pass both --lat and --lon.", file=sys.stderr)
|
|
68
|
+
return 2
|
|
69
|
+
try:
|
|
70
|
+
scraper = BlinkitScraper(token=args.token, lat=args.lat, lon=args.lon, city=args.city)
|
|
71
|
+
except ValueError as exc:
|
|
72
|
+
print(exc, file=sys.stderr)
|
|
73
|
+
return 2
|
|
74
|
+
|
|
75
|
+
failures = 0
|
|
76
|
+
if args.command == "category":
|
|
77
|
+
fields = LISTING_FIELDS
|
|
78
|
+
try:
|
|
79
|
+
info = scraper.get_category(args.url, max_pages=max(1, args.pages), with_info=True)
|
|
80
|
+
except (ScrapeError, ValueError) as exc:
|
|
81
|
+
print(f"fail {args.url}: {exc}", file=sys.stderr)
|
|
82
|
+
return 1
|
|
83
|
+
rows = info["products"]
|
|
84
|
+
print(f"{len(rows)} of {info['total']} products, store {info['merchant_id']} ({info['locality']}), "
|
|
85
|
+
f"{scraper.credits_used} credits", file=sys.stderr)
|
|
86
|
+
else:
|
|
87
|
+
fields = PRODUCT_FIELDS
|
|
88
|
+
urls = _read_urls(args)
|
|
89
|
+
if not urls:
|
|
90
|
+
print("No URLs given. Pass product URLs or --input FILE.", file=sys.stderr)
|
|
91
|
+
return 2
|
|
92
|
+
rows = []
|
|
93
|
+
for url in urls:
|
|
94
|
+
try:
|
|
95
|
+
product = scraper.get_product(url)
|
|
96
|
+
except (ScrapeError, ValueError) as exc:
|
|
97
|
+
failures += 1
|
|
98
|
+
print(f"fail {url}: {exc}", file=sys.stderr)
|
|
99
|
+
continue
|
|
100
|
+
if not product:
|
|
101
|
+
failures += 1
|
|
102
|
+
print(f"fail {url}: no product data", file=sys.stderr)
|
|
103
|
+
continue
|
|
104
|
+
rows.append(product)
|
|
105
|
+
print(f"ok {url}", file=sys.stderr)
|
|
106
|
+
|
|
107
|
+
if args.output:
|
|
108
|
+
with open(args.output, "w", encoding="utf-8", newline="" if args.format == "csv" else None) as handle:
|
|
109
|
+
_write(rows, fields, args.format, handle)
|
|
110
|
+
print(f"Saved {len(rows)} rows to {args.output}", file=sys.stderr)
|
|
111
|
+
else:
|
|
112
|
+
_write(rows, fields, args.format, sys.stdout)
|
|
113
|
+
return 1 if failures and not rows else 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
raise SystemExit(main())
|