Last active
January 15, 2025 15:36
-
-
Save IlanVivanco/68dec8a5a1ee6eac8693de88793c36e3 to your computer and use it in GitHub Desktop.
Extracts useful data from the Applicants view on LinkedIn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Extract LinkedIn Applicants | |
// @namespace http://tampermonkey.net/ | |
// @version 2025-01-15 | |
// @description Extracts useful data from the Applicants view on LinkedIn | |
// @author Ilán Vivanco - https://ilanvivanco.com | |
// @match https://www.linkedin.com/hiring/jobs/xxxxxx/applicants/xxxxx/detail/?r=UNRATED%2CGOOD_FIT%2CMAYBE | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=linkedin.com | |
// @grant none | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
const applicantLinks = document.querySelectorAll('.hiring-applicants__list-item a'); | |
const applicantsData = []; | |
/** | |
* Generator function to yield each applicant link. | |
* This provides control over the iteration process. | |
* @param {NodeList} links - List of applicant links. | |
* @returns {Generator} - Yields one link at a time. | |
*/ | |
function* applicantLinkGenerator(links) { | |
for (const link of links) { | |
yield link; | |
} | |
} | |
/** | |
* Delays execution for a specified time. | |
* @param {number} ms - Milliseconds to wait. | |
* @returns {Promise<void>} | |
*/ | |
function wait(ms) { | |
return new Promise((resolve) => setTimeout(resolve, ms)); | |
} | |
/** | |
* Converts an array of objects into a CSV formatted string. | |
* @param {Object[]} data - Array of data objects. | |
* @returns {string} - CSV formatted string. | |
*/ | |
function convertToCSV(data) { | |
if (!data.length) return ''; | |
// Extract header keys and create the header row | |
const keys = Object.keys(data[0]); | |
const header = keys.join(','); | |
// Create rows by mapping each object to a CSV row | |
const rows = data.map((item) => keys.map((key) => `"${(item[key] || '').replace(/"/g, '""')}"`).join(',')); | |
// Combine header and rows into a single CSV string | |
return [header, ...rows].join('\n'); | |
} | |
/** | |
* Triggers a download of the CSV data. | |
* @param {Object[]} data - Data to be converted and downloaded as CSV. | |
* @param {string} [filename='applicants.csv'] - The filename for the downloaded CSV. | |
*/ | |
function downloadCSV(data, filename = 'applicants.csv') { | |
const csvData = convertToCSV(data); | |
const blob = new Blob([csvData], { type: 'text/csv' }); | |
const url = URL.createObjectURL(blob); | |
// Create a temporary anchor to simulate the download | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = filename; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
} | |
/** | |
* Triggers a download of the resume with a specific naming convention. | |
* @param {string} url - The URL of the resume file. | |
* @param {string} name - The name of the applicant for the file naming. | |
*/ | |
function downloadResume(url, name = 'unknown-applicant') { | |
if (!url) { | |
console.error('No resume URL found.'); | |
return; | |
} | |
// Generate a sanitized file name using the applicant's name | |
const sanitizedFileName = | |
name | |
.toLowerCase() | |
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric characters with "-" | |
.replace(/^-+|-+$/g, '') + // Trim leading and trailing hyphens | |
'-cv.pdf'; | |
// Create an anchor element to trigger the download | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = sanitizedFileName; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
} | |
/** | |
* Extracts data for each applicant link and saves it as a CSV. | |
* Iterates sequentially to ensure data loads before extraction. | |
*/ | |
async function extractData() { | |
const generator = applicantLinkGenerator(applicantLinks); | |
let current = generator.next(); | |
while (!current.done) { | |
const applicantLink = current.value; | |
try { | |
// Simulate navigation to the applicant's details | |
applicantLink.click(); | |
await wait(2000); // Wait for the page to load | |
document.querySelectorAll('.hiring-applicant-header-actions button')[2]?.click(); // Open the "More" dropdown | |
await wait(500); // Wait for the dropdown to load | |
// Extract data for the current applicant | |
const name = applicantLink.querySelector('.artdeco-entity-lockup__title')?.innerText || ''; | |
const position = applicantLink.querySelector('.artdeco-entity-lockup__metadata')?.innerText || ''; | |
const profileLink = document.querySelector('.hiring-profile-highlights__see-full-profile a')?.href || ''; | |
const email = | |
document.querySelectorAll('.hiring-applicant-header-actions__more-content-dropdown-item-text')[0] | |
?.innerText || ''; | |
const phone = | |
document.querySelectorAll('.hiring-applicant-header-actions__more-content-dropdown-item-text')[1] | |
?.innerText || ''; | |
const resumeLink = document.querySelector('.link-without-visited-state')?.href || ''; | |
// Append extracted data | |
const rowData = { | |
rate: '-', | |
name, | |
title: position, | |
email, | |
cv: 'CV link', | |
comments: '', | |
profile: profileLink, | |
}; | |
console.info(rowData); | |
applicantsData.push(rowData); | |
// If this is the first applicant, download their resume | |
if (resumeLink) { | |
downloadResume(resumeLink, name); | |
} | |
} catch (error) { | |
console.error('Error extracting data for an applicant:', error); | |
} | |
// Move to the next applicant | |
current = generator.next(); | |
} | |
// Trigger CSV download once all data is extracted | |
downloadCSV(applicantsData); | |
} | |
// Start data extraction | |
extractData(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment