A learning project built to practice real-world Python skills β async programming, browser automation, HTML parsing, proxy rotation, and Excel file I/O.
Search LinkedIn for any job title, at any company, in any country β export everything to a clean Excel file with one command. Now with free auto-rotating proxy support, a --dry-run smoke mode, and a live dashboard that shows your session health (login state, ban risk, daily search cap) in real time.
This tool is for educational purposes only. Scraping LinkedIn may violate their Terms of Service.
- Do not use this for commercial purposes
- Do not run this at large scale
- Use a secondary LinkedIn account β not your main one
- The author is not responsible for any account restrictions or legal issues
- What It Does
- Project Structure
- Requirements
- Installation
- Configuration
- How to Run
- Proxy Support
- Output
- Live Dashboard & Session Health
- How It Works
- Troubleshooting
- Tech Stack
- π Search LinkedIn for any job title β Recruiter, Engineer, Designer, CEO, anything
- π’ Search across any company list β your own list or the full Fortune 500
- π Filter by country or city using LinkedIn's location system
- π Auto-rotating free proxies β fetches and tests fresh proxies every hour
- π Export results to a formatted Excel file with clickable LinkedIn profile links
- β»οΈ Auto-resumes if stopped β progress is saved after every search, duplicate profiles are skipped
- π‘οΈ Built-in anti-detection β stealth JS, human delays, session breaks, exponential backoff
- π Smart login β restores your saved session, retries with backoff, polls through 2FA/checkpoints
- π§ͺ
--dry-runβ boots the browser, logs in, verifies one search, saves nothing - π Live session health β ban-risk score, error streaks, and daily search cap on the dashboard
linkedin-scraper/
β
βββ main.py β Main entry (menu + --dry-run CLI mode)
βββ main_proxy.py β Alt engine with free proxy rotation
β
βββ linkedin_scraper.py β Core scraping engine (login, proxy rotation,
β anti-block, live session telemetry)
βββ linkedin_scraper_proxy.py β Scraping engine with proxy support
βββ proxy_manager.py β Free proxy fetcher, tester & rotator
β
βββ config.py β ALL your settings (edit this!)
βββ utils.py β Helpers, delays, Excel export, telemetry store
βββ resume_parser.py β Parses and structures profile data
βββ Requirements.txt β Python dependencies
β
βββ dashboard_template.html β Dashboard UI source of truth
βββ generate_dashboard.py β Builds dashboard.html from output/ (stdlib only)
βββ dashboard.html β Generated live dashboard (open in a browser)
β
βββ test_scraper_fixes.py β Behavioral tests (no browser needed)
βββ test_login_flow.py β Drives the real login() against fakes
β
βββ output/ β Auto-created on first run
β βββ linkedin_results.xlsx β Your exported Excel data
β βββ progress.json β Auto-save checkpoint (resume feature)
β βββ proxies.json β Cached working proxy list
β βββ scraper.log β Full activity log
β βββ session_health.json β Live session telemetry (dashboard panel)
β
βββ session/ β Browser session/cookie cache
- Python 3.8 or higher
- A LinkedIn account (use a secondary one!)
- Windows / macOS / Linux
Check your Python version:
python --versiongit clone https://github.com/aster-go/linkedin-scraper.git
cd linkedin-scraperpip install -r Requirements.txtPrefer a project virtual environment (recommended):
python -m venv .venv .venv/Scripts/python.exe -m pip install -r Requirements.txt # Windows source .venv/bin/activate && pip install -r Requirements.txt # Mac / Linux
playwright install chromiumYou only ever need to edit one file: config.py
LINKEDIN_EMAIL = "your_email@gmail.com"
LINKEDIN_PASSWORD = "your_password"π‘ Use environment variables to keep credentials safe:
# Windows set LINKEDIN_EMAIL=your_email@gmail.com set LINKEDIN_PASSWORD=your_password # Mac / Linux export LINKEDIN_EMAIL=your_email@gmail.com export LINKEDIN_PASSWORD=your_password
Edit JOB_TITLES in config.py:
# Recruiters (default)
JOB_TITLES = ["Recruiter", "Technical Recruiter", "Talent Acquisition"]
# Software Engineers
JOB_TITLES = ["Software Engineer", "Backend Developer", "Frontend Developer"]
# Sales
JOB_TITLES = ["Account Executive", "Sales Manager", "VP of Sales"]
# Data / AI
JOB_TITLES = ["Data Scientist", "Machine Learning Engineer", "Data Analyst"]
# Leadership / C-Suite
JOB_TITLES = ["CEO", "CTO", "Co-Founder", "Director of Engineering"]
# Marketing
JOB_TITLES = ["Marketing Manager", "CMO", "Growth Manager", "SEO Specialist"]SEARCH_LOCATION_NAME = "United States"
GEO_URN = "103644278"| Country / City | GEO_URN |
|---|---|
| United States | 103644278 |
| United Kingdom | 101165590 |
| Canada | 101174742 |
| Australia | 101452733 |
| India | 102713980 |
| Germany | 101282230 |
| France | 105015875 |
| Singapore | 102454443 |
| New York City | 105080838 |
| San Francisco | 102277331 |
| London | 102257491 |
# Only save senior-level profiles:
FILTER_KEYWORDS = ["senior", "lead", "principal", "director", "vp"]
# Save everyone β no filter:
FILTER_KEYWORDS = []The scraper uses FORTUNE_500_COMPANIES (25 companies) by default. For a quick first run set:
USE_TEST_COMPANIES = True # runs the 2-company TEST_COMPANIES list insteadYou can also edit the TEST_COMPANIES / FORTUNE_500_COMPANIES lists in main.py.
Proxies (either PROXY_LIST with your own proxies, or free auto-fetch):
PROXY_LIST = [] # e.g. ["http://user:pass@proxy1:8080", ...] β round-robin rotated
USE_FREE_PROXIES = False # True β auto-fetch + test free proxies when PROXY_LIST is emptySafety / resilience:
RETRY_FAILED_PROFILES = True # one final pass over transiently-failed profiles before export
LOGIN_CHALLENGE_TIMEOUT_SECONDS = 90 # how long to poll 2FA/checkpoint pages (no blind waits)Notifications:
DISCORD_WEBHOOK_URL = "" # optional β posts a summary when a scrape completesTelemetry output: output/session_health.json is written automatically (login reason, proxy used,
error streaks, daily search count, ban-risk score). No setup needed.
python main.pySet USE_FREE_PROXIES = True in config.py, then run normally:
python main.pyThe scraper fetches + tests free proxies on launch (cached to output/proxies.json for an hour),
rotates through them, and sheds dead ones automatically. You can also use the dedicated proxy
engine (python main_proxy.py) if you prefer the separate path.
python main.py --dry-run # people mode, one search
python main.py --dry-run --mode jobs # job search
python main.py --dry-run --mode candidates # candidate searchBoots the browser, logs in, verifies one search page, then stops. Scrapes nothing and
writes nothing β a safe end-to-end check that your credentials, session, and search URLs work.
Exit code 0 = all good.
If the scraper stops for any reason β just run the same command again:
python main.pyIt reads output/progress.json and continues exactly where it left off. No data is lost β and
profiles already captured are never re-scraped.
Two ways to use proxies β both feed the same round-robin rotation:
- Your own proxies β set
PROXY_LISTinconfig.py. Rotation never picks the same proxy twice in a row, so every identity rotation actually changes your IP. - Free proxies β set
USE_FREE_PROXIES = True;proxy_manager.pyfetches, tests, caches, and rotates free proxies for you (also the engine behindmain_proxy.py).
| When | What Happens |
|---|---|
| On startup | Fetches 100+ free proxies, tests them, keeps working ones |
| Every hour | Fetches a completely fresh proxy list automatically (or loads the cache) |
| Every identity rotation | Picks the next proxy β never the same IP twice in a row |
| On consecutive errors | Marks the current proxy failed, sheds it, and rotates |
| Proxy list runs out | Auto-fetches a brand new list instantly |
| Source | Refresh Rate |
|---|---|
| ProxyScrape | Every few minutes |
| GeoNode | Hourly |
| Proxy-List.download | Daily |
| ProxyNova | Hourly |
Free proxies are not perfect β expect:
- ~10β30% of fetched proxies will actually work
- Some will be slow (3β8 second response times)
proxy_manager.pyautomatically tests and removes bad proxies
Results saved to output/linkedin_results.xlsx with two sheets:
Sheet 1 β All Profiles
| Column | Description |
|---|---|
| Full Name | Person's full name |
| Job Title | Their current title |
| Company (from Profile) | Company listed on their LinkedIn |
| Searched Company | Company name you searched for |
| Searched Title | Job title you searched for |
| LinkedIn Headline | Their full LinkedIn headline |
| Location | City, State |
| Email Address | If publicly visible |
| Phone | If publicly visible |
| Proxy Used | Which proxy was used (or "direct") |
| LinkedIn URL | π Clickable link to their profile |
| Scraped At | Date and time scraped |
Sheet 2 β Summary
Quick stats: total profiles, companies covered, profiles with email.
Regenerate the self-contained dashboard any time (stdlib only, no install):
python generate_dashboard.pyThen open dashboard.html in any browser. It shows:
- π Overview stats + searchable/sortable profile table (with CSV export)
- π Analytics β title keywords, US states, seniority, email coverage
- π§Ύ Colorized activity feed (last 40 log lines, filterable by level)
- π©Ί Project health β live syntax check of every module, plus known issues
- β€οΈ Session Health β login reason, proxy used, error streaks, daily search cap gauge, a 0β100 ban-risk score, and a live event timeline. While a scrape is running the panel shows a pulsing LIVE badge.
The scraper writes output/session_health.json automatically (atomically, on
every event) β nothing to configure.
| Technique | What It Does |
|---|---|
| Random delays | Waits 8β18 seconds between profiles (not fixed) |
| Session breaks | 1β3 min break every 15 requests, scrolls feed naturally |
| Proxy rotation | Round-robins IPs β never the same proxy twice in a row |
| Exponential backoff | Adaptive breaks grow with the error streak (~60s β capped 600s, jittered) |
| Smart login | Restores saved sessions; retries with backoff; polls 2FA/checkpoints |
| Stealth JS | Hides signs that a browser is being automated |
| User agent rotation | Rotates between real Chrome and Safari agents |
| Human-like typing | Types credentials one character at a time |
| Random scrolling | Scrolls pages up and down like a real person |
| NYC Geolocation | Sets browser location to New York |
β οΈ Do NOT reduce the delay settings. Faster = higher chance of getting blocked.
β Login timeout
Login error: Timeout 15000ms exceeded
β Run again. LinkedIn can be slow to load sometimes.
β No results for every company
β LinkedIn may have rate-limited your IP.
β Switch to proxy mode: python main_proxy.py
β Or wait 1β2 hours and try again.
β Normal! The scraper polls the page every 5 seconds (up to
LOGIN_CHALLENGE_TIMEOUT_SECONDS, default 90s) and continues the moment you
complete the challenge β no wasted waiting. If it isn't finished in time,
run again; your session may need a fresh login.
β "Login rejected by LinkedIn"
β That's LinkedIn saying the email/password is wrong β check config.py or the
LINKEDIN_EMAIL / LINKEDIN_PASSWORD env vars. The scraper gives up immediately
instead of hammering the login (that's how accounts get locked).
β Does everything work?
β Run the test suite (no browser or network needed):
python test_scraper_fixes.py
python test_login_flow.pyβ Or a real end-to-end smoke test: python main.py --dry-run
β All proxies failing
β Free proxies sometimes go down all at once. β The scraper will fall back to direct connection automatically. β Try again after 30 minutes β new proxies will be available.
β ModuleNotFoundError
pip install -r Requirements.txt
playwright install chromiumβ 0 results every time
β LinkedIn may have updated their HTML structure. β Open an Issue and I'll investigate.
| Companies | Job Titles | Mode | Est. Time |
|---|---|---|---|
| 3 (test) | 2 | Any | ~10β20 min |
| 50 | 2 | Direct | ~2β5 hours |
| 50 | 2 | Proxy | ~3β7 hours |
| 500 | 5 | Proxy | ~25β70 hours |
Run overnight for large lists. The resume feature means you can split it across multiple sessions.
| Library | Version | Purpose |
|---|---|---|
| Playwright | 1.40+ | Browser automation |
| BeautifulSoup4 | 4.12+ | HTML parsing |
| Pandas | 2.0+ | Data handling |
| OpenPyXL | 3.1+ | Excel export |
| asyncio | built-in | Async execution |
| urllib | built-in | Proxy fetching |
Found a bug or want to improve something?
- Fork the repo
- Create a branch:
git checkout -b fix/your-fix - Commit:
git commit -m "Fix: describe what you fixed" - Push:
git push origin fix/your-fix - Open a Pull Request
If this helped you, consider giving it a star β it helps others find it!
Built as a learning project. Practicing Python, Playwright, asyncio, proxy rotation, and real-world problem solving.
