Turn your LinkedIn connections export into ranked candidate lists for any target audience — 100% locally.
You define who you're looking for in a small config file (job-title keywords, industries, things to ignore). netsift reads your own LinkedIn data export, scores every connection against your definition, and writes ranked, segmented candidate lists. Great for finding beta testers, design partners, hiring leads, sales prospects, interview subjects — anyone hiding in your network.
Built originally to find beta testers for a compliance/audit SaaS, then generalised. The shipped example config (
config.example.yaml) is that compliance/founders/consultants setup — edit it to describe your audience.
There is no official LinkedIn API or MCP that lets you read your own connections — that access was removed years ago. Third-party scrapers violate LinkedIn's ToS and risk getting your account banned. The clean, allowed path is LinkedIn's own data export, which you process yourself. That's what netsift does.
netsift makes no network calls. Nothing leaves your machine — no API, no
upload, no telemetry. Your export and the generated lists stay local, and the
default .gitignore keeps them out of git.
LinkedIn → Settings & Privacy → Data Privacy → Get a copy of your data →
Connections → request. You'll get a Connections.csv (or a full-archive
.zip containing it) in a few minutes. (Emails are only included for
connections who opted to share them, so most will be blank — that's normal.)
git clone https://github.com/kanylbullen/netsift.git
cd netsift
pip install -r requirements.txt # just PyYAML (skip if you use a JSON config)Requires Python 3.9+.
# Point at the CSV, the export .zip, or the unzipped folder — all work:
python netsift.py --connections Connections.csv --config config.example.yaml --out out/
python netsift.py -c Complete_LinkedInDataExport.zip -k config.example.yaml -o out/ --top 50Try it on the bundled fake data first:
python netsift.py -c examples/sample_connections.csv -k config.example.yaml -o out/| File | What |
|---|---|
report.md |
Per-segment ranked tables + a summary |
top50.md |
Merged top-N across all segments (size = --top) |
candidates_<KEY>.csv |
Full ranked list for each segment |
all_scored.csv |
Every connection with its segment, score and matched signals |
noise.csv |
Everything filtered out (recruiters etc.) — so nothing silently vanishes |
| Flag | Default | Meaning |
|---|---|---|
-c, --connections |
— | Connections.csv, the export .zip, or a folder containing it |
-k, --config |
— | your audience config (.yaml or .json) |
-o, --out |
out |
output directory |
--top |
50 |
size of the merged top-N list |
--per-segment |
40 |
rows per segment in report.md (0 = all) |
Copy config.example.yaml, rename it, and edit. The whole model is just
weighted keywords per segment, plus optional company signals, industry
bonuses and a noise filter.
settings:
industry_bonus: 1.5 # bonus added when a regulated industry matches
search_company_for_keywords: true
substring_match: false # see "Matching" below
segments:
- key: A
name: Compliance / security
priority: 1 # lower = ranked higher in the merged top-N list
keywords:
ciso: 6 # higher weight = stronger signal
iso 27001: 6
compliance: 5
internrevis: 5 # stems work: matches "internrevisionschef"
company_signals: # extra weight when the COMPANY name matches
- segment: C
weight: 3
label: firm
match: [consulting, pwc, kpmg, deloitte]
industries: # company name -> label; adds industry_bonus
finance: [bank, fintech, försäkring]
noise: # drop entirely (title OR company match)
positions: [recruit, talent acquisition, headhunt]
companies: [bbq, frisör]How a connection is placed: its title (and company, by default) is matched
against every segment's keywords; matching weights are summed; it's assigned to
its highest-scoring segment. Add industry_bonus if the company is in a listed
industry. If nothing matches → UNSURE. If a noise term matches → excluded.
By default netsift uses smart matching: short acronyms (≤3 chars like vd,
cio, dpo, grc) match as whole words — so vd (Swedish "CEO") won't hit
avdelning — while longer terms match as substrings/stems, so internrevis
catches internrevisionschef and bank catches Swedbank. Set
substring_match: true to force naive substring matching everywhere.
Keywords are case-insensitive and Unicode-aware (å ä ö work). Works in any
language — just put the right terms in your config.
MIT — see LICENSE. Contributions and config recipes welcome.