【问题标题】:Merge the intersection of two CSV files with Scala使用 Scala 合并两个 CSV 文件的交集
【发布时间】:2025-12-03 03:15:02
【问题描述】:

从输入 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 的新手,任何帮助都将不胜感激。

【问题讨论】:

    标签: scala csv


    【解决方案1】:

    您需要两个文件的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)
    

    好的,现在我们可以从Input1Input2 生成一个普通的Key,让我们得到它们的交集:

    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)的简写形式。

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

    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 的其余部分已经以逗号开头。

    【讨论】:

    • Daniel,我今天刚刚再次阅读了这个答案,不得不说:“哇!它完美地解释了一切”我知道这是两年前的事了,但是非常感谢你的这个好答案,它当时真的帮助我理解了很多关于 Scala 的东西。
    【解决方案2】:

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

    • 为第二个文件 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")
    }
    

    【讨论】:

    • 非常非常非常感谢!太完美了!