TypeScript——CSV导入、API解析、日志处理通用:一套代码解决所有“脏数据“问题
8/23/2026

文章链接:https://blog.csdn.net/qq_43201350/article/details/160660177

问题

解析器经常会陷入两难:

太严格

  • 只要一行错,整个文件都失败

  • 用户数据只有少量格式问题就不能用

  • 体验极差,用户抱怨不断

太宽松

  • 虽然能跑,但没人知道数据已经坏了

  • 错误被吞掉,系统在"看起来能跑"的状态下持续带病运行

  • 排查问题时才发现数据早就错了

在真实业务里,这两种都不好。

典型的故障现场:

// ❌ 太严格:一条错就全失败
function parseStrict(lines: string[]) {
  const result = [];
  
  for (const line of lines) {
    if (!isValid(line)) {
      throw new Error(`Invalid line: ${line}`); // 💥 整个解析失败
    }
    result.push(parseLine(line));
  }
  
  return result;
}
 
// ❌ 太宽松:错误被吞掉
function parseLoose(lines: string[]) {
  const result = [];
  
  for (const line of lines) {
    try {
      result.push(parseLine(line));
    } catch (error) {
      // 静默失败,没人知道出错了
      console.error(error); 
    }
  }
  
  return result;
}

这类问题的本质不是"解析技术难",而是 如何在容错和可观察之间找到平衡


解决方案

采用**“单条容错、整体评估”**的策略:

  1. 单条错误先记录 - 不立刻终止

  2. 解析结束后统计错误比例 - 决定是否整体失败

  3. 保留结构化错误 - 用于排查和监控

  4. 生成错误报告 - 帮助用户修正数据源

核心思路

输入数据

逐条解析

┌──────────────┐
│ 成功 → 收集   │
│ 失败 → 记录   │ ← 单条容错
└──────────────┘

统计错误率

┌──────────────┐
< 阈值 → 成功 │
│ ≥ 阈值 → 失败 │ ← 整体评估
└──────────────┘

返回结果 + 错误列表

具体实现

数据结构定义

type ParseError = {
  line: number;           // 行号
  content: string;        // 原始内容
  reason: string;         // 错误原因
  severity: 'warning' | 'error';  // 严重程度
};
 
type ParseResult<T> = {
  data: T[];              // 成功解析的数据
  errors: ParseError[];   // 错误列表
  warnings: ParseError[]; // 警告列表
  meta: {
    totalLines: number;
    successLines: number;
    errorLines: number;
    warningLines: number;
    errorRate: number;
  };
};

最小示例

先看一个最小示例,展示核心的容错逻辑:

type ParseError = { line: string; reason: string };
 
function parseLines(lines: string[]) {
  const result: string[] = [];
  const errors: ParseError[] = [];
 
  for (const line of lines) {
    if (!line.trim()) continue;
    
    if (line.startsWith('OK:')) {
      result.push(line.slice(3).trim());
    } else {
      errors.push({ line, reason: 'invalid format' });
    }
  }
 
  // 如果错误率超过50%,认为解析失败
  if (errors.length > lines.length * 0.5) {
    throw new Error(`too many invalid lines: ${errors.length}/${lines.length}`);
  }
 
  return { result, errors };
}

工程级实现

下面这个版本更接近真实项目可用的写法:

type ParseError = {
  line: number;
  content: string;
  reason: string;
  severity: 'warning' | 'error';
};
 
type ParseResult<T> = {
  data: T[];
  errors: ParseError[];
  warnings: ParseError[];
  meta: {
    totalLines: number;
    successLines: number;
    errorLines: number;
    warningLines: number;
    errorRate: number;
  };
};
 
class TolerantParser {
  private readonly ERROR_THRESHOLD = 0.5; // 错误率阈值 50%
 
  /**
   * 解析数据
   */
  parse(content: string): ParseResult<string> {
    const lines = content.split('\n');
    const result: string[] = [];
    const errors: ParseError[] = [];
    const warnings: ParseError[] = [];
    
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      const lineNum = i + 1;
      
      // 跳过空行
      if (!line.trim()) {
        continue;
      }
      
      try {
        const parsed = this.parseLine(line, lineNum);
        
        if (parsed.success) {
          result.push(parsed.value);
          
          // 收集警告
          if (parsed.warnings) {
            warnings.push(...parsed.warnings.map(w => ({
              line: lineNum,
              content: line,
              reason: w,
              severity: 'warning' as const
            })));
          }
        } else {
          errors.push({
            line: lineNum,
            content: line,
            reason: parsed.error,
            severity: 'error'
          });
        }
      } catch (error) {
        errors.push({
          line: lineNum,
          content: line,
          reason: error instanceof Error ? error.message : String(error),
          severity: 'error'
        });
      }
    }
    
    const totalLines = lines.filter(l => l.trim()).length;
    const errorRate = totalLines > 0 ? errors.length / totalLines : 0;
    
    return {
      data: result,
      errors,
      warnings,
      meta: {
        totalLines,
        successLines: result.length,
        errorLines: errors.length,
        warningLines: warnings.length,
        errorRate
      }
    };
  }
 
  /**
   * 解析单行
   */
  private parseLine(line: string, lineNum: number): 
    | { success: true; value: string; warnings?: string[] }
    | { success: false; error: string } 
  {
    const warnings: string[] = [];
    
    // 严格格式: OK:value
    if (line.startsWith('OK:')) {
      const value = line.slice(3).trim();
      
      if (!value) {
        return { success: false, error: 'Empty value' };
      }
      
      // 警告: 值包含空格
      if (value.includes(' ')) {
        warnings.push('Value contains spaces');
      }
      
      return { success: true, value, warnings };
    }
    
    // 宽松格式: 直接返回值
    if (line.startsWith('WARN:')) {
      const value = line.slice(5).trim();
      warnings.push('Using legacy format');
      return { success: true, value, warnings };
    }
    
    return { success: false, error: 'Invalid format' };
  }
 
  /**
   * 评估解析质量
   */
  evaluateQuality(result: ParseResult<any>): 'good' | 'fair' | 'poor' | 'failed' {
    const { errorRate } = result.meta;
    
    if (errorRate > this.ERROR_THRESHOLD) {
      return 'failed';
    }
    
    if (errorRate > 0.3) {
      return 'poor';
    }
    
    if (errorRate > 0.1) {
      return 'fair';
    }
    
    return 'good';
  }
}

使用示例

const parser = new TolerantParser();
 
const content = `
OK:value1
OK:value with spaces
INVALID LINE
OK:value3
WARN:legacy value
ANOTHER INVALID
`;
 
const result = parser.parse(content);
 
console.log('Success:', result.data);
// ['value1', 'value with spaces', 'value3', 'legacy value']
 
console.log('Errors:', result.errors);
// [
//   { line: 4, content: 'INVALID LINE', reason: 'Invalid format', severity: 'error' },
//   { line: 7, content: 'ANOTHER INVALID', reason: 'Invalid format', severity: 'error' }
// ]
 
console.log('Warnings:', result.warnings);
// [
//   { line: 3, content: 'OK:value with spaces', reason: 'Value contains spaces', severity: 'warning' },
//   { line: 6, content: 'WARN:legacy value', reason: 'Using legacy format', severity: 'warning' }
// ]
 
console.log('Stats:', result.meta);
// {
//   totalLines: 6,
//   successLines: 4,
//   errorLines: 2,
//   warningLines: 2,
//   errorRate: 0.33
// }
 
// 评估质量
const quality = parser.evaluateQuality(result);
console.log('Quality:', quality); // 'poor'
 
// 根据质量决定后续处理
if (quality === 'failed') {
  throw new Error('Parsing failed due to too many errors');
} else if (quality === 'poor') {
  console.warn('Data quality is poor, please check the source');
}

进阶优化

1. 错误分级处理

根据错误类型采取不同的处理策略:

type ErrorType = 'syntax' | 'validation' | 'runtime';
 
class TolerantParser {
  private parseLine(line: string, lineNum: number) {
    try {
      // 语法检查
      if (!this.checkSyntax(line)) {
        return { 
          success: false, 
          error: 'Syntax error',
          errorType: 'syntax' as ErrorType
        };
      }
      
      // 语义验证
      const value = this.extractValue(line);
      if (!this.validate(value)) {
        return { 
          success: false, 
          error: 'Validation failed',
          errorType: 'validation' as ErrorType
        };
      }
      
      return { success: true, value };
    } catch (error) {
      return { 
        success: false, 
        error: error.message,
        errorType: 'runtime' as ErrorType
      };
    }
  }
  
  private checkSyntax(line: string): boolean {
    // 基本语法检查
    return line.trim().length > 0;
  }
  
  private validate(value: any): boolean {
    // 业务规则验证
    return value !== null && value !== undefined;
  }
}

2. 错误聚合报告

生成易读的错误报告,帮助用户修正数据:

class ErrorReporter {
  generateReport(result: ParseResult<any>): string {
    const report = [
      `Parse Report`,
      `============`,
      `Total Lines: ${result.meta.totalLines}`,
      `Success: ${result.meta.successLines}`,
      `Errors: ${result.meta.errorLines}`,
      `Warnings: ${result.meta.warningLines}`,
      '',
      `Error Rate: ${(result.meta.errorRate * 100).toFixed(2)}%`,
      `Quality: ${this.getQualityLabel(result.meta.errorRate)}`,
      ''
    ];
    
    if (result.errors.length > 0) {
      report.push('Top Errors:');
      const errorGroups = this.groupErrors(result.errors);
      
      for (const [reason, count] of errorGroups) {
        report.push(`  - ${reason}: ${count} occurrences`);
      }
      
      report.push('');
    }
    
    if (result.warnings.length > 0) {
      report.push('Top Warnings:');
      const warningGroups = this.groupErrors(result.warnings);
      
      for (const [reason, count] of warningGroups) {
        report.push(`  - ${reason}: ${count} occurrences`);
      }
    }
    
    return report.join('\n');
  }
 
  private groupErrors(errors: ParseError[]): Map<string, number> {
    const groups = new Map<string, number>();
    
    for (const error of errors) {
      const count = groups.get(error.reason) || 0;
      groups.set(error.reason, count + 1);
    }
    
    // 按出现次数排序
    return new Map(
      Array.from(groups.entries()).sort((a, b) => b[1] - a[1])
    );
  }
  
  private getQualityLabel(errorRate: number): string {
    if (errorRate > 0.5) return 'FAILED';
    if (errorRate > 0.3) return 'POOR';
    if (errorRate > 0.1) return 'FAIR';
    return 'GOOD';
  }
}
 
// 使用示例
const reporter = new ErrorReporter();
console.log(reporter.generateReport(result));

输出示例:

Parse Report
============
Total Lines: 1000
Success: 850
Errors: 100
Warnings: 50
 
Error Rate: 10.00%
Quality: FAIR
 
Top Errors:
  - Invalid format: 60 occurrences
  - Empty value: 30 occurrences
  - Syntax error: 10 occurrences
 
Top Warnings:
  - Value contains spaces: 30 occurrences
  - Using legacy format: 20 occurrences

3. 错误阈值配置

允许根据不同场景配置不同的错误阈值:

interface ParserConfig {
  errorThreshold: number;      // 错误率阈值
  warningThreshold: number;    // 警告率阈值
  strictMode: boolean;         // 严格模式(任何错误都失败)
  maxErrors: number;           // 最大错误数
}
 
class TolerantParser {
  private config: ParserConfig;
 
  constructor(config: Partial<ParserConfig> = {}) {
    this.config = {
      errorThreshold: config.errorThreshold ?? 0.5,
      warningThreshold: config.warningThreshold ?? 0.3,
      strictMode: config.strictMode ?? false,
      maxErrors: config.maxErrors ?? Infinity
    };
  }
 
  parse(content: string): ParseResult<string> {
    const result = this.doParse(content);
    
    // 严格模式:任何错误都失败
    if (this.config.strictMode && result.errors.length > 0) {
      throw new Error(`Strict mode: parsing failed with ${result.errors.length} errors`);
    }
    
    // 错误率超过阈值
    if (result.meta.errorRate > this.config.errorThreshold) {
      throw new Error(
        `Error rate ${result.meta.errorRate.toFixed(2)} exceeds threshold ${this.config.errorThreshold}`
      );
    }
    
    // 错误数超过最大值
    if (result.errors.length > this.config.maxErrors) {
      throw new Error(
        `Error count ${result.errors.length} exceeds maximum ${this.config.maxErrors}`
      );
    }
    
    return result;
  }
}
 
// 使用示例
const strictParser = new TolerantParser({
  strictMode: true  // 任何错误都失败
});
 
const looseParser = new TolerantParser({
  errorThreshold: 0.8,  // 允许80%错误率
  maxErrors: 1000       // 最多1000个错误
});

4. 日志和监控

将错误上报到监控系统:

class TolerantParser {
  private logger: Logger;
  private metrics: MetricsClient;
 
  parse(content: string): ParseResult<string> {
    const startTime = Date.now();
    const result = this.doParse(content);
    const duration = Date.now() - startTime;
    
    // 记录指标
    this.metrics.increment('parser.total_lines', result.meta.totalLines);
    this.metrics.increment('parser.success_lines', result.meta.successLines);
    this.metrics.increment('parser.error_lines', result.meta.errorLines);
    this.metrics.histogram('parser.duration', duration);
    
    // 错误率过高时告警
    if (result.meta.errorRate > 0.5) {
      this.logger.error('High error rate detected', {
        errorRate: result.meta.errorRate,
        totalLines: result.meta.totalLines,
        errorLines: result.meta.errorLines
      });
    }
    
    // 记录前10个错误到日志
    result.errors.slice(0, 10).forEach(error => {
      this.logger.warn('Parse error', error);
    });
    
    return result;
  }
}

最容易踩的5个坑

1. 错误吞掉不记录

排查问题时不知道哪里错了。

错误做法

try {
  result.push(parseLine(line));
} catch (error) {
  // 静默失败,什么都没记录
}

正确做法

try {
  result.push(parseLine(line));
} catch (error) {
  errors.push({
    line: lineNum,
    content: line,
    reason: error.message,
    severity: 'error'
  });
}

2. 一条错就全失败

用户数据只有少量格式问题就不能用。

错误做法

for (const line of lines) {
  if (!isValid(line)) {
    throw new Error('Invalid line'); // 💥 整个解析失败
  }
}

正确做法

for (const line of lines) {
  try {
    result.push(parseLine(line));
  } catch (error) {
    errors.push(error); // 记录错误,继续解析
  }
}
 
// 最后根据错误率决定是否失败
if (errors.length / lines.length > threshold) {
  throw new Error('Too many errors');
}

3. 错误信息不结构化

只存字符串,没法统计分析。

错误做法

const errors: string[] = [];
errors.push(`Line 5: Invalid format`);

正确做法

const errors: ParseError[] = [];
errors.push({
  line: 5,
  content: 'invalid line',
  reason: 'Invalid format',
  severity: 'error'
});

4. 没有错误阈值

99%的数据都错了还继续处理。

应该设置阈值

if (result.meta.errorRate > 0.5) {
  throw new Error('Error rate too high');
}

5. 警告和错误不分

轻微问题和严重问题混在一起。

应该分级

type Severity = 'warning' | 'error';
 
// 警告:不影响整体解析
warnings.push({
  line: 5,
  reason: 'Value contains spaces',
  severity: 'warning'
});
 
// 错误:可能导致数据丢失
errors.push({
  line: 10,
  reason: 'Invalid format',
  severity: 'error'
});

效果验证

测试要点

  1. 测试正常解析
   test('parse valid data', () => {
     const content = 'OK:value1\nOK:value2';
     const result = parser.parse(content);
  
     expect(result.data).toEqual(['value1', 'value2']);
     expect(result.errors).toHaveLength(0);
     expect(result.meta.successLines).toBe(2);
   });
  1. 测试容错能力
   test('tolerate some errors', () => {
     const content = 'OK:value1\nINVALID\nOK:value2';
     const result = parser.parse(content);
  
     expect(result.data).toEqual(['value1', 'value2']);
     expect(result.errors).toHaveLength(1);
     expect(result.meta.errorRate).toBe(1/3);
   });
  1. 测试错误阈值
   test('fail when error rate exceeds threshold', () => {
     const content = 'INVALID1\nINVALID2\nINVALID3\nOK:value';
  
     expect(() => {
       parser.parse(content);
     }).toThrow('Error rate too high');
   });
  1. 测试严格模式
   test('strict mode fails on any error', () => {
     const strictParser = new TolerantParser({ strictMode: true });
     const content = 'OK:value1\nINVALID';
  
     expect(() => {
       strictParser.parse(content);
     }).toThrow('Strict mode');
   });
  1. 测试错误报告
   test('generate error report', () => {
     const content = 'INVALID1\nINVALID2\nINVALID1';
     const result = parser.parse(content);
  
     const report = reporter.generateReport(result);
  
     expect(report).toContain('Invalid format: 2 occurrences');
   });

监控指标

建议监控:

  • 解析成功率

  • 平均错误率

  • 错误类型分布

  • 解析耗时

  • 告警次数


经验总结

  1. 单条容错,整体评估 个别错误不影响整体,但错误率过高时应该整体失败。这样既保证了可用性,又避免了带病运行。

  2. 错误也要结构化 不要只存错误消息字符串,而要记录行号、内容、原因、严重程度等结构化信息。方便统计分析和生成报告。

  3. 设置合理阈值 不同场景需要不同的阈值。严格场景(如金融数据)可能要求 0 错误,宽松场景(如用户导入)可以容忍 50% 错误。

  4. 生成错误报告 帮助用户了解哪些数据有问题,如何修正。好的错误报告可以显著降低支持成本。

  5. 日志很重要 把错误上报到监控系统,及时发现数据质量问题。错误率突然升高可能意味着上游系统出了问题。

  6. 适用场景广泛 这套容错解析方案不仅适用于文本解析,还适合:

    • 数据导入(CSV、Excel)

    • 第三方 API 响应解析

    • 配置文件读取

    • 日志分析

    • ETL 数据处理

凡是"数据来源不可控,格式可能不规范"的场景,都可以考虑这种容错但可观察的设计。