linkedin-reader.js 3.37 KB
/**
 * 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 };