在Java中设计一个类似智能自定义csv的解析器

时间:2012-10-13 23:39:21

标签: 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. 阅读所有其他行并使用它们执行以下操作:
    • 将标记分割为标记
    • 循环遍历它们:
      • for token i在列名switch上执行大else if / i
      • 转换令牌i
      • 将提取的值存储在某处
    • 收集所有内容并构建Thing
  3. 进行。
  4. 这种方法的问题是内部开关。在处理完第一行之后,应该清楚如何解析行。

    我想要什么

    在有闭包的语言中,我会尝试以下方法:

    1. 读取第一行并将其拆分为列名
    2. 为每个列名创建一个闭包,为给定标记设置正确的值并将其添加到解析器闭包数组
    3. 阅读所有其他行并使用它们执行以下操作:
      • 将标记分割为标记
      • 循环遍历它们:
        • 使用令牌i调用解析器关闭i
      • 收集所有内容并构建Thing
    4. 进行。
    5. 我尝试了什么

      我为所有三个令牌解析器提供了一个简单的界面。他们应该获得一个令牌并在给定的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);
          }
      }
      

      我的ThingBuilder buildThings方法是静态的,并在内部创建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;
          }
      }
      

      这有效,但是:

      我不喜欢我的解决方案

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

      提前感谢您的提示!

2 个答案:

答案 0 :(得分:2)

就个人而言,我不会走这条路。有一般框架可以很好地完成这类工作,并允许可自定义的扩展点。

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

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

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

http://jfilehelpers.com/index_en.php

JSefa

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

答案 1 :(得分:1)

我同意@ btiernay的回答,但是如果你想推出自己的实现,请继续阅读......


  

公共缓存设置器的事情。只应允许TokenParsers填充构建器缓存。

是的。这是TokenParser API的结果,以及通过调用ThingBuilder上的setter“返回”值的方式。事实上,这比你所确定的更糟糕。也就是说:您的TokenParser API和所有TokenParser类都特定于一个ThingBuilder类。它们不可重复使用......

我认为使用这样的API会更好:

  public interface TokenParser<T> {
      public T parse(String token);
  }
  

如果我有多个带有int的列,该怎么办?我是否必须为每个列构建一个解析器类,或者是否可以使用一次IntegerParser类?这里的问题是,解析器必须调用正确的缓存设置器方法。

烨。

接下来创建一个RowBuilder接口:

  public interface RowBuilder<R>
      public R buildRow(List<String> tokens);
  }

这是一个棘手的问题---创建一个看起来像这样的通用RowBuilder类:

  public class GenericRowBuilder<R> implements RowBuilder<R> {
      public GenericRowBuilder(Class<R> clazz, TokenParser<?>[] parsers) {
          // Extract the return types of the reified parse objects' `parse` 
          // methods, and use this to locate a matching `Constructor<R>` in 
          // `clazz`.  If there isn't one, throw an exception.
          this.clazz = clazz;
          this.parsers = parsers;
      }
      public R parse(List<String> tokens) {
          // Check number of tokens matches number of parsers.
          // Parse each token with corresponding parsers.
          // Use the `Constructor<R>` found above to create the instance of `R`
      }
  }

现在这一切都非常复杂......并且需要很好地理解你使用Java的反射API ......但最终结果是你可以为你的类实例化一个RowBuilder:

  RowBuilder<MyRow> rb = new GenericRowBuilder<MyRow>(MyRow.class,
      new TokenParser<?>[]{
          new IntTokenParser(), new FloatTokenParser(), new CustomTokenParser});

你有一些东西:

  • 检查正确的类型是否用于正确的字段
  • 检查每行中的值的数量,并为每个列值使用正确的解析器,
  • 使用任何行类R,前提是它具有合适的构造函数。