将Spark DataFrame的内容保存为单个CSV文件

时间:2017-01-31 21:19:45

标签: csv apache-spark pyspark

假设我有一个Spark DataFrame,我想将其保存为CSV文件。在 Spark 2.0.0 之后, DataFrameWriter 类直接支持将其另存为CSV文件。

默认行为是将输出保存在提供的路径中的多个 part - * .csv 文件中。

如何使用以下方式保存DF:

  1. 路径映射到确切的文件名而不是文件夹
  2. 第一行可用的标题
  3. 另存为单个文件而非多个文件。
  4. 处理它的一种方法是合并DF然后保存文件。

    df.coalesce(1).write.option("header", "true").csv("sample_file.csv")
    

    然而,在主机上收集它并且需要拥有足够内存的主机时,这是不利的。

    是否可以在不使用合并的情况下编写单个CSV文件?如果没有,是否有比上述代码更有效的方法?

8 个答案:

答案 0 :(得分:8)

我自己使用 pyspark 和dbutils解决了这个问题,以获取.csv并重命名为所需的文件名。

save_location= "s3a://landing-bucket-test/export/"+year
csv_location = save_location+"temp.folder'
file_location = save_location+'export.csv'

df.repartition(1).write.csv(path=csv_location, mode="append", header="true")

file = dbutils.fs.ls(csv_location)[-1].path
dbutils.fs.cp(file, file_location)
dbutils.fs.rm(csv_location, recurse=True)

这个答案可以通过不使用[-1]来改进,但.csv似乎永远是文件夹中的最后一个。如果您只处理较小的文件并且可以使用重新分区(1)或合并(1),则可以使用简单快速的解决方案。

答案 1 :(得分:5)

答案 2 :(得分:1)

此解决方案基于Shell脚本,并未进行并行化,但仍然非常快,尤其是在SSD上。它在Unix系统上使用cat并输出重定向。假设包含分区的CSV目录位于/my/csv/dir,输出文件为/my/csv/output.csv

#!/bin/bash
echo "col1,col2,col3" > /my/csv/output.csv
for i in /my/csv/dir/*.csv ; do
    echo "Processing $i"
    cat $i >> /my/csv/output.csv
    rm $i
done
echo "Done"

在将每个分区附加到最终的CSV后,它将删除每个分区以释放空间。

"col1,col2,col3"是CSV标头(此处我们有三列名称col1col2col3)。你必须告诉Spark不要在每个分区中放置标题(这是用.option("header", "false")完成的,因为Shell脚本会这样做。

答案 3 :(得分:1)

对于那些仍然想要这样做的人,我是如何通过使用带有java.nio.file帮助的scala中的spark 2.1来完成它的。

基于https://fullstackml.com/how-to-export-data-frame-from-apache-spark-3215274ee9d6

    val df: org.apache.spark.sql.DataFrame = ??? // data frame to write
    val file: java.nio.file.Path = ??? // target output file (i.e. 'out.csv')

    import scala.collection.JavaConversions._

    // write csv into temp directory which contains the additional spark output files
    // could use Files.createTempDirectory instead
    val tempDir = file.getParent.resolve(file.getFileName + "_tmp")
    df.coalesce(1)
        .write.format("com.databricks.spark.csv")
        .option("header", "true")
        .save(tempDir.toAbsolutePath.toString)

    // find the actual csv file
    val tmpCsvFile = Files.walk(tempDir, 1).iterator().toSeq.find { p => 
        val fname = p.getFileName.toString
        fname.startsWith("part-00000") && fname.endsWith(".csv") && Files.isRegularFile(p)
    }.get

    // move to desired final path
    Files.move(tmpCsvFile, file)

    // delete temp directory
    Files.walk(tempDir)
        .sorted(java.util.Comparator.reverseOrder())
        .iterator().toSeq
        .foreach(Files.delete(_))

答案 4 :(得分:1)

以下 scala 方法在本地或客户端模式下工作,并将df写入所选名称的单个csv。它要求df适合内存,否则 collect()会爆炸。



import org.apache.hadoop.fs.{FileSystem, Path}

val SPARK_WRITE_LOCATION = some_directory
val SPARKSESSION = org.apache.spark.sql.SparkSession

def saveResults(results : DataFrame, filename: String) {
    var fs = FileSystem.get(this.SPARKSESSION.sparkContext.hadoopConfiguration)
    
    if (SPARKSESSION.conf.get("spark.master").toString.contains("local")) {
      fs = FileSystem.getLocal(new conf.Configuration())
    }
    
    val tempWritePath = new Path(SPARK_WRITE_LOCATION)
    
    if (fs.exists(tempWritePath)) {
    
      val x = fs.delete(new Path(SPARK_WRITE_LOCATION), true)
      assert(x)
    }
    
    if (results.count > 0) {
      val hadoopFilepath = new Path(SPARK_WRITE_LOCATION, filename)
      val writeStream = fs.create(hadoopFilepath, true)
      val bw = new BufferedWriter( new OutputStreamWriter( writeStream, "UTF-8" ) )
    
      val x = results.collect()
      for (row : Row <- x) {
        val rowString = row.mkString(start = "", sep = ",", end="\n")
        bw.write(rowString)
      }
    
      bw.close()
      writeStream.close()
    
      val resultsWritePath = new Path(WRITE_DIRECTORY, filename)
    
      if (fs.exists(resultsWritePath)) {
        fs.delete(resultsWritePath, true)
      }
      fs.copyToLocalFile(false, hadoopFilepath, resultsWritePath, true)
    } else {
      System.exit(-1)
    }
}
&#13;
&#13;
&#13;

答案 5 :(得分:0)

Hadoop API中的FileUtil.copyMerge()应该可以解决您的问题。

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
}

请参阅Write single CSV file using spark-csv

答案 6 :(得分:0)

这是分布式计算的工作原理!目录中的多个文件正是分布式计算的工作原理,这根本不是问题,因为所有软件都可以处理它。

您的问题应该是“如何下载由多个文件组成的CSV?” - &GT; SO中已经有很多解决方案。

另一种方法可能是使用Spark作为JDBC源(使用令人敬畏的Spark Thrift服务器),编写SQL查询并将结果转换为CSV。

  

为了防止驱动程序中的OOM(因为驱动程序将获得ALL   数据),使用增量收集   (spark.sql.thriftServer.incrementalCollect=true),更多信息   http://www.russellspitzer.com/2017/05/19/Spark-Sql-Thriftserver/

关于Spark“数据分区”概念的小概述:

INPUT (X PARTITIONs) -> COMPUTING (Y PARTITIONs) -> OUTPUT (Z PARTITIONs)

在“阶段”之间,数据可以在分区之间传输,这就是“随机播放”。你想要“Z”= 1,但Y&gt; 1,没有洗牌?这是不可能的。

答案 7 :(得分:0)

df.coalesce(1).write.option("inferSchema","true").csv("/newFolder",header = 
'true',dateFormat = "yyyy-MM-dd HH:mm:ss")