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.
- gem_scrapscrap-1.0.0/PKG-INFO +164 -0
- gem_scrapscrap-1.0.0/README.md +133 -0
- gem_scrapscrap-1.0.0/gem_cli.py +422 -0
- gem_scrapscrap-1.0.0/gem_scrapscrap.egg-info/PKG-INFO +164 -0
- gem_scrapscrap-1.0.0/gem_scrapscrap.egg-info/SOURCES.txt +10 -0
- gem_scrapscrap-1.0.0/gem_scrapscrap.egg-info/dependency_links.txt +1 -0
- gem_scrapscrap-1.0.0/gem_scrapscrap.egg-info/entry_points.txt +2 -0
- gem_scrapscrap-1.0.0/gem_scrapscrap.egg-info/requires.txt +3 -0
- gem_scrapscrap-1.0.0/gem_scrapscrap.egg-info/top_level.txt +1 -0
- gem_scrapscrap-1.0.0/pyproject.toml +45 -0
- gem_scrapscrap-1.0.0/setup.cfg +4 -0
- gem_scrapscrap-1.0.0/setup.py +26 -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gem_cli
|
|
@@ -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,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
|
+
)
|