将两个CSV文件的交集与Scala合并

时间:2011-07-04 21:55:16

标签: scala csv

从输入1:

fruit, apple, cider  
animal, beef, burger

并输入2:

animal, beef, 5kg
fruit, apple, 2liter
fish, tuna, 1kg

我需要制作:

fruit, apple, cider, 2liter
animal, beef, burger, 5kg

我能得到的最接近的例子是:

object FileMerger {
def main(args : Array[String]) {
  import scala.io._
  val f1 = (Source fromFile "file1.csv" getLines) map (_.split(", *")(1))
  val f2 = Source fromFile "file2.csv" getLines
  val out = new java.io.FileWriter("output.csv")
  f1 zip f2 foreach { x => out.write(x._1 + ", " + x._2 + "\n") }
  out.close
  }
}

问题是该示例假定两个CSV文件包含相同数量的元素并且顺序相同。我的合并结果必须只包含第一个和第二个文件中的元素。我是Scala的新手,非常感谢任何帮助。

2 个答案:

答案 0 :(得分:9)

您需要intersection这两个文件:来自file1和file2的行共享一些条件。从集合理论的角度考虑这个问题:你有两个共同的元素集合,你需要一个带有这些元素的新集合。嗯,还有更多的东西,因为线条不是真的相等......

所以,假设你读了file1,那就是List[Input1]类型。我们可以像这样编码,而不需要了解Input1的任何细节:

case class Input1(line: String)
val f1: List[Input1] = (Source fromFile "file1.csv" getLines () map Input1).toList

我们可以对file2和List[Input2]执行相同的操作:

case class Input2(line: String)
val f2: List[Input2] = (Source fromFile "file2.csv" getLines () map Input2).toList

您可能想知道为什么我创建了两个不同的类,如果它们具有完全相同的定义。好吧,如果您正在阅读结构化数据,那么有两种不同的类型,所以让我们看看如何处理更复杂的情况。

好的,我们如何匹配它们,因为Input1Input2是不同的类型?嗯,这些行由键匹配,根据您的代码,键是每个键中的第一列。因此,让我们创建一个类Key,并转换Input1 => KeyInput2 => Key

case class Key(key: String)
def Input1IsKey(input: Input1): Key = Key(input.line split "," head) // using regex would be better
def Input2IsKey(input: Input2): Key = Key(input.line split "," head)

好的,现在我们可以从KeyInput1生成一个共同的Input2,让我们得到它们的交集:

val intersection = (f1 map Input1IsKey).toSet intersect (f2 map Input2IsKey).toSet

所以我们可以建立我们想要的线路交叉点,但我们没有线路!问题是,对于每个密钥,我们需要知道它来自哪条线。考虑一下我们有一组密钥,对于每个密钥,我们要跟踪一个值 - 这正是Map的内容!所以我们可以建立这个:

val m1 = (f1 map (input => Input1IsKey(input) -> input)).toMap
val m2 = (f2 map (input => Input2IsKey(input) -> input)).toMap

因此输出可以像这样产生:

val output = intersection map (key => m1(key).line + ", " + m2(key).line)

现在你所要做的只是输出。

让我们考虑一下这段代码的一些改进。首先,请注意上面生成的输出重复了密钥 - 这正是您的代码所做的,但不是您在示例中所需的内容。然后,让我们更改Input1Input2以将密钥与其他args分开:

case class Input1(key: String, rest: String)
case class Input2(key: String, rest: String)

现在初始化f1和f2有点困难。我们不会使用split来不必要地破坏所有线路(并且性能成本很高),而是将第一个逗号分隔为正确的行:前面的所有内容都是关键,之后的所有内容都是休息。方法span执行此操作:

def breakLine(line: String): (String, String) = line span (',' !=)

在REPL上使用span方法稍微玩一下,以便更好地理解它。至于(',' !=),这只是说(x => ',' != x)的缩写形式。

接下来,我们需要一种从元组创建Input1Input2的方法(breakLine的结果):

def TupleIsInput1(tuple: (String, String)) = Input1(tuple._1, tuple._2)
def TupleIsInput2(tuple: (String, String)) = Input2(tuple._1, tuple._2)

我们现在可以阅读文件:

val f1: List[Input1] = (Source fromFile "file1.csv" getLines () map breakLine map TupleIsInput1).toList
val f2: List[Input2] = (Source fromFile "file2.csv" getLines () map breakLine map TupleIsInput2).toList

我们可以简化的另一件事是交叉。当我们创建Map时,其键集,因此我们可以先创建地图,然后使用它们的键来计算交集:

case class Key(key: String)
def Input1IsKey(input: Input1): Key = Key(input.key)
def Input2IsKey(input: Input2): Key = Key(input.key)

// We now only keep the "rest" as the map value
val m1 = (f1 map (input => Input1IsKey(input) -> input.rest)).toMap
val m2 = (f2 map (input => Input2IsKey(input) -> input.rest)).toMap

val intersection = m1.keySet intersect m2.keySet

输出的计算如下:

val output = intersection map (key => key + m1(key) + m2(key))

请注意,我不再附加逗号 - f1和f2的其余部分都以逗号开头。

答案 1 :(得分:1)

很难从一个例子中推断出一个要求。可能是这样的东西可以满足您的需求:

  • 为第二个文件f2(从"animal, beef" -> "5kg"
  • 创建一个从一个到另一个行的映射
  • 对于第一个文件f1中的每一行,获取要在地图中查找的键
  • 查找值,如果找到写入输出

转换为

val f1 = Source fromFile "file1.csv" getLines
val f2 = Source fromFile "file2.csv" getLines
val map = f2.map(_.split(", *")).map(arr => arr.init.mkString(", ") -> arr.last}.toMap
for {
  line <- f1
  key = line.split(", *").init.mkString(", ")
  value <- map.get(key)
} {
  out.write(line + ", " + value + "\n")
}