F#合并具有不同列的CSV文件

时间:2018-08-29 01:45:32

标签: csv f#

我对F#相当陌生,但是我对此很着迷,并希望将其应用于某些应用程序。目前,我有多个csv文件,这些文件只是时间戳和一些传感器的值,时间戳是唯一的,但列的值是不同的。 例如我有两个csv文件

csv1:

timestamp, sensor1
time1, 1.0

csv2:

timestamp, sensor1, sensor2
time2, 2.0, 3.0

我想要的结果是

timestamp, sensor1, sensor2
time1, 1.0, 
time2, 2.0, 3.0

我想知道是否有任何简单的方法可以在F#中做到这一点。谢谢

更新1:
在这里,我当前的解决方案涉及使用 LumenWorks.Framework.IO.Csv https://www.nuget.org/packages/LumenWorksCsvReader)将csv解析为Data.DataTable和 Deedle https://www.nuget.org/packages/Deedle )将Data.DataTable转换为Frame并使用SaveCsv方法保存到csv文件。

open System.IO
open System
open LumenWorks.Framework.IO.Csv
open Deedle

// get list of csv files
let filelist = expression_to_get_list_of_csv_file_path

// func to readCsv from path and return Data.DataTable
let funcReadCSVtoDataTable (path:string) = 
    use csv = new CachedCsvReader(new StreamReader(path), true)
    let tmpdata = new Data.DataTable()
    tmpdata.Load(csv)
    tmpdata        

// map list of file paths to get list of datatable
let allTables = List.map funcReadCSVtoDataTable filelist

// create allData table to iterate over the list
let allData = new Data.DataTable()
List.iter (fun (x:Data.DataTable) -> allData.Merge(x)) allTables

//convert datatable to Deedle Frame and save to csv file
let df = Frame.ReadReader (allData.CreateDataReader())   
df.SaveCsv("./final_csv.csv")

使用 LumenWorks.Framework.IO.Csv 的原因是因为我需要同时解析数千个文件,并且根据本文(https://www.codeproject.com/Articles/11698/A-Portable-and-Efficient-Generic-Parser-for-Flat-F)< em> LumenWorks.Framework.IO.Csv 是最快的。

更新2:最终解决方案 感谢Tomas关于RowsKey映射解决方案的帮助(请参阅下面的评论),对于文件列表,我重新扭曲了他的代码

// get list of csv files
let filelist = expression_to_get_list_of_csv_file_path

// function to merge two Frames
let domerge (df0:Frame<int,string>) (df1:Frame<int,string>) = 
    df1 
    |> Frame.mapRowKeys (fun k-> k+df0.Rows.KeyCount)
    |> Frame.merge df0

// read filelist to Frame list 
let dflist = filelist |> List.map (fun (x:string)-> Frame.ReadCsv x)

// using List.fold to "fold" through the list with dflist.[0] is the intial state
let dffinal = List.tail dflist |> List.fold domerge (List.head dflist)
dffinal.SaveCsv("./final_csv.csv")

现在代码看起来“可以正常工作”,但是,我得到了一个关于Frame.ReadCsv的小警告,该方法并不适用于F#,但仍然可以使用。

2 个答案:

答案 0 :(得分:2)

如果您愿意使用外部库,则可以使用data frame manipulation library called Deedle非常轻松地完成此操作。 Deedle可让您从CSV文件中读取数据帧,并在合并数据帧时确保为您对齐列键和行键:

open Deedle

let f1 = Frame.ReadCsv("c:/temp/f1.csv")
let f2 = Frame.ReadCsv("c:/temp/f2.csv")

let merged = 
  f2 
  |> Frame.mapRowKeys (fun k -> k + f1.Rows.KeyCount)
  |> Frame.merge f1

merged.SaveCsv("c:/temp/merged.csv")

我们在这里要做的一件棘手的事情是使用mapRowKeys。当您阅读框架时,Deedle会自动为您的数据生成序数行键,因此合并将失败,因为您有两行带有键0。通过mapRowKeys函数,我们可以变换键,以便它们是唯一的,并且可以合并帧。 (保存CSV文件不会自动将行键写入输出,因此其结果正是您想要的。)

答案 1 :(得分:1)

如果您要进行大量此类处理,则应查看CSV TypeProviderParser或我最喜欢的FileHelpers

如果您不想使用任何第三方库,以下是一个快速的分步过程,以读取,重新组装和写出文件:

open System.IO
open System

let csv1path = @"E:\tmp\csv1.csv"
let csv2path = @"E:\tmp\csv2.csv"

/// Read the file, split it up, and remove the header from the first csv file 
let csv1 = 
    File.ReadAllLines(csv1path)
    |> Array.map (fun x -> x.Split(','))
    |> Array.tail

let csv2 = 
    File.ReadAllLines(csv2path)
    |> Array.map (fun x -> x.Split(','))

///Split the header and data in the second csv file
let header', data = (csv2.[0], Array.tail csv2)
let header = String.Join(",", header')

///put back the data together, this is an array of arrays
let csv3 = 
    Array.append(csv1) data

///Sort the combined file, put it back together as a csv and add back the header   
let csv4 =
    csv3 
    |> Array.sort 
    |> Array.map (fun x -> String.Join(",", x))
    |> Array.append [|header|]

///Write it out
File.WriteAllLines(@"E:\tmp\combined.csv",csv4)