Skip to main content

Atrium Job Scraper

A modular, polite, production-oriented scraper that collects publicly available internship/job listings for Atrium's student community.

⚠️ Read this first: a note on live-site verification

This project was built in a sandboxed environment with no network access to linkedin.com, indeed.com, naukri.com, or internshala.com — only package registries (PyPI/npm/GitHub) were reachable. That means:

  • The architecture, pipeline, tests, error handling, and CLI are fully built and verified end-to-end using saved HTML fixtures (tests/fixtures/*.html) that mirror each site's real DOM structure.
  • Internshala is the fully working reference implementation — its selectors are based on Internshala's known server-rendered markup and pass parsing tests against a realistic fixture.
  • Indeed, Naukri, and LinkedIn are implemented with the same architecture and best-effort selectors, but you must run them against the live sites yourself (from a machine with normal internet access) and adjust selectors in scraper/sources/<name>.py if a site has changed its markup since this was written. Each source file isolates its selectors in one or two small methods specifically so this kind of update is quick and low-risk.
  • Run python main.py --source internshala first, then check output/*.json and logs/scraper.log. If Internshala's HTML has drifted, the log will show 0 jobs found on page 1 — that's your signal to inspect the live page and update the selectors in scraper/sources/internshala.py.

This is standard practice for any scraper anyway: sites change their markup periodically, so selector maintenance is an ongoing task, not a one-time build step, regardless of who wrote the original code.

Architecture

scraper/
  sources/          # one file per site, isolated selectors
    base.py          # BaseSource ABC — pagination loop, error isolation
    internshala.py    # requests + BeautifulSoup (fully working reference)
    naukri.py          # requests, JSON-payload parsing + DOM fallback
    indeed.py            # Selenium (site requires JS / blocks plain requests)
    linkedin.py            # requests, guest endpoint only (see limitations)
  core/
    scraper.py       # orchestrator: runs sources, isolates failures
    normalizer.py     # shared text/date/skill cleanup helpers
    filters.py         # KEYWORDS / LOCATION / REMOTE_ONLY / etc.
    deduplicator.py     # stable UID-based dedup
    output.py            # JSON + CSV writers, Postgres stub
  models/
    job.py           # the Job dataclass every source normalizes into
  utils/
    config.py        # .env -> typed Config object
    logger.py          # console + rotating file logging
    http.py             # retries, backoff, rate limiting, robots.txt
    browser.py           # reusable headless Selenium manager
main.py              # CLI entrypoint
tests/               # pytest suite + HTML fixtures (no live network needed)

Data flow: main.py → core.scraper.run_sources() → for each source: BaseSource.scrape() paginates, calls the source's parse_listing_page() + normalize() → all Job objects are merged → Deduplicator removes duplicates → filters.apply_filters() applies your .env filters → core.output writes JSON + CSV.

requests vs. Selenium — which sources use which, and why

Source Method Why
Internshala requests + BeautifulSoup Listing pages are server-rendered; the job cards are present in the raw HTML, no JS execution needed.
Naukri requests + BeautifulSoup The listing data ships as a JSON blob embedded in a <script> tag (Next.js hydration payload) inside the server-rendered shell — no browser needed to read it. A DOM-based fallback parser is included in case a page variant doesn't include the payload.
Indeed Selenium Indeed serves a JS/CAPTCHA challenge page to most non-browser HTTP clients, even with correct headers — a plain requests.get reliably returns a challenge page, not listings. A real browser is a genuine technical requirement here, not a workaround. We do not attempt to solve or bypass the challenge — if Selenium itself lands on a challenge page, we log and skip it.
LinkedIn requests (guest endpoint only) LinkedIn's public guest job-search page returns cards without login. Full job descriptions redirect to a login wall, which we deliberately do not cross — description/skills/qualifications are left empty for LinkedIn jobs. See the docstring in scraper/sources/linkedin.py for details.

Setup & Installation

# 1. Install via pip
pip install atrium-job-scraper

# 2. Or, if installing from source:
git clone https://github.com/devkunal2812/Atrium-Scraper.git
cd Atrium-Scraper
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# 3. Configuration
cp .env.example .env
# Edit .env to set your KEYWORDS, LOCATION, MAX_PAGES, etc.

Selenium 4.6+ uses Selenium Manager, which auto-downloads the correct ChromeDriver the first time it runs — no manual driver setup needed. You do need Google Chrome (or Chromium) installed on the machine running the Indeed source.

Configuration (.env)

KEYWORDS=react,node.js,python,java   # matched against title/description/skills
SKILLS=                              # optional, narrower than KEYWORDS
LOCATION=india                       # substring match
REMOTE_ONLY=false
INTERNSHIP_ONLY=false
MAX_PAGES=5

(All config settings can be overridden per-run via CLI flags).

Usage Guide

The package provides multiple ways to run the scraper depending on your workflow:

1. Interactive Terminal (TUI) + Web UI 🌟

Run the following command to start both the modern Interactive Terminal Interface and the Web UI in the background:

atrium-scraper
  • Interactive Terminal: An opencode-style interface will appear in your terminal. You can run commands like /scrape naukri, /scrape all, or even standard shell commands (e.g., !ls).
  • Web UI: Automatically launches at http://localhost:8000. You can monitor scrape progress and results via the web dashboard simultaneously!

2. Standalone Web UI

If you only want to start the Web UI (and access the API/dashboard without the interactive terminal):

uvicorn app:app --host 0.0.0.0 --port 8000

3. Traditional CLI

If you prefer standard CLI arguments (great for cron jobs or CI/CD pipelines), use atrium-cli:

# Single source
atrium-cli --source internshala
atrium-cli --source linkedin

# All sources at once
atrium-cli --source all

# Override filters for one run
atrium-cli --source all --keyword "frontend developer" --location "india"
atrium-cli --source internshala --max-pages 5 --remote-only

Output goes to output/jobs_<timestamp>.json and .csv. Logs go to logs/scraper.log and the console.

Running the tests

python -m pytest -q

44 tests cover: normalization helpers (dates, remote-status detection, skill parsing), the Job UID/dedup logic, filter combinations, and HTML parsing for Internshala and Naukri against saved fixtures — so the suite runs fully offline and doesn't depend on live sites being up or unchanged.

Deduplication

Each Job computes a stable UID via Job.compute_uid():

  1. source + source_job_id if the site exposes a real job ID (best).
  2. source + application_url if no ID but a canonical URL exists.
  3. sha256(source + title + company + location) as a last resort.

The Deduplicator runs across the combined results of all sources in one invocation, so --source all won't double-count a job that happens to be posted identically on two sites under different IDs (it will still count it once per source if the title/company/location differ even slightly — true cross-source identity matching would need fuzzy text matching, which isn't implemented).

Error handling & rate limiting

  • Every page fetch and every per-job normalization is wrapped so one bad URL or malformed card never kills the whole run — it's logged and skipped.
  • HttpClient retries transient failures (timeouts, 5xx, 429) with exponential backoff, respects robots.txt by default, and treats anything that looks like a CAPTCHA/bot-block page as a failure to skip, never something to "solve."
  • A configurable delay is enforced between every request (REQUEST_DELAY_SECONDS).

Connecting to Postgres later

scraper/core/output.py has a save_to_postgres() stub with the intended implementation shape documented in its docstring. Because every source already normalizes into the same Job dataclass and job_uid is a natural primary key, wiring up a real DB (SQLAlchemy/psycopg2, an INSERT ... ON CONFLICT (job_uid) DO NOTHING) is a small, isolated change — no scraping code needs to change.

Known limitations (be upfront about these with stakeholders)

  • Selector drift: sites redesign their frontends periodically. Expect to revisit selectors every so often, especially for Naukri/Indeed/LinkedIn. This is normal maintenance for any scraper.
  • LinkedIn descriptions are unavailable without logging in, which this scraper deliberately does not do.
  • Indeed depends on a headless browser succeeding past its bot-check; on a machine/IP with a poor reputation, more pages may come back blocked. This is expected and by design (we don't try to force through).
  • Location filtering is a plain substring match, not geocoding — see the comment in scraper/core/filters.py.
  • Job descriptions/skills from Internshala's listing page are limited to what the card shows; getting the full description would require an additional fetch per job to its detail page (not done by default to keep request volume low — can be added as an optional --deep flag if Atrium wants it).

Metadata

Release files for atrium-job-scraper 2.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for atrium-job-scraper 2.0.2
File Size Uploaded Provenance
atrium_job_scraper-2.0.2.tar.gz 46.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for atrium-job-scraper 2.0.2
File Interpreter ABI Platform
atrium_job_scraper-2.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 103.3 kB

Release files / atrium_job_scraper-2.0.2.tar.gz

Download URL atrium_job_scraper-2.0.2.tar.gz
Size 46.7 kB
Tags Source
SHA-256 checksum
How to use checksums
637846100ee8776d537383d1fe4cf9c83364a0847aa74295749ab03e0246789e
BLAKE2b-256 checksum
How to use checksums
3492a00b58d4aaf87e35885ad415c5060e5e1f49ad99ef0a40a07a4cd010b64c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release files / atrium_job_scraper-2.0.2-py3-none-any.whl

Download URL atrium_job_scraper-2.0.2-py3-none-any.whl
Size 56.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
85d32e4d500caffca31e628d0c178d33b41e5b148c0150cd3dcf98735597a23c
BLAKE2b-256 checksum
How to use checksums
fe346e8c803a96efd59a2cd7194ae30b99ce8ba669ef593f872b700dd5a6e426
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Security for atrium-job-scraper 2.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page