/** * LinkedInReader — 解析 LinkedIn 官方导出包中的 Connections.csv * * CSV 字段:First Name, Last Name, Email Address, Company, Position, Connected On * 返回:Array<{firstName, lastName, email, company, position, connectedOn}> */ const fs = require('fs'); class LinkedInReader { constructor(filePath) { this.filePath = filePath; } /** * 解析 CSV 文件(纯内置模块实现) * @returns {Array<{firstName, lastName, email, company, position, connectedOn}>} */ parse() { const raw = fs.readFileSync(this.filePath, 'utf-8'); const lines = raw.split(/\r?\n/); // 找到 header 行(跳过 LinkedIn 导出文件顶部的空行/注释行) let headerIndex = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].includes('First Name') && lines[i].includes('Last Name')) { headerIndex = i; break; } } if (headerIndex === -1) { throw new Error('找不到 CSV header 行,请确认文件格式为 LinkedIn Connections.csv'); } const headers = this._parseCsvLine(lines[headerIndex]).map(h => h.trim()); const colIdx = { firstName: headers.findIndex(h => h === 'First Name'), lastName: headers.findIndex(h => h === 'Last Name'), email: headers.findIndex(h => h === 'Email Address'), company: headers.findIndex(h => h === 'Company'), position: headers.findIndex(h => h === 'Position'), connectedOn: headers.findIndex(h => h === 'Connected On'), }; const contacts = []; for (let i = headerIndex + 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const cols = this._parseCsvLine(line); if (cols.length < 2) continue; contacts.push({ firstName: colIdx.firstName >= 0 ? (cols[colIdx.firstName] || '').trim() : '', lastName: colIdx.lastName >= 0 ? (cols[colIdx.lastName] || '').trim() : '', email: colIdx.email >= 0 ? (cols[colIdx.email] || '').trim() : '', company: colIdx.company >= 0 ? (cols[colIdx.company] || '').trim() : '', position: colIdx.position >= 0 ? (cols[colIdx.position] || '').trim() : '', connectedOn: colIdx.connectedOn >= 0 ? (cols[colIdx.connectedOn] || '').trim() : '', }); } return contacts; } /** * 解析单行 CSV(处理引号包裹、逗号、换行) */ _parseCsvLine(line) { const cols = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (ch === '"') { if (inQuotes && line[i + 1] === '"') { // 转义双引号 current += '"'; i++; } else { inQuotes = !inQuotes; } } else if (ch === ',' && !inQuotes) { cols.push(current); current = ''; } else { current += ch; } } cols.push(current); return cols; } } module.exports = { LinkedInReader };