gem-scrapscrap 1.0.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.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: gem-scrapscrap
3
+ Version: 1.0.0
4
+ Summary: CLI tool to scrape and download contract documents from GeM (Government e-Marketplace)
5
+ Home-page: https://github.com/Wickcore/gem-scrapscrap
6
+ Author: Wickcore
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/Wickcore/gem-scrapscrap
9
+ Project-URL: Repository, https://github.com/Wickcore/gem-scrapscrap
10
+ Project-URL: Issues, https://github.com/Wickcore/gem-scrapscrap/issues
11
+ Keywords: gem,government,scraping,contracts,tenders
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: requests>=2.28.0
27
+ Requires-Dist: beautifulsoup4>=4.12.0
28
+ Requires-Dist: rich>=13.0.0
29
+ Dynamic: home-page
30
+ Dynamic: requires-python
31
+
32
+ # GeM Document Scraper
33
+
34
+ A Python web app that scrapes and downloads contract documents from [GeM (Government e-Marketplace)](https://gem.gov.in). Enter a category, set a date range, and download all matching contract PDFs — with automatic CAPTCHA solving.
35
+
36
+ ## Features
37
+
38
+ - **Web UI** — Clean dark-themed interface, no terminal needed
39
+ - **Category Search** — Autocomplete from GeM's 13,000+ categories
40
+ - **Auto CAPTCHA** — Solves GeM's image CAPTCHAs via Tesseract OCR
41
+ - **PDF Downloads** — Downloads individual files or all as ZIP
42
+ - **Real-time Progress** — Live logs and progress bar while scraping
43
+ - **Background Processing** — Scraper runs in a thread, UI stays responsive
44
+
45
+ ## Demo
46
+
47
+ 1. Enter a category (e.g., "Ball Point Pens (V2) as per IS 3705")
48
+ 2. Set date range (DD-MM-YYYY)
49
+ 3. Click "Start Scraping"
50
+ 4. Watch live progress, download PDFs when done
51
+
52
+ ## Quick Start
53
+
54
+ ### Prerequisites
55
+
56
+ - Python 3.9+
57
+
58
+ ### Install
59
+
60
+ ```bash
61
+ pip3 install -r requirements.txt
62
+ ```
63
+
64
+ ### Run CLI (Interactive Terminal)
65
+
66
+ ```bash
67
+ python3 gem_cli.py
68
+ ```
69
+
70
+ ### Run Web UI
71
+
72
+ ```bash
73
+ python3 app.py
74
+ ```
75
+
76
+ Open **http://localhost:5000** in your browser.
77
+
78
+ ## How It Works
79
+
80
+ 1. **Search** — User enters category + date range in the web UI
81
+ 2. **CAPTCHA** — Opens GeM's contract page, extracts the CAPTCHA image, reads it via Tesseract OCR, submits automatically (retries up to 15 times)
82
+ 3. **Scrape** — Collects all matching contract documents from the results
83
+ 4. **Download** — For each document, calls GeM's AJAX endpoint to get the download URL, then fetches the PDF via HTTP
84
+ 5. **Serve** — Downloaded PDFs are served back through the web UI for browsing/downloading
85
+
86
+ ## Project Structure
87
+
88
+ ```
89
+ GeM-Document-Scraper/
90
+ ├── app.py # Flask web server (routes, job management, threading)
91
+ ├── scraper.py # Background scraping engine (Selenium + AJAX downloads)
92
+ ├── actions.py # Selenium helper functions (category, dates, CAPTCHA)
93
+ ├── main.py # Legacy CLI entry point (still works standalone)
94
+ ├── templates/
95
+ │ └── index.html # Web UI (HTML + CSS + JS, no frameworks)
96
+ ├── requirements.txt # Python dependencies
97
+ ├── downloads/ # Downloaded PDFs (per job)
98
+ └── captcha/ # CAPTCHA images (temporary)
99
+ ```
100
+
101
+ ## API Endpoints
102
+
103
+ | Method | Route | Description |
104
+ |--------|-------|-------------|
105
+ | GET | `/` | Web UI |
106
+ | POST | `/scrape` | Start a new scraping job |
107
+ | GET | `/status/<job_id>` | Job status + logs + file list |
108
+ | GET | `/download/<job_id>/<file>` | Download a single PDF |
109
+ | GET | `/download_all/<job_id>` | Download all PDFs as ZIP |
110
+
111
+ ## Tested Categories
112
+
113
+ These categories are known to have contracts on GeM:
114
+
115
+ | Category | Notes |
116
+ |----------|-------|
117
+ | `Ball Point Pens (V2) as per IS 3705` | Office supplies, high volume |
118
+ | `Note Sorting Machines (V2)` | Banking equipment |
119
+ | `LED Bulb with Battery as per IS 16102` | Govt LED scheme |
120
+ | `Split Air Conditioner, Wall Mount Type (V3) ISI Marked to IS 1391 (Part 2)` | HVAC |
121
+ | `Nitrogen Tyre Inflators` | Automotive |
122
+
123
+ Use a wide date range (e.g., `01-01-2025` to `07-07-2025`) for more results.
124
+
125
+ ## Date Format
126
+
127
+ The web UI accepts multiple formats and auto-converts:
128
+ - `DD-MM-YYYY` (e.g., 01-07-2025)
129
+ - `DD/MM/YYYY` (e.g., 01/07/2025)
130
+ - `YYYY-MM-DD` (e.g., 2025-07-01)
131
+
132
+ ## Dependencies
133
+
134
+ | Package | Purpose |
135
+ |---------|---------|
136
+ | Flask | Web server |
137
+ | Selenium | Browser automation |
138
+ | Tesseract OCR (`pytesseract`) | CAPTCHA reading |
139
+ | Pillow | Image processing |
140
+ | Requests | HTTP file downloads |
141
+ | Gunicorn | Production WSGI server |
142
+
143
+ ## Deploy to Railway
144
+
145
+ 1. Go to [railway.app](https://railway.app) → Sign up with GitHub
146
+ 2. Click **New Project** → **Deploy from GitHub repo**
147
+ 3. Select `Wickcore/gem-scrapscrap`
148
+ 4. Railway auto-detects the Dockerfile
149
+ 5. Click **Deploy**
150
+
151
+ Your app will be live at `https://your-app.up.railway.app`
152
+
153
+ **Note:** Railway gives $5 free credits/month — enough for this app.
154
+
155
+ ## Notes
156
+
157
+ - CAPTCHA accuracy depends on image quality; the scraper retries up to 15 times
158
+ - Headless Chrome is used — no browser window pops up
159
+ - Each scraping job runs in its own thread with its own download directory
160
+ - GeM may change their page structure, which could break selectors
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,133 @@
1
+ # GeM Document Scraper
2
+
3
+ A Python web app that scrapes and downloads contract documents from [GeM (Government e-Marketplace)](https://gem.gov.in). Enter a category, set a date range, and download all matching contract PDFs — with automatic CAPTCHA solving.
4
+
5
+ ## Features
6
+
7
+ - **Web UI** — Clean dark-themed interface, no terminal needed
8
+ - **Category Search** — Autocomplete from GeM's 13,000+ categories
9
+ - **Auto CAPTCHA** — Solves GeM's image CAPTCHAs via Tesseract OCR
10
+ - **PDF Downloads** — Downloads individual files or all as ZIP
11
+ - **Real-time Progress** — Live logs and progress bar while scraping
12
+ - **Background Processing** — Scraper runs in a thread, UI stays responsive
13
+
14
+ ## Demo
15
+
16
+ 1. Enter a category (e.g., "Ball Point Pens (V2) as per IS 3705")
17
+ 2. Set date range (DD-MM-YYYY)
18
+ 3. Click "Start Scraping"
19
+ 4. Watch live progress, download PDFs when done
20
+
21
+ ## Quick Start
22
+
23
+ ### Prerequisites
24
+
25
+ - Python 3.9+
26
+
27
+ ### Install
28
+
29
+ ```bash
30
+ pip3 install -r requirements.txt
31
+ ```
32
+
33
+ ### Run CLI (Interactive Terminal)
34
+
35
+ ```bash
36
+ python3 gem_cli.py
37
+ ```
38
+
39
+ ### Run Web UI
40
+
41
+ ```bash
42
+ python3 app.py
43
+ ```
44
+
45
+ Open **http://localhost:5000** in your browser.
46
+
47
+ ## How It Works
48
+
49
+ 1. **Search** — User enters category + date range in the web UI
50
+ 2. **CAPTCHA** — Opens GeM's contract page, extracts the CAPTCHA image, reads it via Tesseract OCR, submits automatically (retries up to 15 times)
51
+ 3. **Scrape** — Collects all matching contract documents from the results
52
+ 4. **Download** — For each document, calls GeM's AJAX endpoint to get the download URL, then fetches the PDF via HTTP
53
+ 5. **Serve** — Downloaded PDFs are served back through the web UI for browsing/downloading
54
+
55
+ ## Project Structure
56
+
57
+ ```
58
+ GeM-Document-Scraper/
59
+ ├── app.py # Flask web server (routes, job management, threading)
60
+ ├── scraper.py # Background scraping engine (Selenium + AJAX downloads)
61
+ ├── actions.py # Selenium helper functions (category, dates, CAPTCHA)
62
+ ├── main.py # Legacy CLI entry point (still works standalone)
63
+ ├── templates/
64
+ │ └── index.html # Web UI (HTML + CSS + JS, no frameworks)
65
+ ├── requirements.txt # Python dependencies
66
+ ├── downloads/ # Downloaded PDFs (per job)
67
+ └── captcha/ # CAPTCHA images (temporary)
68
+ ```
69
+
70
+ ## API Endpoints
71
+
72
+ | Method | Route | Description |
73
+ |--------|-------|-------------|
74
+ | GET | `/` | Web UI |
75
+ | POST | `/scrape` | Start a new scraping job |
76
+ | GET | `/status/<job_id>` | Job status + logs + file list |
77
+ | GET | `/download/<job_id>/<file>` | Download a single PDF |
78
+ | GET | `/download_all/<job_id>` | Download all PDFs as ZIP |
79
+
80
+ ## Tested Categories
81
+
82
+ These categories are known to have contracts on GeM:
83
+
84
+ | Category | Notes |
85
+ |----------|-------|
86
+ | `Ball Point Pens (V2) as per IS 3705` | Office supplies, high volume |
87
+ | `Note Sorting Machines (V2)` | Banking equipment |
88
+ | `LED Bulb with Battery as per IS 16102` | Govt LED scheme |
89
+ | `Split Air Conditioner, Wall Mount Type (V3) ISI Marked to IS 1391 (Part 2)` | HVAC |
90
+ | `Nitrogen Tyre Inflators` | Automotive |
91
+
92
+ Use a wide date range (e.g., `01-01-2025` to `07-07-2025`) for more results.
93
+
94
+ ## Date Format
95
+
96
+ The web UI accepts multiple formats and auto-converts:
97
+ - `DD-MM-YYYY` (e.g., 01-07-2025)
98
+ - `DD/MM/YYYY` (e.g., 01/07/2025)
99
+ - `YYYY-MM-DD` (e.g., 2025-07-01)
100
+
101
+ ## Dependencies
102
+
103
+ | Package | Purpose |
104
+ |---------|---------|
105
+ | Flask | Web server |
106
+ | Selenium | Browser automation |
107
+ | Tesseract OCR (`pytesseract`) | CAPTCHA reading |
108
+ | Pillow | Image processing |
109
+ | Requests | HTTP file downloads |
110
+ | Gunicorn | Production WSGI server |
111
+
112
+ ## Deploy to Railway
113
+
114
+ 1. Go to [railway.app](https://railway.app) → Sign up with GitHub
115
+ 2. Click **New Project** → **Deploy from GitHub repo**
116
+ 3. Select `Wickcore/gem-scrapscrap`
117
+ 4. Railway auto-detects the Dockerfile
118
+ 5. Click **Deploy**
119
+
120
+ Your app will be live at `https://your-app.up.railway.app`
121
+
122
+ **Note:** Railway gives $5 free credits/month — enough for this app.
123
+
124
+ ## Notes
125
+
126
+ - CAPTCHA accuracy depends on image quality; the scraper retries up to 15 times
127
+ - Headless Chrome is used — no browser window pops up
128
+ - Each scraping job runs in its own thread with its own download directory
129
+ - GeM may change their page structure, which could break selectors
130
+
131
+ ## License
132
+
133
+ MIT
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env python3
2
+ """GeM Document Scraper CLI — Interactive terminal tool."""
3
+
4
+ import os
5
+ import sys
6
+ import time
7
+ import re
8
+ import json
9
+ import zipfile
10
+ import shutil
11
+ import requests as req
12
+ from bs4 import BeautifulSoup
13
+ from rich.console import Console
14
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
15
+ from rich.table import Table
16
+ from rich.panel import Panel
17
+ from rich.columns import Columns
18
+ from rich.text import Text
19
+ from rich.layout import Layout
20
+ from rich.align import Align
21
+ from rich.rule import Rule
22
+ from rich import box
23
+
24
+ console = Console()
25
+
26
+ # ─── Helpers ────────────────────────────────────────────────────────────────
27
+
28
+ def get_terminal_width():
29
+ return shutil.get_terminal_size((80, 24)).columns
30
+
31
+ def get_terminal_height():
32
+ return shutil.get_terminal_size((80, 24)).lines
33
+
34
+ def banner():
35
+ w = get_terminal_width()
36
+ lines = [
37
+ " ╔═══════════════════════════════════════════╗ ",
38
+ " ║ GeM Document Scraper CLI v1.0 ║ ",
39
+ " ║ Government e-Marketplace Downloader ║ ",
40
+ " ╚═══════════════════════════════════════════╝ ",
41
+ ]
42
+ for line in lines:
43
+ console.print(Align.center(line, width=w), style="bold cyan")
44
+
45
+ def divider():
46
+ console.print(Rule(style="dim blue"))
47
+
48
+ def success(msg):
49
+ console.print(f" [bold green]✓[/bold green] {msg}")
50
+
51
+ def error(msg):
52
+ console.print(f" [bold red]✗[/bold red] {msg}")
53
+
54
+ def info(msg):
55
+ console.print(f" [bold blue]i[/bold blue] {msg}")
56
+
57
+ def section(title):
58
+ console.print()
59
+ console.print(f" [bold yellow]{title}[/bold yellow]")
60
+ divider()
61
+
62
+ def input_field(prompt, default="", validator=None):
63
+ while True:
64
+ try:
65
+ val = console.input(f" [bold cyan]{prompt}:[/bold cyan] ").strip()
66
+ except EOFError:
67
+ return default
68
+ if not val and default:
69
+ return default
70
+ if validator and not validator(val):
71
+ continue
72
+ return val
73
+
74
+ def confirm(prompt, default=True):
75
+ try:
76
+ ans = console.input(f" [bold yellow]{prompt}[/bold yellow] [{'Y/n' if default else 'y/N'}]: ").strip().lower()
77
+ except EOFError:
78
+ return default
79
+ if not ans:
80
+ return default
81
+ return ans in ("y", "yes")
82
+
83
+ def pick_from_list(items, label="option"):
84
+ """Let user pick from a numbered list."""
85
+ w = get_terminal_width()
86
+ max_name = max(len(c["text"]) for c in items) if items else 0
87
+ cols = max(1, (w - 4) // max(max_name + 8, 25))
88
+
89
+ for i, item in enumerate(items, 1):
90
+ console.print(f" [green]{i:3d}.[/green] {item['text']}")
91
+
92
+ while True:
93
+ try:
94
+ choice = console.input(f"\n [bold]Pick {label} (1-{len(items)}):[/bold] ").strip()
95
+ except EOFError:
96
+ return None
97
+ if choice.isdigit() and 1 <= int(choice) <= len(items):
98
+ return items[int(choice) - 1]
99
+ error("Invalid choice. Try again.")
100
+
101
+ # ─── GeM API ────────────────────────────────────────────────────────────────
102
+
103
+ def get_session():
104
+ s = req.Session()
105
+ s.headers.update({
106
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
107
+ })
108
+ return s
109
+
110
+ def load_categories(session):
111
+ resp = session.get("https://gem.gov.in/view_contracts", timeout=60)
112
+ soup = BeautifulSoup(resp.text, "html.parser")
113
+ cat_select = soup.find("select", {"id": "buyer_category"})
114
+ categories = []
115
+ if cat_select:
116
+ for opt in cat_select.find_all("option"):
117
+ text = opt.text.strip()
118
+ value = opt["value"]
119
+ if text and text not in ("--Select Category--", "--Select--"):
120
+ categories.append({"text": text, "value": value})
121
+ return categories
122
+
123
+ def search_contracts(session, cat_value, from_date, to_date, page=0):
124
+ search_data = {
125
+ "fromDate": from_date,
126
+ "toDate": to_date,
127
+ "department": "",
128
+ "bno": "",
129
+ "buyer_category": cat_value,
130
+ "page": page,
131
+ }
132
+ resp = session.post(
133
+ "https://gem.gov.in/view_contracts/contract_details",
134
+ data=search_data,
135
+ timeout=60,
136
+ )
137
+ soup = BeautifulSoup(resp.text, "html.parser")
138
+ return soup.find_all("div", class_="border")
139
+
140
+ def load_all_pages(session, cat_value, from_date, to_date, max_pages=50):
141
+ all_docs = []
142
+ for page in range(max_pages):
143
+ docs = search_contracts(session, cat_value, from_date, to_date, page)
144
+ if not docs:
145
+ break
146
+ all_docs.extend(docs)
147
+ if len(docs) < 20:
148
+ break
149
+ time.sleep(0.3)
150
+ return all_docs
151
+
152
+ def download_document(session, doc, index, download_dir):
153
+ link = doc.find("a")
154
+ if not link:
155
+ return None
156
+ onclick = link.get("onclick", "")
157
+ match = re.search(r"openCap\('([^']+)'\)", onclick)
158
+ if not match:
159
+ return None
160
+ contract_id = match.group(1)
161
+
162
+ dl_resp = session.post(
163
+ "https://gem.gov.in/view_contracts/sbtCaptcha",
164
+ data={"oid": contract_id},
165
+ timeout=60,
166
+ )
167
+ dl_data = json.loads(dl_resp.text)
168
+ if dl_data.get("status") != "1":
169
+ return None
170
+
171
+ code_html = dl_data.get("code", "")
172
+ url_match = re.search(r'href=["\']([^"\']+)["\']', code_html)
173
+ if not url_match:
174
+ return None
175
+
176
+ pdf_resp = session.get(url_match.group(1), timeout=60)
177
+ if len(pdf_resp.content) > 1000:
178
+ filename = f"document_{index}.pdf"
179
+ filepath = os.path.join(download_dir, filename)
180
+ with open(filepath, "wb") as f:
181
+ f.write(pdf_resp.content)
182
+ return {"filename": filename, "size": len(pdf_resp.content), "contract": contract_id}
183
+ return None
184
+
185
+ def create_zip(download_dir, zip_path):
186
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
187
+ for f in sorted(os.listdir(download_dir)):
188
+ if f.endswith(".pdf"):
189
+ zf.write(os.path.join(download_dir, f), f)
190
+ return os.path.getsize(zip_path)
191
+
192
+ # ─── Category Selection ─────────────────────────────────────────────────────
193
+
194
+ def select_category(categories):
195
+ section("Select Category")
196
+ info(f"{len(categories)} categories available")
197
+
198
+ search = input_field("Search (type to filter)", default="")
199
+
200
+ matches = categories
201
+ if search:
202
+ matches = [c for c in categories if search.lower() in c["text"].lower()]
203
+
204
+ if not matches:
205
+ error("No categories match your search.")
206
+ return None
207
+
208
+ console.print()
209
+ if len(matches) > 30:
210
+ console.print(f" [dim]Showing 30 of {len(matches)} matches[/dim]")
211
+ matches = matches[:30]
212
+
213
+ selected = pick_from_list(matches, "category")
214
+ if selected:
215
+ success(f"Selected: {selected['text']}")
216
+ return selected
217
+
218
+ def select_dates():
219
+ section("Date Range")
220
+ info("Format: DD-MM-YYYY (e.g. 01-01-2025)")
221
+
222
+ def valid_date(d):
223
+ if not re.match(r"^\d{2}-\d{2}-\d{4}$", d):
224
+ error("Invalid format. Use DD-MM-YYYY")
225
+ return False
226
+ return True
227
+
228
+ from_date = input_field("From date", validator=valid_date)
229
+ to_date = input_field("To date", validator=valid_date)
230
+ return from_date, to_date
231
+
232
+ def show_documents(docs):
233
+ section(f"Found {len(docs)} Documents")
234
+ w = get_terminal_width()
235
+
236
+ table = Table(box=box.ROUNDED, show_lines=False, expand=True)
237
+ table.add_column("#", style="green", width=4, no_wrap=True)
238
+ table.add_column("Contract", style="cyan", ratio=3)
239
+ table.add_column("Status", style="yellow", width=10, no_wrap=True)
240
+
241
+ shown = min(15, len(docs))
242
+ for i, doc in enumerate(docs[:shown], 1):
243
+ link = doc.find("a")
244
+ text = link.text.strip()[:w - 25] if link else "Unknown"
245
+ table.add_row(str(i), text, "Ready")
246
+
247
+ if len(docs) > shown:
248
+ table.add_row("...", f"[dim]{len(docs) - shown} more[/dim]", "")
249
+
250
+ console.print(table)
251
+
252
+ def select_download_option():
253
+ section("Download Options")
254
+ console.print(" [green]1.[/green] Download ALL documents")
255
+ console.print(" [green]2.[/green] Download as ZIP")
256
+ console.print(" [green]3.[/green] Pick specific documents")
257
+ console.print(" [green]4.[/green] Skip")
258
+
259
+ choice = input_field("Choose", default="2", validator=lambda x: x in ("1","2","3","4"))
260
+ return {"1": "all", "2": "zip", "3": "pick", "4": "skip"}[choice]
261
+
262
+ def pick_documents(docs):
263
+ section("Pick Documents")
264
+ info("Enter numbers separated by commas (e.g. 1,3,5-8)")
265
+ raw = input_field("Documents")
266
+
267
+ indices = []
268
+ for part in raw.split(","):
269
+ part = part.strip()
270
+ if "-" in part:
271
+ start, end = part.split("-", 1)
272
+ if start.isdigit() and end.isdigit():
273
+ indices.extend(range(int(start), int(end) + 1))
274
+ elif part.isdigit():
275
+ indices.append(int(part))
276
+
277
+ return [docs[i - 1] for i in indices if 1 <= i <= len(docs)]
278
+
279
+ def download_with_progress(session, docs, download_dir):
280
+ section(f"Downloading {len(docs)} Documents")
281
+
282
+ results = []
283
+ with Progress(
284
+ SpinnerColumn(),
285
+ TextColumn("[progress.description]{task.description}"),
286
+ BarColumn(bar_width=get_terminal_width() - 30),
287
+ TaskProgressColumn(),
288
+ TimeElapsedColumn(),
289
+ console=console,
290
+ ) as progress:
291
+ task = progress.add_task("Downloading...", total=len(docs))
292
+
293
+ for i, doc in enumerate(docs):
294
+ progress.update(task, description=f"Document {i+1}/{len(docs)}")
295
+ result = download_document(session, doc, i + 1, download_dir)
296
+ if result:
297
+ results.append(result)
298
+ progress.advance(task)
299
+
300
+ return results
301
+
302
+ def show_summary(results, download_dir):
303
+ section("Download Complete")
304
+
305
+ table = Table(box=box.ROUNDED, expand=True)
306
+ table.add_column("File", style="cyan", ratio=2)
307
+ table.add_column("Contract", style="dim", ratio=3)
308
+ table.add_column("Size", style="green", justify="right", width=10)
309
+
310
+ total_size = 0
311
+ for r in results:
312
+ size_kb = r["size"] / 1024
313
+ total_size += r["size"]
314
+ table.add_row(r["filename"], r.get("contract", ""), f"{size_kb:.1f} KB")
315
+
316
+ console.print(table)
317
+ console.print()
318
+ success(f"{len(results)} files saved to: [bold]{download_dir}[/bold]")
319
+ success(f"Total size: {total_size / 1024:.1f} KB")
320
+
321
+ # ─── Main ───────────────────────────────────────────────────────────────────
322
+
323
+ def main():
324
+ console.clear()
325
+ banner()
326
+ divider()
327
+
328
+ # Load categories
329
+ session = get_session()
330
+ with console.status("[bold green]Connecting to GeM...[/bold green]"):
331
+ categories = load_categories(session)
332
+
333
+ console.print()
334
+ success(f"Loaded {len(categories)} categories from GeM")
335
+
336
+ while True:
337
+ console.print()
338
+ divider()
339
+ console.print(" [bold]MAIN MENU[/bold]")
340
+ divider()
341
+ console.print(" [green]1.[/green] Scrape & Download contracts")
342
+ console.print(" [green]2.[/green] Search categories")
343
+ console.print(" [green]3.[/green] Quit")
344
+ divider()
345
+
346
+ action = input_field("Choose", default="1", validator=lambda x: x in ("1","2","3"))
347
+
348
+ if action == "3":
349
+ console.print()
350
+ info("Goodbye!")
351
+ console.print()
352
+ break
353
+
354
+ if action == "2":
355
+ section("Category Search")
356
+ search = input_field("Search")
357
+ matches = [c for c in categories if search.lower() in c["text"].lower()]
358
+ if matches:
359
+ for c in matches[:15]:
360
+ console.print(f" {c['text']}")
361
+ if len(matches) > 15:
362
+ console.print(f" [dim]... {len(matches) - 15} more[/dim]")
363
+ else:
364
+ error("No matches found")
365
+ continue
366
+
367
+ # Full workflow
368
+ selected = select_category(categories)
369
+ if not selected:
370
+ continue
371
+
372
+ from_date, to_date = select_dates()
373
+ if not from_date:
374
+ continue
375
+
376
+ with console.status("[bold yellow]Searching GeM...[/bold yellow]"):
377
+ docs = load_all_pages(session, selected["value"], from_date, to_date)
378
+
379
+ if not docs:
380
+ error("No documents found. Try wider date range or different category.")
381
+ continue
382
+
383
+ show_documents(docs)
384
+
385
+ option = select_download_option()
386
+ if option == "skip":
387
+ continue
388
+
389
+ if option == "pick":
390
+ to_download = pick_documents(docs)
391
+ if not to_download:
392
+ error("No documents selected.")
393
+ continue
394
+ else:
395
+ to_download = docs
396
+
397
+ download_dir = os.path.join(os.getcwd(), "gem_downloads")
398
+ os.makedirs(download_dir, exist_ok=True)
399
+
400
+ results = download_with_progress(session, to_download, download_dir)
401
+
402
+ if not results:
403
+ error("No documents downloaded.")
404
+ continue
405
+
406
+ # ZIP
407
+ if option in ("all", "zip") and len(results) > 1:
408
+ if confirm("Create ZIP file?", default=True):
409
+ zip_path = os.path.join(download_dir, "all_documents.zip")
410
+ with console.status("[bold yellow]Creating ZIP...[/bold yellow]"):
411
+ zip_size = create_zip(download_dir, zip_path)
412
+ success(f"ZIP created: {zip_path} ({zip_size / 1024:.0f} KB)")
413
+
414
+ show_summary(results, download_dir)
415
+
416
+ if __name__ == "__main__":
417
+ try:
418
+ main()
419
+ except KeyboardInterrupt:
420
+ console.print("\n")
421
+ info("Interrupted. Bye!")
422
+ sys.exit(0)
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: gem-scrapscrap
3
+ Version: 1.0.0
4
+ Summary: CLI tool to scrape and download contract documents from GeM (Government e-Marketplace)
5
+ Home-page: https://github.com/Wickcore/gem-scrapscrap
6
+ Author: Wickcore
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/Wickcore/gem-scrapscrap
9
+ Project-URL: Repository, https://github.com/Wickcore/gem-scrapscrap
10
+ Project-URL: Issues, https://github.com/Wickcore/gem-scrapscrap/issues
11
+ Keywords: gem,government,scraping,contracts,tenders
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: requests>=2.28.0
27
+ Requires-Dist: beautifulsoup4>=4.12.0
28
+ Requires-Dist: rich>=13.0.0
29
+ Dynamic: home-page
30
+ Dynamic: requires-python
31
+
32
+ # GeM Document Scraper
33
+
34
+ A Python web app that scrapes and downloads contract documents from [GeM (Government e-Marketplace)](https://gem.gov.in). Enter a category, set a date range, and download all matching contract PDFs — with automatic CAPTCHA solving.
35
+
36
+ ## Features
37
+
38
+ - **Web UI** — Clean dark-themed interface, no terminal needed
39
+ - **Category Search** — Autocomplete from GeM's 13,000+ categories
40
+ - **Auto CAPTCHA** — Solves GeM's image CAPTCHAs via Tesseract OCR
41
+ - **PDF Downloads** — Downloads individual files or all as ZIP
42
+ - **Real-time Progress** — Live logs and progress bar while scraping
43
+ - **Background Processing** — Scraper runs in a thread, UI stays responsive
44
+
45
+ ## Demo
46
+
47
+ 1. Enter a category (e.g., "Ball Point Pens (V2) as per IS 3705")
48
+ 2. Set date range (DD-MM-YYYY)
49
+ 3. Click "Start Scraping"
50
+ 4. Watch live progress, download PDFs when done
51
+
52
+ ## Quick Start
53
+
54
+ ### Prerequisites
55
+
56
+ - Python 3.9+
57
+
58
+ ### Install
59
+
60
+ ```bash
61
+ pip3 install -r requirements.txt
62
+ ```
63
+
64
+ ### Run CLI (Interactive Terminal)
65
+
66
+ ```bash
67
+ python3 gem_cli.py
68
+ ```
69
+
70
+ ### Run Web UI
71
+
72
+ ```bash
73
+ python3 app.py
74
+ ```
75
+
76
+ Open **http://localhost:5000** in your browser.
77
+
78
+ ## How It Works
79
+
80
+ 1. **Search** — User enters category + date range in the web UI
81
+ 2. **CAPTCHA** — Opens GeM's contract page, extracts the CAPTCHA image, reads it via Tesseract OCR, submits automatically (retries up to 15 times)
82
+ 3. **Scrape** — Collects all matching contract documents from the results
83
+ 4. **Download** — For each document, calls GeM's AJAX endpoint to get the download URL, then fetches the PDF via HTTP
84
+ 5. **Serve** — Downloaded PDFs are served back through the web UI for browsing/downloading
85
+
86
+ ## Project Structure
87
+
88
+ ```
89
+ GeM-Document-Scraper/
90
+ ├── app.py # Flask web server (routes, job management, threading)
91
+ ├── scraper.py # Background scraping engine (Selenium + AJAX downloads)
92
+ ├── actions.py # Selenium helper functions (category, dates, CAPTCHA)
93
+ ├── main.py # Legacy CLI entry point (still works standalone)
94
+ ├── templates/
95
+ │ └── index.html # Web UI (HTML + CSS + JS, no frameworks)
96
+ ├── requirements.txt # Python dependencies
97
+ ├── downloads/ # Downloaded PDFs (per job)
98
+ └── captcha/ # CAPTCHA images (temporary)
99
+ ```
100
+
101
+ ## API Endpoints
102
+
103
+ | Method | Route | Description |
104
+ |--------|-------|-------------|
105
+ | GET | `/` | Web UI |
106
+ | POST | `/scrape` | Start a new scraping job |
107
+ | GET | `/status/<job_id>` | Job status + logs + file list |
108
+ | GET | `/download/<job_id>/<file>` | Download a single PDF |
109
+ | GET | `/download_all/<job_id>` | Download all PDFs as ZIP |
110
+
111
+ ## Tested Categories
112
+
113
+ These categories are known to have contracts on GeM:
114
+
115
+ | Category | Notes |
116
+ |----------|-------|
117
+ | `Ball Point Pens (V2) as per IS 3705` | Office supplies, high volume |
118
+ | `Note Sorting Machines (V2)` | Banking equipment |
119
+ | `LED Bulb with Battery as per IS 16102` | Govt LED scheme |
120
+ | `Split Air Conditioner, Wall Mount Type (V3) ISI Marked to IS 1391 (Part 2)` | HVAC |
121
+ | `Nitrogen Tyre Inflators` | Automotive |
122
+
123
+ Use a wide date range (e.g., `01-01-2025` to `07-07-2025`) for more results.
124
+
125
+ ## Date Format
126
+
127
+ The web UI accepts multiple formats and auto-converts:
128
+ - `DD-MM-YYYY` (e.g., 01-07-2025)
129
+ - `DD/MM/YYYY` (e.g., 01/07/2025)
130
+ - `YYYY-MM-DD` (e.g., 2025-07-01)
131
+
132
+ ## Dependencies
133
+
134
+ | Package | Purpose |
135
+ |---------|---------|
136
+ | Flask | Web server |
137
+ | Selenium | Browser automation |
138
+ | Tesseract OCR (`pytesseract`) | CAPTCHA reading |
139
+ | Pillow | Image processing |
140
+ | Requests | HTTP file downloads |
141
+ | Gunicorn | Production WSGI server |
142
+
143
+ ## Deploy to Railway
144
+
145
+ 1. Go to [railway.app](https://railway.app) → Sign up with GitHub
146
+ 2. Click **New Project** → **Deploy from GitHub repo**
147
+ 3. Select `Wickcore/gem-scrapscrap`
148
+ 4. Railway auto-detects the Dockerfile
149
+ 5. Click **Deploy**
150
+
151
+ Your app will be live at `https://your-app.up.railway.app`
152
+
153
+ **Note:** Railway gives $5 free credits/month — enough for this app.
154
+
155
+ ## Notes
156
+
157
+ - CAPTCHA accuracy depends on image quality; the scraper retries up to 15 times
158
+ - Headless Chrome is used — no browser window pops up
159
+ - Each scraping job runs in its own thread with its own download directory
160
+ - GeM may change their page structure, which could break selectors
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,10 @@
1
+ README.md
2
+ gem_cli.py
3
+ pyproject.toml
4
+ setup.py
5
+ gem_scrapscrap.egg-info/PKG-INFO
6
+ gem_scrapscrap.egg-info/SOURCES.txt
7
+ gem_scrapscrap.egg-info/dependency_links.txt
8
+ gem_scrapscrap.egg-info/entry_points.txt
9
+ gem_scrapscrap.egg-info/requires.txt
10
+ gem_scrapscrap.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gem-scrapscrap = gem_cli:main
@@ -0,0 +1,3 @@
1
+ requests>=2.28.0
2
+ beautifulsoup4>=4.12.0
3
+ rich>=13.0.0
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gem-scrapscrap"
7
+ version = "1.0.0"
8
+ description = "CLI tool to scrape and download contract documents from GeM (Government e-Marketplace)"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ {name = "Wickcore"}
14
+ ]
15
+ keywords = ["gem", "government", "scraping", "contracts", "tenders"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Internet :: WWW/HTTP",
28
+ "Topic :: Software Development :: Libraries :: Python Modules",
29
+ ]
30
+ dependencies = [
31
+ "requests>=2.28.0",
32
+ "beautifulsoup4>=4.12.0",
33
+ "rich>=13.0.0",
34
+ ]
35
+
36
+ [project.scripts]
37
+ gem-scrapscrap = "gem_cli:main"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/Wickcore/gem-scrapscrap"
41
+ Repository = "https://github.com/Wickcore/gem-scrapscrap"
42
+ Issues = "https://github.com/Wickcore/gem-scrapscrap/issues"
43
+
44
+ [tool.setuptools]
45
+ py-modules = ["gem_cli"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="gem-scrapscrap",
5
+ version="1.0.0",
6
+ description="GeM Document Scraper — Download contract documents from Government e-Marketplace",
7
+ author="Wickcore",
8
+ url="https://github.com/Wickcore/gem-scrapscrap",
9
+ py_modules=["gem_cli"],
10
+ install_requires=[
11
+ "requests",
12
+ "beautifulsoup4",
13
+ "rich",
14
+ ],
15
+ entry_points={
16
+ "console_scripts": [
17
+ "gem-scrapscrap=gem_cli:main",
18
+ ],
19
+ },
20
+ python_requires=">=3.9",
21
+ classifiers=[
22
+ "Programming Language :: Python :: 3",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Operating System :: OS Independent",
25
+ ],
26
+ )