2011-07-04 54 views
2

從輸入1:合併兩個CSV文件的交叉點使用Scala

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文件包含相同數量的元素並且順序相同。我的合併結果只能包含第一個和第二個文件中的元素。我是斯卡拉新手,任何幫助將不勝感激。

回答

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 

我們可以做同樣的事,文件2和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)的縮寫形式。

接下來,我們需要一種方法來創建一個元組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都以逗號開頭。

+0

工程就像一個魅力,謝謝丹尼爾! – Jack

+0

丹尼爾,我今天剛剛讀完這個答案,不得不說:「哇!它完美地解釋了所有事情「我知道這是兩年前的事情,但非常感謝您提供了這個偉大的答案,當時我真的幫助我理解了關於Scala的一大堆事情。 – Jack

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") 
} 
+0

非常非常非常感謝!這是完美的! – Jack