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


Answers:


168

它正在创建一个包含多个文件的文件夹,因为每个分区都是单独保存的。如果您只需要一个输出文件(仍在文件夹中),则可以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。使用此选项之前,请确保您了解发生了什么以及将所有数据传输到单个工作程序的成本是多少。如果将分布式文件系统用于复制,则数据将被多次传输-首先被提取到单个工作服务器,然后再分布在存储节点上。

另外,您可以保留您的代码,并使用通用工具(例如HDFS)cat或以后简单地合并所有部分。getmerge


6
您也可以使用合并:df.coalesce(1).write.format(“ com.databricks.spark.csv”).option(“ header”,“ true”).save(“ mydata.csv”)
ravi

当我们设置.coalesce(1)它在_temporary目录上显示某些FileNotFoundException 时,spark 1.6会引发错误。它仍然是火花中的错误:issue.apache.org/jira/browse/SPARK-2984
Harsha

@哈莎不太可能。而是coalesce(1)非常昂贵且通常不实用的简单结果。
zero323 '16

同意@ zero323,但是如果您有特殊要求要合并到一个文件中,则鉴于您有足够的资源和时间,仍然应该可以。
哈沙

2
@哈莎我不说没有。如果您正确调整GC,它应该可以正常工作,但这只是浪费时间,很可能会损害整体性能。因此,就我个人而言,我认为没有任何理由要打扰,特别是因为在Spark外部合并文件非常简单,而根本无需担心内存使用情况。
zero323 '16

36

如果您将Spark与HDFS一起运行,则可以通过正常编写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()

不记得我从哪里学到了这个技巧,但是它可能对您有用。


我没有尝试过-怀疑它可能不是直接的。
Minkymorgan

1
谢谢。我添加了一个适用于Databricks 的答案
Josiah Yoder

@Minkymorgan我有类似的问题,但无法正确完成..您能看看这个问题

4
@SUDARSHAN我上面的功能适用于未压缩的数据。在您的示例中,我认为您在编写文件时使用gzip压缩,然后在尝试将这些文件合并在一起时失败。那是行不通的,因为您无法将gzip文件合并在一起。Gzip不是可拆分压缩算法,因此肯定不是“可合并的”。您可能会测试“ snappy”或“ bz2”压缩-但直觉是这在合并时也会失败。可能最好的办法是删除压缩,合并原始文件,然后使用可拆分编解码器进行压缩。
Minkymorgan '17

如果我想保留标题,该怎么办?它为每个文件部分重复
正常的

32

我可能在这里玩游戏有些迟了,但是使用coalesce(1)repartition(1)可能适用于小型数据集,但大型数据集将全部扔入一个节点上的一个分区中。这很可能会引发OOM错误,或者充其量只能使其缓慢处理。

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

编辑 -这有效地将数据带给驱动程序而不是执行者节点。Coalesce()如果单个执行程序具有比驱动程序更多的RAM供使用,那就很好了。

编辑2copyMerge()已在Hadoop 3.0中删除。有关如何使用最新版本的更多信息,请参见以下堆栈溢出文章:如何在Hadoop 3.0中执行CopyMerge?


关于如何以这种方式获取带有标题行的csv的任何想法?不想让文件产生一个标头,因为那会散布整个文件中的标头,每个分区一个。
nojo

我曾经在这里记录过一个选项:markhneedham.com/blog/2014/11/30/…–
etspaceman

@etspaceman酷。不幸的是,我仍然没有很好的方法来执行此操作,因为我需要能够在Java(或Spark)中执行此操作,但这种方法不会消耗大量内存并且可以处理大文件。 。我仍然不敢相信他们删除了此API调用...这是非常普遍的用法,即使Hadoop生态系统中的其他应用程序未完全使用它。
活泉

20

如果您正在使用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的建议使用FileUtils.copyMerge()。我尚未执行此操作,并且尚不知道是否可行,例如在S3上。

该答案建立在该问题的先前答案以及我自己对提供的代码段的测试之上。我最初将其发布到Databricks,并在此重新发布。

我发现的有关dbfs rm rm递归选项的最佳文档在Databricks论坛上


3

适用于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
  )
}

copyMerge实现列出所有文件并对其进行迭代,这在s3中并不安全。如果您写了文件然后列出了它们-这并不保证所有文件都会被列出。看到[this | docs.aws.amazon.com/AmazonS3/latest/dev/...
LiranBo

3

spark的df.write()API将在给定的路径内创建多个零件文件...强制spark仅使用单个零件文件,df.coalesce(1).write.csv(...)而不是df.repartition(1).write.csv(...)因为coacece是一个狭窄的转换,而repartition是一个广泛的转换,请参见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 

具有用户友好的文件名


或者,如果数据帧不太大(〜GB或可以容纳在驱动程序内存中),您也可以使用df.toPandas().to_csv(path)此方法以首选文件名写入单个csv
pprasad009

1
gh,如此令人沮丧,只能通过转换为熊猫来完成。只写没有UUID的文件有多困难?
ijoseph

2

重新分区/合并到1个分区,然后再保存(您仍然会得到一个文件夹,但是其中会有一个零件文件)


2

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

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


1
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)

步骤3:-(在hdfs文件夹路径中获取路径)

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

步骤4:-(从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)


1

该答案扩展了已接受的答案,提供了更多上下文,并提供了可在计算机上的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

NB 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"
)

有关更多信息,请参见此处

避免copyMerge

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

源代码

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

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斯卡拉方法和df.toPandas()Python的办法只能用于小数据集。庞大的数据集不能作为单个文件写出。从性能的角度来看,将数据作为单个文件写出并不是最佳选择,因为不能并行写入数据。


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()

-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)}

名称'true'未定义
Arron
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.