使用spark-csv编写单个CSV文件

时间:2015-07-28 11:08:20

标签: scala csv apache-spark spark-csv

我正在使用https://github.com/databricks/spark-csv,我正在尝试编写单个CSV,但无法创建文件夹。

需要一个Scala函数,它将获取路径和文件名等参数并写入该CSV文件。

14 个答案:

答案 0 :(得分:132)

它正在创建一个包含多个文件的文件夹,因为每个分区都是单独保存的。如果您需要单个输出文件(仍然在文件夹中),您可以repartition(如果上游数据很大,但需要随机播放,则首选):

df
   .repartition(1)
   .write.format("com.databricks.spark.csv")
   .option("header", "true")
   .save("mydata.csv")

coalesce

df
   .coalesce(1)
   .write.format("com.databricks.spark.csv")
   .option("header", "true")
   .save("mydata.csv")
保存前的

数据框:

所有数据都将写入mydata.csv/part-00000。在使用此选项之前请确保您了解正在进行的操作以及将所有数据传输到单个工作人员的成本。如果使用带复制的分布式文件系统,数据将被多次传输 - 首先获取到单个工作线,然后分布在存储节点上。

或者,您可以保留代码,并使用catHDFS getmerge等通用工具简单地合并所有部分。

答案 1 :(得分:31)

如果您使用HDFS运行Spark,我一直在通过正常编写csv文件并利用HDFS进行合并来解决问题。我直接在Spark(1.6)中这样做:

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs._

def merge(srcPath: String, dstPath: String): Unit =  {
   val hadoopConfig = new Configuration()
   val hdfs = FileSystem.get(hadoopConfig)
   FileUtil.copyMerge(hdfs, new Path(srcPath), hdfs, new Path(dstPath), true, hadoopConfig, null) 
   // the "true" setting deletes the source files once they are merged into the new output
}


val newData = << create your dataframe >>


val outputfile = "/user/feeds/project/outputs/subject"  
var filename = "myinsights"
var outputFileName = outputfile + "/temp_" + filename 
var mergedFileName = outputfile + "/merged_" + filename
var mergeFindGlob  = outputFileName

    newData.write
        .format("com.databricks.spark.csv")
        .option("header", "false")
        .mode("overwrite")
        .save(outputFileName)
    merge(mergeFindGlob, mergedFileName )
    newData.unpersist()

不记得我在哪里学到了这个技巧,但它可能适合你。

答案 2 :(得分:22)

我这里的游戏可能有点晚了,但是使用coalesce(1)repartition(1)可能适用于小型数据集,但是大型数据集都会被投入到一个节点上的一个分区中。这很可能会导致OOM错误,或者说最好是处理缓慢。

我强烈建议您使用Hadoop API中的FileUtil.copyMerge()函数。这会将输出合并为一个文件。

编辑 - 这有效地将数据带到驱动程序而不是执行程序节点。如果单个执行程序比驱动程序使用更多RAM,则Coalesce()会没问题。

编辑2:在Hadoop 3.0中删除了copyMerge()。有关如何使用最新版本的更多信息,请参阅以下堆栈溢出文章:Hadoop how to do CopyMerge in Hadoop 3.0

答案 3 :(得分:13)

如果您正在使用Databricks并且可以将所有数据放入一个工作线程的RAM中(因此可以使用.coalesce(1)),则可以使用dbfs查找并移动生成的CSV文件:

val fileprefix= "/mnt/aws/path/file-prefix"

dataset
  .coalesce(1)       
  .write             
//.mode("overwrite") // I usually don't use this, but you may want to.
  .option("header", "true")
  .option("delimiter","\t")
  .csv(fileprefix+".tmp")

val partition_path = dbutils.fs.ls(fileprefix+".tmp/")
     .filter(file=>file.name.endsWith(".csv"))(0).path

dbutils.fs.cp(partition_path,fileprefix+".tab")

dbutils.fs.rm(fileprefix+".tmp",recurse=true)

如果您的文件不适合工作人员的RAM,您可能需要考虑chaotic3quilibrium's suggestion to use FileUtils.copyMerge()。我还没有这样做,并且还不知道是否可能,例如,在S3上。

这个答案建立在此问题的先前答案以及我自己对提供的代码段的测试之上。 I originally posted it to Databricks并在此重新发布。

我找到的dbfs rm递归选项的最佳文档位于a Databricks forum

答案 4 :(得分:2)

在保存之前重新分区/合并到1个分区(你仍然会得到一个文件夹,但它会有一个部分文件)

答案 5 :(得分:2)

您可以使用rdd.coalesce(1, true).saveAsTextFile(path)

它会将数据作为单一文件存储在path / part-00000

答案 6 :(得分:2)

一种适用于Minkymorgan修改的S3的解决方案。

只需将临时分区目录路径(名称与最终路径不同)传递为srcPath,将单个最终csv / txt传递为destPath,如果要删除该目录,还请指定deleteSource。原始目录。

/**
* Merges multiple partitions of spark text file output into single file. 
* @param srcPath source directory of partitioned files
* @param dstPath output path of individual path
* @param deleteSource whether or not to delete source directory after merging
* @param spark sparkSession
*/
def mergeTextFiles(srcPath: String, dstPath: String, deleteSource: Boolean): Unit =  {
  import org.apache.hadoop.fs.FileUtil
  import java.net.URI
  val config = spark.sparkContext.hadoopConfiguration
  val fs: FileSystem = FileSystem.get(new URI(srcPath), config)
  FileUtil.copyMerge(
    fs, new Path(srcPath), fs, new Path(dstPath), deleteSource, config, null
  )
}

答案 7 :(得分:2)

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs._
import org.apache.spark.sql.{DataFrame,SaveMode,SparkSession}
import org.apache.spark.sql.functions._

我使用以下方法(HDFS重命名文件名)解决了:-

步骤1 :-(创建数据帧并写入HDFS)

df.coalesce(1).write.format("csv").option("header", "false").mode(SaveMode.Overwrite).save("/hdfsfolder/blah/")

步骤2 :-(创建Hadoop Config)

val hadoopConfig = new Configuration()
val hdfs = FileSystem.get(hadoopConfig)

Step3 :-(在hdfs文件夹路径中获取路径)

val pathFiles = new Path("/hdfsfolder/blah/")

Step4 :-(从hdfs文件夹获取spark文件名)

val fileNames = hdfs.listFiles(pathFiles, false)
println(fileNames)

setp5 :-(创建scala可变列表以保存所有文件名并将其添加到列表中)

    var fileNamesList = scala.collection.mutable.MutableList[String]()
    while (fileNames.hasNext) {
      fileNamesList += fileNames.next().getPath.getName
    }
    println(fileNamesList)

步骤6 :-(从文件名scala列表中过滤_SUCESS文件顺序)

    // get files name which are not _SUCCESS
    val partFileName = fileNamesList.filterNot(filenames => filenames == "_SUCCESS")

步骤7 :-(将scala列表转换为字符串,并将所需的文件名添加到hdfs文件夹字符串,然后应用重命名)

val partFileSourcePath = new Path("/yourhdfsfolder/"+ partFileName.mkString(""))
    val desiredCsvTargetPath = new Path(/yourhdfsfolder/+ "op_"+ ".csv")
    hdfs.rename(partFileSourcePath , desiredCsvTargetPath)

答案 8 :(得分:2)

我正在Python中使用它来获取一个文件:

df.toPandas().to_csv("/tmp/my.csv", sep=',', header=True, index=False)

答案 9 :(得分:2)

此答案扩展了接受的答案,提供了更多上下文,并提供了可在计算机上的Spark Shell中运行的代码段。

有关已接受答案的更多上下文

接受的答案可能会给您印象,示例代码将输出单个mydata.csv文件,事实并非如此。让我们演示一下:

val df = Seq("one", "two", "three").toDF("num")
df
  .repartition(1)
  .write.csv(sys.env("HOME")+ "/Documents/tmp/mydata.csv")

这是输出的内容:

Documents/
  tmp/
    mydata.csv/
      _SUCCESS
      part-00000-b3700504-e58b-4552-880b-e7b52c60157e-c000.csv

mydata.csv是已接受答案中的文件夹-它不是文件!

如何输出具有特定名称的单个文件

我们可以使用spark-daria来写出一个mydata.csv文件。

import com.github.mrpowers.spark.daria.sql.DariaWriters
DariaWriters.writeSingleFile(
    df = df,
    format = "csv",
    sc = spark.sparkContext,
    tmpFolder = sys.env("HOME") + "/Documents/better/staging",
    filename = sys.env("HOME") + "/Documents/better/mydata.csv"
)

这将输出文件,如下所示:

Documents/
  better/
    mydata.csv

S3路径

您需要将s3a路径传递到DariaWriters.writeSingleFile才能在S3中使用此方法:

DariaWriters.writeSingleFile(
    df = df,
    format = "csv",
    sc = spark.sparkContext,
    tmpFolder = "s3a://bucket/data/src",
    filename = "s3a://bucket/data/dest/my_cool_file.csv"
)

有关更多信息,请参见here

避免copyMerge

copyMerge已从Hadoop 3中删除。DariaWriters.writeSingleFile实现使用fs.renameas described hereSpark 3 still used Hadoop 2,因此copyMerge实现将在2020年工作。我不确定Spark何时升级到Hadoop 3,但最好避免使用任何copyMerge方法,该方法会在Spark升级Hadoop时导致代码中断。

源代码

如果要检查实现,请在spark-daria源代码中查找DariaWriters对象。

PySpark实施

使用PySpark写入单个文件更容易,因为您可以将DataFrame转换为默认情况下作为单个文件写入的Pandas DataFrame。

from pathlib import Path
home = str(Path.home())
data = [
    ("jellyfish", "JALYF"),
    ("li", "L"),
    ("luisa", "LAS"),
    (None, None)
]
df = spark.createDataFrame(data, ["word", "expected"])
df.toPandas().to_csv(home + "/Documents/tmp/mydata-from-pyspark.csv", sep=',', header=True, index=False)

限制

DariaWriters.writeSingleFile Scala方法和df.toPandas() Python方法仅适用于小型数据集。庞大的数据集不能作为单个文件写出。从性能的角度来看,将数据作为单个文件写出并不是最佳选择,因为不能并行写入数据。

答案 10 :(得分:0)

spark的df.write() API将在给定路径内创建多个零件文件...强制火花仅使用df.coalesce(1).write.csv(...)而非df.repartition(1).write.csv(...)写入单个零件文件,因为合并是一个狭窄的转换,而重新分区是一个广泛的转变,请参见Spark - repartition() vs coalesce()

df.coalesce(1).write.csv(filepath,header=True) 

将使用一个part-0001-...-c000.csv文件在给定的文件路径中创建文件夹 使用

cat filepath/part-0001-...-c000.csv > filename_you_want.csv 

具有用户友好的文件名

答案 11 :(得分:0)

通过使用Listbuffer,我们可以将数据保存到单个文件中:

import java.io.FileWriter
import org.apache.spark.sql.SparkSession
import scala.collection.mutable.ListBuffer
    val text = spark.read.textFile("filepath")
    var data = ListBuffer[String]()
    for(line:String <- text.collect()){
      data += line
    }
    val writer = new FileWriter("filepath")
    data.foreach(line => writer.write(line.toString+"\n"))
    writer.close()

答案 12 :(得分:0)

spark.sql("select * from df").coalesce(1).write.option("mode","append").option("header","true").csv("/your/hdfs/path/")

spark.sql("select * from df") --> 这是数据框

coalesce(1) 或 repartition(1) --> 这将使您的输出文件仅成为 1 个部分文件

写入 --> 写入数据

option("mode","append") --> 将数据追加到现有目录

option("header","true") --> 启用标题

csv("") --> 写入 CSV 文件及其在 HDFS 中的输出位置

答案 13 :(得分:-2)

还有一种方法可以使用Java

import java.io._

def printToFile(f: java.io.File)(op: java.io.PrintWriter => Unit) 
  {
     val p = new java.io.PrintWriter(f);  
     try { op(p) } 
     finally { p.close() }
  } 

printToFile(new File("C:/TEMP/df.csv")) { p => df.collect().foreach(p.println)}