All notable changes to LinkedIn Jobs Scraper Pro.
- Listing-phase IP rotation —
scrape_keyword / fetch_search_page now call proxy_cfg.new_url() per request, not per run. Previously a single shared proxy_url was baked into one httpx.AsyncClient for all 88+ (title × location) calls, so LinkedIn was throttling from a fixed IP. Detail phase already rotated correctly; this brings listing to parity.
- Removed short-page early break —
scrape_keyword no longer bails out when a page returns < 25 cards. Lets the scraper page through LinkedIn's fuzzy guest-API pagination instead of stopping at the first partial page.
- Default
seniority_levels: "4,5,6" → "" (all levels — LinkedIn's f_E filter is no longer applied by default, which was the single biggest volume bottleneck)
- Default
max_jobs_per_keyword: 50 → 500 (the schema max — 20 pages)
- Default
days_posted: 30 → 90 (full quarter)
- Default
concurrency: 5 → 10
- Default
listing_concurrency: 5 → 8
- Default
min_seniority: 4 → 0 (no post-fetch filter)
- Default
max_results: 100 → 1000 (the schema max)
- Inter-page sleep dropped from 0.5s to 0.3s
listing_concurrency is now in the input schema (was only available via API before)
- JSON-LD
JobPosting extraction from detail pages → unlocks salaryMin, salaryMax, salaryCurrency, salaryUnit, salaryRaw, industry, jobFunction, validThrough, directApply, descriptionHtml, structured company info
- Salary parser for inline ranges (
$120k–$180k a year, ₹50–80 LPA, etc.)
- New listing fields:
companyUrl, companySlug, companyLogoUrl, postedTimeAgo, isNew, badge
concurrency input (1–20) for parallel detail fetching — runs 3–5× faster
include_description_html input (default off) — adds raw HTML to output
min_salary input — filter by parsed minimum salary
output_format input — json (dataset), xlsx (KV store), csv (KV store), or all
- XLSX export to Key-Value Store via
openpyxl
- CSV export to Key-Value Store
- Real types:
easyApply, remoteHybrid, isNew, directApply are now bool; applicantsCount is int|null
- Robust applicant-count parser (
"Over 200 applicants", "47 applicants", "1,200 applicants")
- Pay-per-event pricing ($0.0005/job) configured in
actor.json
- 9-field overview view in dataset schema
- Detail fetch is now parallel (
asyncio.gather + asyncio.Semaphore)
- Default proxy group is
RESIDENTIAL per request, not per run
- Dedup key is now
jobId (or cleaned URL) — survives across keywords and locations
- Removed dead Playwright code path (
get_job_details was never invoked)
- Image
data-delayed-url is preferred over src for company logos (skips placeholder)
- HTTP client uses connection pooling tuned to
concurrency
Actor.config is a dataclass, not a dict — runtime was crashing on actorRunId access
- Applicant-count regex was matching CSS class names in the HTML source, not the rendered text — replaced with proper BeautifulSoup selectors
experienceRequirements from JSON-LD is in months, not a label — fall back to the visible seniority string from description__job-criteria-item
sameAs from JSON-LD is sometimes a list, not a string — handled both shapes
employmentType, industry, jobFunction from JSON-LD may be a list or dict — flatten to string
description__job-criteria-item parsing now runs on every detail fetch, not only when JSON-LD is missing
Original linkedin-jobs-scraper-pro (build 0.0.29). Guest API listing + sequential httpx detail fetch. Kept the architecture; this version is a full rewrite of the parsing and orchestration layer.