java - 用 Java 设计智能自定义类 csv 解析器

标签 java parsing csv

问题描述

我想重构一个解析器,以实现灵活的 csv 格式,该格式描述第一行中的列。根据这些信息,我希望解析器构建具有简单属性但也具有复杂属性的对象,例如 List<String> (空格分隔),例如Thing s:

数据类型示例

import java.util.List;

public class Thing {
    protected int           foo;
    protected String        bar
    protected List<String>  baz;

    public Thing(int foo, String bar, List<String> baz) {
        this.foo = foo;
        this.bar = bar;
        this.baz = baz;
    }

    public String toString() {
        return "foo: " + foo + ", bar: " + bar + ", baz: " + baz;
    }
}

解析器的输入将是文本文件,第一行带有列行(逗号分隔),数据位于 n 中。下一行(逗号分隔)。为了简化测试,我将使用 Iterator<String>用于输入线。这个简单的测试应该说明我想要构建什么:

JUnit 测试

// prepare example string iterator
List<String> lines = new ArrayList<String>();
lines.add("bar,baz,foo");
lines.add("yay,quux quuux,17");
lines.add("hey,qaax qaaax,42");

// test parsed things
List<Thing> things = ThingBuilder.buildThings(lines.iterator());
assertNotNull(things);
assertEquals(2, things.size());
assertEquals("foo: 17, bar: yay, baz: [quux, quuux]", things.get(0).toString());
assertEquals("foo: 42, bar: hey, baz: [qaax, qaaax]", things.get(1).toString());

最简单的方法

  1. 读取第一行并将其拆分为列名称
  2. 读取所有其他行并对它们执行以下操作:
    • 将行拆分为标记
    • 循环它们:
      • 代币 i干大事switch/else if关于列名称 i
      • 转换 token i
      • 将提取的值存储在某处
    • 收集所有内容并构建 Thing
  3. 完成。

我的这种方法的问题是内部开关。处理完第一行后,应该清楚各行是如何解析的。

我想要什么

在带有闭包的语言中,我会尝试以下操作:

  1. 读取第一行并将其拆分为列名称
  2. 为每个列名创建一个闭包,为给定标记设置正确的值并将其添加到解析器闭包数组
  3. 读取所有其他行并对它们执行以下操作:
    • 将行拆分为标记
    • 循环它们:
      • 调用解析器闭包i带 token i
    • 收集所有内容并构建 Thing
  4. 完成。

我尝试了什么

我有一个适用于所有三个标记解析器的简单界面。他们应该获得一个 token 并将生成的值注入(inject)给定的 ThingBuilder 中。的缓存:

public interface TokenParser {
    public void parse(String token, ThingBuilder builder);
}

public class FooParser implements TokenParser {
    @Override public void parse(String token, ThingBuilder builder) {
        builder.setFoo(Integer.parseInt(token));
    }
}

public class BarParser implements TokenParser {
    @Override public void parse(String token, ThingBuilder builder) {
        builder.setBar(token);
    }
}

import java.util.ArrayList;
import java.util.List;
public class BazParser implements TokenParser {
    @Override public void parse(String token, ThingBuilder builder) {
        List<String> baz = new ArrayList<String>();
        for (String s : token.split(" ")) baz.add(s);
        builder.setBaz(baz);
    }
}

我的ThingBuilderbuildThings方法是静态的并创建一个 ThingBuilder对象内部,构造函数获取第一(列)行。这也是填充标记解析器列表的地方。之后隐藏ThingBuilder对象已准备就绪,并具有以下输入行 buildThing重复调用方法来创建 Thing 的列表s:

import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;

public class ThingBuilder {

    // single column parsers
    protected List<TokenParser> columnParsers;

    // thing attribute cache
    protected int           fooCache;
    protected String        barCache;
    protected List<String>  bazCache;

    // thing attribute cache setter
    public void setFoo(int          foo) { fooCache = foo; }
    public void setBar(String       bar) { barCache = bar; }
    public void setBaz(List<String> baz) { bazCache = baz; }

    // cleanup helper method
    protected void cleanup() {
        setFoo(0); setBar(null); setBaz(null);
    }

    // statically build a list of things from given lines
    public static List<Thing> buildThings(Iterator<String> lines) {

        // prepare builder with the first line
        ThingBuilder builder = new ThingBuilder(lines.next());

        // parse things
        List<Thing> things = new ArrayList<Thing>();
        while (lines.hasNext()) {
            things.add(builder.buildThing(lines.next()));
        }
        return things;
    }

    // prepares a builder to parse thing lines
    protected ThingBuilder(String columnLine) {

        // split line into columns
        String[] columns = columnLine.split(",");

        // prepare a parser for each column
        columnParsers = new ArrayList<TokenParser>();
        for (String column : columns) {
            TokenParser parser;
            if      (column.equals("foo")) parser = new FooParser();
            else if (column.equals("bar")) parser = new BarParser();
            else if (column.equals("baz")) parser = new BazParser();
            else throw new RuntimeException("unknown column: " + column);
            columnParsers.add(parser);
        }
    }

    // builds a thing from a string
    protected Thing buildThing(String line) {

        // split the line in tokens
        String[] tokens = line.split(",");

        // let the parsers do the work
        for (int i = 0; i < tokens.length; i++) {
            columnParsers.get(i).parse(tokens[i], this);
        }

        // hopefully they're done
        Thing thing = new Thing(fooCache, barCache, bazCache);
        cleanup();
        return thing;
    }
}

这可行,但是:

我不喜欢我的解决方案的地方

  • 感觉很复杂!
  • 公共(public)缓存 setter 。仅TokenParser应该允许 s 填充构建器缓存。
  • 如果我有多个包含 int 的列怎么办?的? 我是否必须为每列构建一个解析器类,或者是否可以多次使用 IntegerParser 类?这里的问题是,解析器必须调用正确的缓存设置方法。

预先感谢您的提示!

最佳答案

就我个人而言,我不会走这条路。有一些通用框架可以很好地完成此类事情,并允许可定制的扩展点。

例如,考虑流行的 OpenCSV项目:

http://opencsv.sourceforge.net/#javabean-integration

或者,如果您想要注释,请考虑 JFileHelpers

http://jfilehelpers.com/index_en.php

或者JSefa

http://jsefa.sourceforge.net/quick-tutorial.html#CSV

关于java - 用 Java 设计智能自定义类 csv 解析器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12878017/

相关文章:

java - 卸载Android Studio,但保留android SDK

mysql - 如何获得真实日期而不是实际日期

Python 正则表达式。删除 ':' 之后的所有字符(包括行尾和特定字符串除外)

mysql - 由于执行前错误 0xc020802e,我无法将 CSV 导入 Microsoft SQL Server Management Studio 2014

java - 是否有类似 Apache Commons 之类的 Java 库的项目?

java - Infinispan 中的分组 api

java - 如何从jess中的java类中读取变量?

python - 用 python 函数包装 html

bash - 检查文件是否有给定的列数

regex - 使用正则表达式从 CSV 中删除多余的引号字符