Search people on LinkedIn, ONE or MANY searches with AUTO-PAGINATION.
{"queries":"array of objects; each one requires page (pages optional, max 100)"}{"count":"int","results":"array of { query, pagesFetched, count, results:[{ name, degree, slug, profileUrl, headline, location }] }","skipped":"int (if applicable)"}Search people on LinkedIn, ONE or MANY searches with AUTO-PAGINATION. Single PARAM: queries = array of objects { keywords, title, company, firstName, lastName, geoUrn, network, page, pages }. page REQUIRED per query; pages optional (how many pages from page, default 1, max 100). FIXED internal RATE-LIMIT (NOT configurable): pool of 20 fetches + ~150ms jittered before each one. 20 is the measured clean ceiling (20 pages at once = 200/200 results); above that LinkedIn silently returns truncated/empty pages (no 429). Total cap 100 fetches per call (reports skipped). Filters: keywords; title/company/firstName/lastName = exact free text (the URL adds quotes); geoUrn = bare ID or bracketed (linkedin-geo-typeahead); network = bare degrees ('S','F,S') or bracketed. ~10 per page; paginates up to ~page 100+. Returns { count, skipped?, results:[{ query, pagesFetched, count, results:[profiles] }] }; profile { name, degree, slug, profileUrl, headline, location }. Requires a session. Regex parser over SSR HTML; if the markup changes, recapture.
params. It does the in-page work and returns the JSON in returns.( async (root, params) => { let qs = params && params.queries; if (typeof qs === 'string') { try { qs = JSON.parse(qs); } catch(e){ qs = []; } } if (!Array.isArray(qs)) qs = []; const CONC = 20; const DELAY = 150; const MAXTASKS = 100; const enc = encodeURIComponent; const sleep = ms => new Promise(r=>setTimeout(r,ms)); const wrapGeo = v => { v=String(v||'').trim(); if(!v) return ''; return v.charAt(0)==='[' ? v : '["'+v+'"]'; }; const wrapNet = v => { v=String(v||'').trim(); if(!v) return ''; if(v.charAt(0)==='[') return v; return '['+v.split(',').map(s=>'"'+s.trim()+'"').join(',')+']'; }; const buildUrl = (q,pg) => 'https://www.linkedin.com/search/results/people/?keywords='+enc(q.keywords||'')+'&title=%22'+enc(q.title||'')+'%22&company=%22'+enc(q.company||'')+'%22&firstName=%22'+enc(q.firstName||'')+'%22&lastName=%22'+enc(q.lastName||'')+'%22&geoUrn='+enc(wrapGeo(q.geoUrn))+'&network='+enc(wrapNet(q.network))+'&page='+enc(String(pg))+'&origin=FACETED_SEARCH'; const parse = (html) => { const re=/href="https://www.linkedin.com/in/([^"]+)"\s+componentkey="[0-9a-f-]+"/g; const starts=[]; let m; while((m=re.exec(html))){ const slug=m[1].split(/[/?#]/)[0]; starts.push({slug, idx:m.index}); } const noise=/^(Connect|Follow|Send message|Message|View profile|Premium|Status)$/i; const out=[]; for(let k=0;k<starts.length;k++){ let chunk=html.slice(starts[k].idx, (starts[k+1]?starts[k+1].idx:starts[k].idx+2500)); chunk=chunk.slice(chunk.indexOf('>')+1); const lines=chunk.split(/<[^>]+>/).map(s=>s.replace(/&/g,'&').replace(/'/g,"'").replace(/"/g,'"').trim()).filter(Boolean); if(!lines.length) continue; let name=lines[0], degree=null; const dm=name.match(/â¢\s*(1er|2do|3er.|1st|2nd|3rd.|\d+º)/); if(dm){degree=dm[1];name=name.replace(/â¢./,'').trim();} const di=lines.findIndex(l=>/^â¢/.test(l)); if(!degree && di>=0) degree=lines[di].replace(/[â¢\s]/g,''); const rest=lines.slice(1).filter(l=>!noise.test(l) && !/^â¢/.test(l) && l.length>1 && l!==name); const headline=rest[0]||null; const location=rest.find(l=>/,| y alrededores|Argentina|Spain|Espa|United|Francia|Brasil|Chile|M[eé]xico|Egypt|Egipto/.test(l) && l!==headline) || null; out.push({slug:decodeURIComponent(starts[k].slug), name, degree, headline, location, profileUrl:'https://www.linkedin.com/in/'+starts[k].slug}); } return out; }; let tasks=[]; qs.forEach((q,qi)=>{ if (q.page===undefined || q.page===null || String(q.page).trim()==='') return; const start=parseInt(String(q.page),10)||1; let n=parseInt(String(q.pages||'1'),10)||1; if(n<1)n=1; if(n>100)n=100; for(let pg=start; pg<start+n; pg++) tasks.push({qi,pg,q}); }); let skipped=0; if(tasks.length>MAXTASKS){ skipped=tasks.length-MAXTASKS; tasks=tasks.slice(0,MAXTASKS); } const fetched=new Array(tasks.length); let next=0; const worker = async () => { while(true){ const i=next++; if(i>=tasks.length) break; const t=tasks[i]; if(DELAY>0) await sleep(DELAY0.5 + Math.random()*DELAY); try{ const r=await fetch(buildUrl(t.q,t.pg),{credentials:'include'}); const h=await r.text(); fetched[i]={qi:t.qi,pg:t.pg,people:parse(h)}; } catch(e){ fetched[i]={qi:t.qi,pg:t.pg,people:[],error:String(e)}; } } }; await Promise.all(Array.from({length:Math.min(CONC,tasks.length||1)}, worker)); const results=qs.map((q,qi)=>{ if (q.page===undefined || q.page===null || String(q.page).trim()==='') return {query:q,error:'page is required',count:0,results:[]}; const mine=fetched.filter(f=>f&&f.qi===qi).sort((a,b)=>a.pg-b.pg); const seen=new Set(); const merged=[]; mine.forEach(f=>f.people.forEach(p=>{ if(p.slug && !seen.has(p.slug)){seen.add(p.slug); merged.push(p);} })); return {query:q, pagesFetched:mine.length, count:merged.length, results:merged}; }); const ret={count:results.length, results}; if(skipped) ret.skipped=skipped; return ret; } )
Response text contains "LinkedIn".