Local, human-supervised CLI tool for extracting structured metadata from a batch of LinkedIn profile URLs.
This project is intentionally conservative:
- It runs locally on your machine.
- It tries a public-access pass before a logged-in pass.
- It does not ask for your LinkedIn password inside the app.
- It stores audit artifacts per URL so failures can be reviewed later.
- It is designed for low-volume, personal workflows rather than aggressive automation.
This first implementation pass includes:
- project scaffold
- Pydantic data models
- Typer CLI skeleton
- CSV input loading
- JSON and CSV export
- SQLite job ledger for resume support
- Playwright session initialization
- public-pass navigation and page classification
- HTML snapshot parsing with offline test fixtures
The authenticated flow is now split into two steps:
- capture a logged-in session once from a normal user-controlled Chrome window
- run the authenticated scraping pass in an independent Playwright browser using the saved session state
This keeps the batch run out of your normal Chrome tabs and avoids repeated Chrome crash or recovery dialogs.
python3 -m venv .venv
source .venv/bin/activatepip install -r requirements.txt
python -m playwright install chromiumIf you prefer not to activate the environment, the equivalent direct commands are:
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m playwright install chromiumExpected columns:
profile_urlcandidate_idoptionalnotesoptional
Example:
profile_url,candidate_id,notes
https://www.linkedin.com/in/example-1,cand_001,referral batch
https://www.linkedin.com/in/example-2,cand_002,Start a normal Chrome window with a dedicated profile directory and remote debugging enabled:
python app.py open-chromeThis launches system Chrome with a dedicated local profile and prints the attach URL, which defaults to http://127.0.0.1:9222.
In that Chrome window:
- log in to LinkedIn manually
- verify you can open your LinkedIn home feed or profile
- leave Chrome running
After you finish logging in, capture the authenticated session into a reusable storage-state file:
python app.py attach-sessionBy default this saves .session/storage_state.json. It reuses your existing Chrome session and does not need to open a new LinkedIn tab just to capture the session.
This opens a visible browser window using Playwright's bundled Chromium or system Chrome. Log in manually, then wait for the save step to complete.
python app.py init-sessionBy default the saved session is written to .session/storage_state.json.
This opens a Chromium-based browser with a specific user data directory so you can log in manually and later reuse that same local profile. The attach-to-running-Chrome flow above is now the preferred option.
python app.py init-session --browser-profile-dir "/path/to/local/chrome-user-data"This is useful when you want the scraper to launch a persistent local browser profile instead of loading a Playwright storage_state.json.
python app.py scrape-public input.csv --output-dir outputs/run_001Useful safety flags:
python app.py scrape-public input.csv \
--output-dir outputs/run_001 \
--max-profiles 25 \
--delay-min 3 \
--delay-max 7 \
--pause-every 10This only targets rows already marked requires_login in the existing run state.
python app.py scrape-auth input.csv --output-dir outputs/run_001 --resumeDefault background mode using the captured storage state:
python app.py scrape-auth input.csv \
--output-dir outputs/run_001 \
--storage-state .session/storage_state.jsonThis launches an independent Playwright browser and runs the authenticated batch in the background by default, so it does not open profile tabs in your normal Chrome window.
If you want to watch the authenticated pass while debugging selectors:
python app.py scrape-auth input.csv \
--output-dir outputs/run_001 \
--storage-state .session/storage_state.json \
--show-browserLegacy persistent-profile mode still exists, but it is no longer the preferred batch path:
python app.py scrape-auth input.csv \
--output-dir outputs/run_001 \
--browser-profile-dir "/path/to/local/chrome-user-data"When using --browser-profile-dir, keep the profile local and user-controlled. In practice it is safest to close other Chrome windows using the same profile before running the tool.
python app.py export outputs/run_001python app.py validate outputs/run_001/results.jsonEach run writes to a dedicated output directory:
outputs/run_001/
job_state.sqlite
results.json
results.csv
results_en.json
results_en.csv
run_summary.json
logs/run.log
artifacts/screenshots/
artifacts/raw_html/
artifacts/debug_json/
The SQLite ledger stores:
- input profile URL
- phase
- access mode
- status
- failure reason
- retry count
- result payload path
Using --resume skips URLs already attempted in the same phase.
For each processed URL, the tool keeps audit details including:
- requested URL
- final redirected URL
- page title
- page classification
- HTML snapshot path
- screenshot path when applicable
- debug text snippet
This makes it easier to inspect login walls, unavailable pages, verification pages, and parser failures manually.
- Selector coverage is still heuristic-based and should still be validated on more saved real-world samples.
- The preferred authenticated workflow assumes you launch Chrome once for manual login capture, then reuse the saved storage state for batch scraping.
- The legacy
--attach-browserbatch flow is intentionally no longer supported because it interferes with normal Chrome tab usage. - The parser is DOM-first with lightweight heuristics; it does not yet fully handle all LinkedIn layout variants.
- English export writes
results_en.jsonandresults_en.csv, but it requires access to an online translation service at export time. - Date normalization is conservative and preserves raw strings when parsing is uncertain.
- No CAPTCHA solving, proxy rotation, fingerprint spoofing, or credential capture is implemented.
You are responsible for ensuring your use of this tool complies with applicable law, website terms, platform policies, and your organization's internal policies.