Skip to content

Instantly share code, notes, and snippets.

@IlanVivanco
Last active January 15, 2025 15:36
Show Gist options
  • Save IlanVivanco/68dec8a5a1ee6eac8693de88793c36e3 to your computer and use it in GitHub Desktop.
Save IlanVivanco/68dec8a5a1ee6eac8693de88793c36e3 to your computer and use it in GitHub Desktop.
Extracts useful data from the Applicants view on LinkedIn
// ==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