如何在 CSV 文件中查找特定数值模式并自动归类文件

本文介绍如何使用 java(opencsv)遍历多个 csv 文件,检测某列是否同时包含指定数值(如 50、500、5000),并根据匹配结果将文件移动至 `processed` 或 `invalid_files` 目录,提供可复用、模块化、健壮的实现方案。

在实际数据处理任务中,常需基于内容规则对 CSV 文件进行分类或过滤。例如:若某数值列(如 score)同时出现 50、500 和 5000 这三个值,则认为该文件“含敏感模式”,应移入 invalid_files 目录;否则视为正常,归入 processed 目录。原始代码存在多个关键问题:逻辑判断位置错误(在循环内过早触发移动)、状态重置混乱、br.close() 被重复调用、未跳过表头、且条件硬编码导致扩展性差。

以下为优化后的专业实现,采用职责分离设计,确保清晰、可维护、可扩展:

✅ 核心改进点

  • 条件解耦:使用 Predicate 列表定义“禁止出现的值”,支持任意数量、动态配置;
  • 语义明确的验证逻辑:isLineValid() 判断单行是否 符合要求(即不等于任一禁止值),isFileValid() 判断文件是否 整体有效(即所有行均满足);
  • 高效短路判断:一旦累计违规值数量达目标数(如 3 个),立即返回 false,避免冗余解析;
  • 健壮路径操作:自动创建目标目录,使用 Files.move() 安全迁移,显式处理分隔符与表头;
  • 资源安全:严格依赖 try-with-resources 管理 Reader,杜绝手动 close() 引发的异常。

? 示例 CSV 结构(以分号分隔)

car;score;description
Opel;30;43
Volvo;500;434
Kia;50;3
Toyota;4;4
Mazda;5000;4

? 数据模型(CsvLine.java)

import com.opencsv.bean.CsvBindByPosition;
import java.math.BigDecimal;

public class CsvLine {
    @CsvBindByPosition(position = 1) // 对应 score 列(索引从 0 开始,第 1 位)
    private BigDecimal value;

    public BigDecimal getValue() { return value; }
    public void setValue(BigDecimal value) { this.value = value; }
}

⚙️ 主流程与工具方法

import com.opencsv.bean.CsvToBeanBuilder;
import java.io.*;
import java.math.BigDecimal;
import java.nio.file.*;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class CsvPatternValidator {

    public static void main(String[] args) throws IOException {
        File directory = Path.of("/path/to/your/csv/directory").toFile();
        FilenameFilter csvFilter = (dir, name) -> name.toLowerCase().endsWith(".csv");

        // ✅ 定义“禁止出现”的数值列表(即:若文件中同时存在这些值,则判定为 invalid)
        List
> forbiddenPatterns = List.of(
            ne(new BigDecimal("50")),
            ne(new BigDecimal("500")),
            ne(new BigDecimal("5000"))
        );

        processDirectory(directory, csvFilter, forbiddenPatterns);
    }

    public static void processDirectory(File dir, FilenameFilter filter,
                                        List
> forbidden) throws IOException {
        Path processed = createDirectory(dir.toPath().resolve("processed"));
        Path invalid = createDirectory(dir.toPath().resolve("invalid_files"));

        File[] files = dir.listFiles(filter);
        if (files == null) return;

        for (File file : files) {
            boolean valid = isFileValid(file, forbidden);
            Path target = valid ? processed : invalid;
            Files.move(file.toPath(), target.resolve(file.getName()), StandardCopyOption.REPLACE_EXISTING);
        }
    }

    public static boolean isFileValid(File file, List
> forbidden) throws IOException {
        try (Reader reader = Files.newBufferedReader(file.toPath())) {
            List lines = new CsvToBeanBuilder(reader)
                    .withType(CsvLine.class)
                    .withSkipLines(1)        // 跳过表头行
                    .withSeparator(';')      // 显式指定分隔符
                    .build()
                    .parse();

            Set violations = new HashSet<>();
            for (CsvLine line : lines) {
                if (line.getValue() == null) continue;
                if (!isLineValid(line, forbidden)) {
                    violations.add(line.getValue());
                    if (violations.size() == forbidden.size()) {
                        return false; // 找齐所有禁止值 → 文件无效
                    }
                }
            }
            return true; // 未找齐 → 文件有效
        }
    }

    public static boolean isLineValid(CsvLine line, List
> conditions) {
        return conditions.stream().allMatch(p -> p.test(line.getValue()));
    }

    // 工厂方法:生成“不等于 target”的谓词
    public static > Predicate ne(T target) {
        return value -> value == null || value.compareTo(target) != 0;
    }

    public static Path createDirectory(Path path) throws IOException {
        return Files.exists(path) ? path : Files.createDirectory(path);
    }
}

⚠️ 注意事项

  • 依赖项:需在 pom.xml 中添加 OpenCSV(≥ 5.7.1)和 Lombok(可选,简化 getter/setter):
    
        com.opencsv
        opencsv
        5.7.1
    
  • 编码处理:示例默认 UTF-8;若 CSV 为 UTF-16,请在 Files.newBufferedReader() 中传入 StandardCharsets.UTF_16;
  • 扩展性:新增校验规则只需向 forbiddenPatterns 列表追加 ne(...) 即可,无需修改核心逻辑;
  • 性能提示:对超大文件,可考虑流式解析(如 CsvParser)替代 CsvToBeanBuilder,避免全量加载内存。

该方案彻底解决了原始代码的逻辑缺陷,以函数式风格提升可读性与可测试性,是生产环境中处理 CSV 规则校验的推荐实践。