您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Manually scroll the IMOS dynamic roster while the script captures visible rows, then click to stop and download CSV.
// ==UserScript== // @name IMOS Manual-Scroll Missionary Roster Scraper // @namespace http://tampermonkey.net/ // @version 1.2 // @description Manually scroll the IMOS dynamic roster while the script captures visible rows, then click to stop and download CSV. // @author You // @match https://imos.churchofjesuschrist.org/dynamic-roster/* // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; /******************** * UI: floating button ********************/ const style = document.createElement('style'); style.textContent = ` #imos-monkey-btn { position: fixed; bottom: 20px; left: 20px; background: #1e90ff; color: white; border: none; padding: 10px 14px; font-size: 14px; border-radius: 10px; z-index: 2147483647; cursor: pointer; box-shadow: 0 6px 18px rgba(0,0,0,0.25); display: flex; gap: 8px; align-items: center; white-space: nowrap; } #imos-monkey-badge { background: rgba(255,255,255,0.15); padding: 4px 8px; border-radius: 999px; font-weight: 600; } #imos-monkey-hint { position: fixed; bottom: 70px; left: 20px; background: rgba(0,0,0,0.8); color: white; padding: 8px 10px; border-radius: 8px; z-index: 2147483646; font-size: 13px; max-width: 360px; } `; document.head.appendChild(style); const btn = document.createElement('button'); btn.id = 'imos-monkey-btn'; btn.innerHTML = '🐒 Start manual scrape <span id="imos-monkey-badge">0</span>'; document.body.appendChild(btn); const hint = document.createElement('div'); hint.id = 'imos-monkey-hint'; hint.innerText = 'Click "Start manual scrape", then manually scroll the roster (slowly) until you reach the end. Click the button again to Stop & Download.'; document.body.appendChild(hint); /******************** * Scrape helpers ********************/ let recording = false; const collected = new Map(); // key -> data object let intervalId = null; let scrollTimeout = null; function updateBadge() { const b = document.getElementById('imos-monkey-badge'); if (b) b.textContent = `${collected.size}`; if (recording) { btn.style.background = '#0b63d6'; } else { btn.style.background = '#1e90ff'; } } function safeText(el) { if (!el) return ''; return (el.textContent || '').replace(/\s+/g, ' ').trim(); } function parseRow(row) { // row is a <tr> element try { const nameAnchor = row.querySelector('.col-preferredName a.missionary-name') || row.querySelector('.col-preferredName a') || row.querySelector('a[href*="detail"]'); if (!nameAnchor) return null; const href = (nameAnchor.getAttribute('href') || '').trim(); const imosMatch = href.match(/detail\/default\/(\d+)(?:\/|$)/) || href.match(/detail\/.*\/(\d+)(?:\/|$)/); const imosNumber = imosMatch ? imosMatch[1] : ''; const fullName = safeText(nameAnchor); let lastName = ''; let firstName = ''; if (fullName.includes(',')) { const parts = fullName.split(',', 2); lastName = parts[0].trim(); firstName = parts[1].trim(); } else { // fallback firstName = fullName; } const emailNode = row.querySelector('.missionary-email a[href^="mailto:"]'); const email = emailNode ? (emailNode.getAttribute('href') || '').replace(/^mailto:/i, '').trim() : ''; const missionId = safeText(row.querySelector('.col-legacyMissId span')) || safeText(row.querySelector('.col-legacyMissId')); const title = safeText(row.querySelector('.col-missType span')) || safeText(row.querySelector('.col-missType')); const assignmentType = safeText(row.querySelector('.col-assignment span')) || safeText(row.querySelector('.col-assignment')); const status = safeText(row.querySelector('.col-status span')) || safeText(row.querySelector('.col-status')); const zone = safeText(row.querySelector('.col-zone span')) || safeText(row.querySelector('.col-zone')); const district = safeText(row.querySelector('.col-district span')) || safeText(row.querySelector('.col-district')); const area = safeText(row.querySelector('.col-area span')) || safeText(row.querySelector('.col-area')); // phone may be nested divs let phone = ''; const phoneContainer = row.querySelector('.col-areaPhoneNumbers'); if (phoneContainer) { // find first non-empty inner text in its children const divs = phoneContainer.querySelectorAll('div, p, span'); for (const d of divs) { const t = safeText(d); if (t && /\d/.test(t)) { phone = t; break; } } if (!phone) phone = safeText(phoneContainer); } const mtcDate = safeText(row.querySelector('.col-mtcDate span')) || safeText(row.querySelector('.col-mtcDate')); const arrivalDate = safeText(row.querySelector('.col-arrivalDate span')) || safeText(row.querySelector('.col-arrivalDate')); const releaseDate = safeText(row.querySelector('.col-releaseDate span')) || safeText(row.querySelector('.col-releaseDate')); return { imosNumber, lastName, firstName, email, missionId, title, assignmentType, status, zone, district, area, phone, mtcDate, arrivalDate, releaseDate, fullName }; } catch (e) { console.error('parseRow error', e); return null; } } function collectVisibleRows() { const rows = document.querySelectorAll('tbody.imos-library-table-place-content-above-thead tr'); if (!rows || rows.length === 0) return; let added = 0; rows.forEach((r) => { const obj = parseRow(r); if (!obj) return; const key = obj.imosNumber || obj.missionId || `${obj.lastName}|${obj.firstName}` || obj.fullName; if (!collected.has(key)) { collected.set(key, obj); added++; } }); if (added > 0) updateBadge(); } /******************** * CSV creation + download ********************/ function escapeCsvField(str) { if (str === null || str === undefined) return ''; const s = String(str); if (s.includes('"') || s.includes(',') || s.includes('\n')) { return '"' + s.replace(/"/g, '""') + '"'; } return s; } function buildAndDownloadCSV() { // header order (adjustable) const headers = [ 'IMOS Number', 'Last Name', 'First Name', 'Email', 'Missionary ID', 'Title', 'Type', 'Status', 'Zone', 'District', 'Area', 'Phone', 'MTC Date', 'Arrival Date', 'Release Date' ]; const rows = [headers.join(',')]; for (const obj of collected.values()) { const row = [ escapeCsvField(obj.imosNumber), escapeCsvField(obj.lastName), escapeCsvField(obj.firstName), escapeCsvField(obj.email), escapeCsvField(obj.missionId), escapeCsvField(obj.title), escapeCsvField(obj.assignmentType), escapeCsvField(obj.status), escapeCsvField(obj.zone), escapeCsvField(obj.district), escapeCsvField(obj.area), escapeCsvField(obj.phone), escapeCsvField(obj.mtcDate), escapeCsvField(obj.arrivalDate), escapeCsvField(obj.releaseDate) ]; rows.push(row.join(',')); } const csvContent = rows.join('\n'); // filename: YYYY-MM-DD_HH-MM-SS_missionary_roster_scraped.csv (local time) const now = new Date(); const pad = (n) => String(n).padStart(2, '0'); const filename = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}_missionary_roster_scraped.csv`; const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } /******************** * Start / Stop logic ********************/ function startRecording() { if (recording) return; recording = true; collected.clear(); updateBadge(); btn.innerHTML = `⏳ Stop & Download <span id="imos-monkey-badge">${collected.size}</span>`; // immediate collect of whatever is already visible collectVisibleRows(); // collect on scroll (throttled) window.addEventListener('scroll', onScrollCapture, { passive: true }); // also poll periodically (in case virtual scroll updates without user scroll) intervalId = setInterval(collectVisibleRows, 1200); } function stopRecording() { if (!recording) return; recording = false; window.removeEventListener('scroll', onScrollCapture); if (intervalId) { clearInterval(intervalId); intervalId = null; } // final collect to get anything newly visible collectVisibleRows(); updateBadge(); btn.innerHTML = `✅ Downloading (${collected.size})`; // download buildAndDownloadCSV(); // reset UI after short delay setTimeout(() => { btn.innerHTML = `🐒 Start manual scrape <span id="imos-monkey-badge">${collected.size}</span>`; collected.clear(); updateBadge(); }, 900); } function onScrollCapture() { if (!recording) return; if (scrollTimeout) clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { collectVisibleRows(); }, 150); } // toggle button behavior btn.addEventListener('click', () => { if (!recording) { startRecording(); } else { stopRecording(); } }); // small safety: if user navigates away/hard reload, stop interval window.addEventListener('beforeunload', () => { if (intervalId) clearInterval(intervalId); }); })();