LinkedIn Feed Automation & Excel Export

Auto-expands posts, filters ads, exports to Excel with scroll tracking

Size

17.0 KB

Version

1.0.1

Created

Jan 6, 2026

Updated

15 days ago

1// ==UserScript==
2// @name		LinkedIn Feed Automation & Excel Export
3// @description		Auto-expands posts, filters ads, exports to Excel with scroll tracking
4// @version		1.0.1
5// @match		https://*.linkedin.com/*
6// @icon		https://static.licdn.com/aero-v1/sc/h/3loy7tajf3n0cho89wgg0fjre
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.xmlhttpRequest
10// @require		https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
11// ==/UserScript==
12(function() {
13    'use strict';
14
15    // Configuration
16    const CONFIG = {
17        FIRST_RUN_LIMIT: 100,
18        MAX_POSTS: 200,
19        SCROLL_DELAY: 2000,
20        CLICK_DELAY: 500,
21        STORAGE_KEY: 'linkedin_jobs_last_run',
22        JOBS_DATA_KEY: 'linkedin_jobs_data'
23    };
24
25    // State management
26    let isRunning = false;
27    let collectedJobs = [];
28    let processedJobIds = new Set();
29
30    // Utility: Debounce function
31    function debounce(func, wait) {
32        let timeout;
33        return function executedFunction(...args) {
34            const later = () => {
35                clearTimeout(timeout);
36                func(...args);
37            };
38            clearTimeout(timeout);
39            timeout = setTimeout(later, wait);
40        };
41    }
42
43    // Utility: Wait function
44    function wait(ms) {
45        return new Promise(resolve => setTimeout(resolve, ms));
46    }
47
48    // Extract job data from a job card element
49    function extractJobCardData(jobElement) {
50        try {
51            const jobId = jobElement.getAttribute('data-occludable-job-id');
52            
53            if (!jobId || processedJobIds.has(jobId)) {
54                return null;
55            }
56
57            const titleElement = jobElement.querySelector('.job-card-list__title--link, a[aria-label*=""]');
58            const title = titleElement ? titleElement.textContent.trim().replace(/\s+/g, ' ') : '';
59            const link = titleElement ? titleElement.href : '';
60            
61            const companyElement = jobElement.querySelector('.artdeco-entity-lockup__subtitle, .job-card-container__primary-description');
62            const company = companyElement ? companyElement.textContent.trim() : '';
63            
64            const locationElement = jobElement.querySelector('.job-card-container__metadata-item');
65            const location = locationElement ? locationElement.textContent.trim() : '';
66            
67            const timeElement = jobElement.querySelector('.job-card-container__listed-time, time');
68            const timePosted = timeElement ? timeElement.textContent.trim() : '';
69
70            console.log(`Extracted job card: ${title} at ${company}`);
71
72            return {
73                jobId,
74                title,
75                company,
76                location,
77                timePosted,
78                link,
79                extractedAt: new Date().toISOString()
80            };
81        } catch (error) {
82            console.error('Error extracting job card data:', error);
83            return null;
84        }
85    }
86
87    // Click on a job to load its details
88    async function clickJobCard(jobElement) {
89        try {
90            const clickableElement = jobElement.querySelector('.job-card-container__link, .job-card-list__title--link');
91            if (clickableElement) {
92                clickableElement.click();
93                await wait(CONFIG.CLICK_DELAY);
94                return true;
95            }
96        } catch (error) {
97            console.error('Error clicking job card:', error);
98        }
99        return false;
100    }
101
102    // Extract detailed job information from the details panel
103    function extractJobDetails() {
104        try {
105            const detailsPanel = document.querySelector('.jobs-details__main-content, .jobs-search__job-details--container');
106            if (!detailsPanel) {
107                return {};
108            }
109
110            // Job type (Contract, Full-time, etc.)
111            const jobTypeElements = detailsPanel.querySelectorAll('.job-details-jobs-unified-top-card__job-insight span, .job-details-jobs-unified-top-card__job-insight');
112            let jobType = '';
113            let workplaceType = '';
114            let isExternal = false;
115
116            jobTypeElements.forEach(el => {
117                const text = el.textContent.trim();
118                if (text.match(/contract|full-time|part-time|temporary|internship|freelance/i)) {
119                    jobType = text;
120                }
121                if (text.match(/remote|on-site|hybrid/i)) {
122                    workplaceType = text;
123                }
124            });
125
126            // Check if externally managed
127            const externalIndicator = detailsPanel.querySelector('.job-details-jobs-unified-top-card__job-insight--highlight, [class*="external"]');
128            const applyButton = detailsPanel.querySelector('button[aria-label*="Easy Apply"], .jobs-apply-button');
129            isExternal = !applyButton || detailsPanel.textContent.includes('externally') || detailsPanel.textContent.includes('extern');
130
131            // Get full description
132            const descriptionElement = detailsPanel.querySelector('.jobs-description__content, .jobs-description-content__text');
133            const description = descriptionElement ? descriptionElement.textContent.trim() : '';
134
135            // Get seniority level
136            const seniorityElement = detailsPanel.querySelector('.job-details-jobs-unified-top-card__job-insight--highlight');
137            const seniorityLevel = seniorityElement ? seniorityElement.textContent.trim() : '';
138
139            // Get number of applicants
140            const applicantsElement = detailsPanel.querySelector('.job-details-jobs-unified-top-card__applicant-count, [class*="applicant"]');
141            const applicants = applicantsElement ? applicantsElement.textContent.trim() : '';
142
143            console.log(`Extracted details: Type=${jobType}, External=${isExternal}, Workplace=${workplaceType}`);
144
145            return {
146                jobType,
147                workplaceType,
148                isExternal,
149                seniorityLevel,
150                applicants,
151                description: description.substring(0, 500) // Limit description length
152            };
153        } catch (error) {
154            console.error('Error extracting job details:', error);
155            return {};
156        }
157    }
158
159    // Scroll to load more jobs
160    async function scrollToLoadMore() {
161        const jobsList = document.querySelector('.jobs-search-results-list, .scaffold-layout__list');
162        if (jobsList) {
163            jobsList.scrollTop = jobsList.scrollHeight;
164            await wait(CONFIG.SCROLL_DELAY);
165            console.log('Scrolled to load more jobs');
166            return true;
167        }
168        return false;
169    }
170
171    // Main collection function
172    async function collectJobs() {
173        if (isRunning) {
174            console.log('Collection already running');
175            return;
176        }
177
178        isRunning = true;
179        collectedJobs = [];
180        processedJobIds = new Set();
181
182        try {
183            // Get last run info
184            const lastRun = await GM.getValue(CONFIG.STORAGE_KEY, null);
185            const maxJobs = lastRun ? CONFIG.MAX_POSTS : CONFIG.FIRST_RUN_LIMIT;
186            
187            console.log(`Starting job collection. Max jobs: ${maxJobs}, Last run: ${lastRun}`);
188            updateStatus(`Collecting jobs... (0/${maxJobs})`);
189
190            let previousCount = 0;
191            let noNewJobsCount = 0;
192
193            while (collectedJobs.length < maxJobs && noNewJobsCount < 3) {
194                // Get all job cards
195                const jobCards = document.querySelectorAll('li[data-occludable-job-id]');
196                console.log(`Found ${jobCards.length} job cards on page`);
197
198                // Process each job card
199                for (let i = 0; i < jobCards.length && collectedJobs.length < maxJobs; i++) {
200                    const jobCard = jobCards[i];
201                    const basicData = extractJobCardData(jobCard);
202
203                    if (!basicData) continue;
204
205                    // Check if we've reached the last run timestamp
206                    if (lastRun && basicData.extractedAt <= lastRun) {
207                        console.log('Reached last run timestamp, stopping collection');
208                        noNewJobsCount = 3;
209                        break;
210                    }
211
212                    // Click to load details
213                    await clickJobCard(jobCard);
214                    await wait(CONFIG.CLICK_DELAY);
215
216                    // Extract detailed information
217                    const detailedData = extractJobDetails();
218
219                    // Combine data
220                    const fullJobData = {
221                        ...basicData,
222                        ...detailedData
223                    };
224
225                    collectedJobs.push(fullJobData);
226                    processedJobIds.add(basicData.jobId);
227
228                    updateStatus(`Collecting jobs... (${collectedJobs.length}/${maxJobs})`);
229                    console.log(`Collected job ${collectedJobs.length}: ${basicData.title}`);
230                }
231
232                // Check if we got new jobs
233                if (collectedJobs.length === previousCount) {
234                    noNewJobsCount++;
235                } else {
236                    noNewJobsCount = 0;
237                    previousCount = collectedJobs.length;
238                }
239
240                // Scroll to load more
241                if (collectedJobs.length < maxJobs && noNewJobsCount < 3) {
242                    await scrollToLoadMore();
243                }
244            }
245
246            // Save collected data
247            await GM.setValue(CONFIG.JOBS_DATA_KEY, JSON.stringify(collectedJobs));
248            await GM.setValue(CONFIG.STORAGE_KEY, new Date().toISOString());
249
250            console.log(`Collection complete. Total jobs: ${collectedJobs.length}`);
251            updateStatus(`Collection complete! ${collectedJobs.length} jobs collected`);
252
253        } catch (error) {
254            console.error('Error during job collection:', error);
255            updateStatus('Error during collection. Check console.');
256        } finally {
257            isRunning = false;
258        }
259    }
260
261    // Export to Excel
262    async function exportToExcel() {
263        try {
264            const savedData = await GM.getValue(CONFIG.JOBS_DATA_KEY, '[]');
265            const jobs = JSON.parse(savedData);
266
267            if (jobs.length === 0) {
268                alert('No jobs to export. Please collect jobs first.');
269                return;
270            }
271
272            console.log(`Exporting ${jobs.length} jobs to Excel`);
273
274            // Prepare data for Excel
275            const excelData = jobs.map(job => ({
276                'Job ID': job.jobId,
277                'Title': job.title,
278                'Company': job.company,
279                'Location': job.location,
280                'Job Type': job.jobType || '',
281                'Workplace Type': job.workplaceType || '',
282                'External': job.isExternal ? 'Yes' : 'No',
283                'Seniority Level': job.seniorityLevel || '',
284                'Applicants': job.applicants || '',
285                'Time Posted': job.timePosted,
286                'Description': job.description || '',
287                'Link': job.link,
288                'Extracted At': job.extractedAt
289            }));
290
291            // Create workbook
292            const ws = XLSX.utils.json_to_sheet(excelData);
293            const wb = XLSX.utils.book_new();
294            XLSX.utils.book_append_sheet(wb, ws, 'LinkedIn Jobs');
295
296            // Auto-size columns
297            const maxWidth = 50;
298            const colWidths = Object.keys(excelData[0]).map(key => {
299                const maxLen = Math.max(
300                    key.length,
301                    ...excelData.map(row => String(row[key] || '').length)
302                );
303                return { wch: Math.min(maxLen, maxWidth) };
304            });
305            ws['!cols'] = colWidths;
306
307            // Generate filename with timestamp
308            const timestamp = new Date().toISOString().split('T')[0];
309            const filename = `LinkedIn_Jobs_${timestamp}.xlsx`;
310
311            // Write file
312            XLSX.writeFile(wb, filename);
313
314            console.log(`Excel file exported: ${filename}`);
315            updateStatus(`Exported ${jobs.length} jobs to ${filename}`);
316
317        } catch (error) {
318            console.error('Error exporting to Excel:', error);
319            alert('Error exporting to Excel. Check console for details.');
320        }
321    }
322
323    // Export to JSON
324    async function exportToJSON() {
325        try {
326            const savedData = await GM.getValue(CONFIG.JOBS_DATA_KEY, '[]');
327            const jobs = JSON.parse(savedData);
328
329            if (jobs.length === 0) {
330                alert('No jobs to export. Please collect jobs first.');
331                return;
332            }
333
334            const timestamp = new Date().toISOString().split('T')[0];
335            const filename = `LinkedIn_Jobs_${timestamp}.json`;
336            
337            const blob = new Blob([JSON.stringify(jobs, null, 2)], { type: 'application/json' });
338            const url = URL.createObjectURL(blob);
339            
340            const a = document.createElement('a');
341            a.href = url;
342            a.download = filename;
343            a.click();
344            
345            URL.revokeObjectURL(url);
346
347            console.log(`JSON file exported: ${filename}`);
348            updateStatus(`Exported ${jobs.length} jobs to ${filename}`);
349
350        } catch (error) {
351            console.error('Error exporting to JSON:', error);
352            alert('Error exporting to JSON. Check console for details.');
353        }
354    }
355
356    // Update status display
357    function updateStatus(message) {
358        const statusElement = document.getElementById('linkedin-jobs-status');
359        if (statusElement) {
360            statusElement.textContent = message;
361        }
362    }
363
364    // Create UI controls
365    function createUI() {
366        // Check if UI already exists
367        if (document.getElementById('linkedin-jobs-control-panel')) {
368            return;
369        }
370
371        const controlPanel = document.createElement('div');
372        controlPanel.id = 'linkedin-jobs-control-panel';
373        controlPanel.innerHTML = `
374            <div style="position: fixed; top: 80px; right: 20px; z-index: 9999; background: white; border: 2px solid #0a66c2; border-radius: 8px; padding: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 280px; font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
375                <div style="font-weight: 600; font-size: 16px; color: #000; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
376                    <span>LinkedIn Jobs Extractor</span>
377                    <button id="linkedin-jobs-close" style="background: none; border: none; font-size: 20px; cursor: pointer; color: #666; padding: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;">×</button>
378                </div>
379                <div id="linkedin-jobs-status" style="font-size: 13px; color: #666; margin-bottom: 12px; min-height: 20px;">Ready to collect jobs</div>
380                <button id="linkedin-jobs-collect" style="width: 100%; padding: 10px; background: #0a66c2; color: white; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; margin-bottom: 8px; font-size: 14px;">
381                    Start Collection
382                </button>
383                <button id="linkedin-jobs-export-excel" style="width: 100%; padding: 10px; background: #057642; color: white; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; margin-bottom: 8px; font-size: 14px;">
384                    Export to Excel
385                </button>
386                <button id="linkedin-jobs-export-json" style="width: 100%; padding: 10px; background: #5e5e5e; color: white; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; font-size: 14px;">
387                    Export to JSON
388                </button>
389            </div>
390        `;
391
392        document.body.appendChild(controlPanel);
393
394        // Add event listeners
395        document.getElementById('linkedin-jobs-collect').addEventListener('click', collectJobs);
396        document.getElementById('linkedin-jobs-export-excel').addEventListener('click', exportToExcel);
397        document.getElementById('linkedin-jobs-export-json').addEventListener('click', exportToJSON);
398        document.getElementById('linkedin-jobs-close').addEventListener('click', () => {
399            controlPanel.remove();
400        });
401
402        console.log('LinkedIn Jobs Extractor UI created');
403    }
404
405    // Initialize
406    async function init() {
407        // Only run on LinkedIn jobs pages
408        if (!window.location.href.includes('linkedin.com/jobs')) {
409            console.log('Not on LinkedIn jobs page, extension inactive');
410            return;
411        }
412
413        console.log('LinkedIn Jobs Extractor initialized');
414
415        // Wait for page to load
416        if (document.readyState === 'loading') {
417            document.addEventListener('DOMContentLoaded', createUI);
418        } else {
419            // Wait a bit for dynamic content to load
420            setTimeout(createUI, 2000);
421        }
422    }
423
424    // Start the extension
425    init();
426
427})();