An automated LinkedIn job scraper that collects Data Scientist positions in Israel every 12-24 hours, stores them in a CSV file with historical snapshots, and provides a modern web dashboard for viewing and downloading the data.
This project is for educational purposes only. Scraping LinkedIn may violate their Terms of Service. Use at your own risk and responsibility. The authors are not responsible for any account restrictions or legal issues that may arise from using this tool.
- Automated Scraping: Playwright-based scraper with anti-detection measures
- Scheduled Execution: APScheduler runs scraping every 12 hours (configurable)
- Data Storage: CSV with deduplication and historical timestamps
- Modern Dashboard: Real-time countdown timer, job table, search, and download
- REST API: JSON endpoints for data access and scraper control
- Anti-Detection: Random delays, user-agent rotation, human-like behavior
- Production-Ready: Logging, error handling, graceful retries
- Requirements
- Installation
- Configuration
- Running the Application
- Using the Dashboard
- API Reference
- Project Structure
- How It Works
- Limitations
- Troubleshooting
- Python 3.10+
- pip (Python package manager)
- Chromium browser (installed automatically by Playwright)
git clone https://github.com/yourusername/linkedin-job-scraper.git
cd linkedin-job-scraper# Windows
python -m venv venv
venv\Scripts\activate
# macOS/Linux
python3 -m venv venv
source venv/bin/activatepip install -r requirements.txtplaywright install chromium# Copy example configuration
cp .env.example .env
# Edit with your settings (optional)
# nano .env # or use any text editorEdit the .env file to customize the scraper:
# Application Settings
APP_NAME=LinkedIn Job Scraper
DEBUG=false
# Server Settings
HOST=0.0.0.0
PORT=8000
# Scraper Settings
SEARCH_KEYWORDS=Data Scientist
SEARCH_LOCATION=Israel
MAX_JOBS_PER_RUN=50
SCRAPER_HEADLESS=true
# Scheduler Settings
SCRAPE_INTERVAL_HOURS=12
SCHEDULER_ENABLED=true
# Logging
LOG_LEVEL=INFO| Variable | Description | Default |
|---|---|---|
SEARCH_KEYWORDS |
Job title to search | Data Scientist |
SEARCH_LOCATION |
Location to filter | Israel |
MAX_JOBS_PER_RUN |
Maximum jobs per scrape | 50 |
SCRAPE_INTERVAL_HOURS |
Hours between scrapes | 12 |
SCRAPER_HEADLESS |
Run browser without UI | true |
python run.pyuvicorn app.main:app --host 0.0.0.0 --port 8000 --reload# Install production server
pip install gunicorn
# Run with multiple workers
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000Open your browser and navigate to:
- Dashboard: http://localhost:8000
- API Docs: http://localhost:8000/docs
- Health Check: http://localhost:8000/api/health
- Countdown Timer: Shows time until next scheduled scrape
- Statistics: Total jobs, unique companies, locations
- Job Table: Searchable, sortable list of all scraped jobs
- Download CSV: Export all data to a CSV file
- Manual Scrape: Trigger an immediate scraping run
Use the search box to filter jobs by:
- Job title
- Company name
- Location
- Required degree
- Experience level
Returns all job listings as JSON.
Query Parameters:
limit(int): Maximum jobs to returnoffset(int): Number of jobs to skipkeyword(string): Filter by keyword in titlecompany(string): Filter by company namelocation(string): Filter by location
Example:
curl "http://localhost:8000/api/data?limit=10&keyword=senior"Download the CSV file.
curl "http://localhost:8000/api/download" -o jobs.csvGet scheduler status and next run time.
Response:
{
"is_running": true,
"enabled": true,
"interval_hours": 12,
"next_run": "2024-01-15T18:00:00",
"seconds_until_next_run": 43200,
"last_run": "2024-01-15T06:00:00",
"last_run_success": true,
"jobs_scraped_last_run": 45
}Trigger a manual scrape.
curl -X POST "http://localhost:8000/api/scrape"Get job statistics.
Health check endpoint.
linkedin-job-scraper/
βββ app/
β βββ __init__.py # Package init
β βββ main.py # FastAPI application
β βββ config.py # Configuration management
β βββ api/
β β βββ __init__.py
β β βββ routes.py # API endpoints
β βββ scraper/
β β βββ __init__.py
β β βββ linkedin_scraper.py # Playwright scraper
β β βββ anti_detection.py # Anti-bot utilities
β βββ scheduler/
β β βββ __init__.py
β β βββ jobs.py # APScheduler configuration
β βββ storage/
β β βββ __init__.py
β β βββ csv_handler.py # CSV read/write operations
β βββ utils/
β β βββ __init__.py
β β βββ helpers.py # Utility functions
β βββ templates/
β β βββ index.html # Dashboard UI
β βββ static/ # Static files (CSS, JS, images)
βββ data/
β βββ jobs/ # CSV output directory
β βββ linkedin_jobs.csv
βββ logs/ # Application logs
βββ tests/
β βββ __init__.py
β βββ test_scraper.py
β βββ test_api.py
βββ .env.example # Example configuration
βββ .gitignore
βββ requirements.txt # Python dependencies
βββ run.py # Application entry point
βββ README.md # This file
ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
β Scheduler ββββββΆβ Playwright ββββββΆβ CSV Handler β
β (12h interval) β β Browser β β (deduplicate) β
ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
β β β
βΌ βΌ βΌ
APScheduler LinkedIn Jobs CSV File with
AsyncIOScheduler Search Page timestamps
- Random Delays: 2-5 seconds between actions
- User-Agent Rotation: Pool of realistic browser agents
- Human-like Behavior: Mouse movements, scrolling patterns
- Browser Fingerprinting: Realistic viewport, timezone, locale
- Stealth Scripts: Override automation detection properties
- Scheduler triggers scraping job (or manual trigger via API)
- Playwright launches headless Chromium
- Scraper navigates to LinkedIn job search
- Job cards are extracted with anti-detection delays
- Optional: Navigate to each job for detailed info
- Results are deduplicated and saved to CSV
- Dashboard displays updated data
- Rate Limiting: LinkedIn may temporarily block excessive requests
- Login Walls: Some job details require authentication
- CAPTCHA: Automated detection may trigger challenges
- Dynamic Content: Page structure may change without notice
- Legal Risks: Violates LinkedIn Terms of Service
- Use moderate scraping intervals (12-24 hours)
- Run in headless=false mode for debugging
- Monitor logs for access denied errors
- Consider using a residential proxy for better success
- Keep MAX_JOBS_PER_RUN reasonable (25-50)
pip install playwright
playwright install chromium- LinkedIn may have changed their page structure
- Check if the search URL works manually
- Review logs for detailed error messages
- Reduce scraping frequency
- Try running with
SCRAPER_HEADLESS=false - Consider using a VPN or proxy
# Ensure data directory exists
mkdir -p data/jobs
# Check write permissions
ls -la data/jobs/Enable debug mode for verbose logging:
DEBUG=true
LOG_LEVEL=DEBUG
SCRAPER_HEADLESS=falseCheck application logs:
tail -f logs/app.logRun the test suite:
# All tests
pytest
# With coverage
pytest --cov=app
# Specific test file
pytest tests/test_api.py- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
For questions or support, please open an issue on GitHub.
Built with β€οΈ using FastAPI, Playwright, and APScheduler
