spark-csvを使用して単一のCSVファイルを書き込む


回答:


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ます。このオプションを使用する前に、何が起こっているのか、およびすべてのデータを1人のワーカーに転送する場合のコストを理解しておいてください。レプリケーションで分散ファイルシステムを使用する場合、データは複数回転送されます。最初に単一のワーカーにフェッチされ、その後ストレージノードに分散されます。

または、コードをそのままにして、catまたはHDFSgetmergeなどの汎用ツールを使用して、後ですべての部分をマージすることもできます。


6
あなたはまた、COALESCEを使用することができます。df.coalesce(1).write.format( "com.databricks.spark.csv").OPTION( "ヘッダ"、 "真").SAVE( "mydata.csv")
ラヴィ

設定.coalesce(1)すると、spark 1.6がエラーをスローします。_temporaryディレクトリにいくつかのFileNotFoundExceptionが表示されます。これはまだSparkの
Harsha

@ Harshaありそうもない。むしろ、coalesce(1)非常に高価であり、通常は実用的ではないという単純な結果です。
zero323

@ zero323で合意しましたが、1つのファイルに統合する特別な要件がある場合でも、十分なリソースと時間があることを考えると、可能です。
Harsha 2016

2
@ハルシャはないと言っていません。GCを正しく調整すれば、問題なく機能するはずですが、それは単に時間の浪費であり、全体的なパフォーマンスを低下させる可能性があります。だから個人的には、特にメモリ使用量を気にせずにSparkの外でファイルをマージするのは簡単なことなので、気にする理由はないと思います。
zero323

36

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

どこでこのトリックを学んだか思い出せませんが、あなたのために役立つかもしれません。


私はそれを試していません-そしてそれは簡単ではないかもしれないと疑っています。
Minkymorgan 2017

1
ありがとう。私がきた答えを追加したことDatabricksの作品
ジョサイアYoderの

私はあなたがこの質問をご覧ください..Canそれを正しく行うことができ、同様の問題ではなく、持っている@Minkymorgan stackoverflow.com/questions/46812388/...
SUDARSHAN

4
@SUDARSHAN上記の私の関数は、非圧縮データで機能します。あなたの例では、ファイルを書き込むときにgzip圧縮を使用していると思います-その後、これらをマージしようとすると失敗します。gzipファイルをマージできないため、これは機能しません。Gzipは分割可能圧縮アルゴリズムではないため、「マージ可能」ではありません。"snappy"または "bz2"圧縮をテストすることもできますが、マージすると失敗します。おそらく最善の方法は、圧縮を削除し、生ファイルをマージしてから、分割可能なコーデックを使用して圧縮することです。
Minkymorgan 2017年

ヘッダーを保持したい場合はどうなりますか?ファイルパーツごとに複製
通常の

32

私はここでのゲームに少し遅れるかもしれませんが、小さなデータセットを使用しcoalesce(1)たりrepartition(1)機能したりする可能性がありますが、大きなデータセットはすべて1つのノードの1つのパーティションにスローされます。これは、OOMエラーをスローするか、遅くとも処理が遅くなる可能性があります。

FileUtil.copyMerge()Hadoop APIの関数を使用することを強くお勧めします。これにより、出力が1つのファイルにマージされます。

編集 -これは事実上、エグゼキューターノードではなくドライバーにデータをもたらします。Coalesce()単一のエグゼキューターがドライバーよりも多くのRAMを使用する場合は問題ありません。

編集2copyMerge()Hadoop 3.0で削除されます。最新バージョンでの作業方法の詳細については、次のスタックオーバーフローの記事を参照してください。Hadoop 3.0でCopyMergeを行う方法は?


このようにヘッダー行を持つCSVを取得する方法についての考えはありますか?ファイルにヘッダーを作成させたくありません。ヘッダーが各パーティションに1つずつ、ファイル全体に散在するからです。
nojo 2017年

ここに文書化され、私が過去に使用したというオプションがあります:markhneedham.com/blog/2014/11/30/...
etspaceman

@etspacemanかっこいい。残念ながら、Java(またはSparkでこれを実行できるようにする必要がありますが、大量のメモリを消費せず、大きなファイルを操作できるため) 。彼らがこのAPI呼び出しを削除したとはまだ信じられません...これは、Hadoopエコシステムの他のアプリケーションで正確に使用されていなくても、非常に一般的な使用方法です。
発声

20

Databricksを使用していて、すべてのデータを1つのワーカーの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に収まらない場合は、FileUtils.copyMerge()を使用するchaotic3quilibriumの提案を検討する ことをお勧めします。私はこれを行っていないので、S3などで可能かどうかはまだわかりません。

この回答は、この質問に対する以前の回答と、提供されたコードスニペットの私自身のテストに基づいています。私はもともとそれをDatabricksに投稿し、ここで再公開しています。

私が見つけたdbfsのrmの再帰オプションに関する最良のドキュメントは、Databricksフォーラムにあります。


3

Minkymorganから変更されたS3で機能するソリューション。

元のディレクトリを削除する場合 も、一時パーティションディレクトリパス(最終パスとは異なる名前のパス)srcPathdestPath指定し、単一の最終csv / txtを[ 指定] として渡すだけ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では安全ではありません。あなたがあなたのファイルを書いて、それらをリストするならば-これはそれらのすべてがリストされることを保証しません。[これを見る| docs.aws.amazon.com/AmazonS3/latest/dev/...
LiranBo

3

スパークのdf.write()APIは、指定されたパス内の複数の部品ファイルを作成します...唯一の単一部品ファイルの使用スパーク書き込みを強制するdf.coalesce(1).write.csv(...)代わりに、df.repartition(1).write.csv(...)再分割が広い変態参照のことであるのに対し、合体としては狭い変換で配分()COALESCE対() -スパーク

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

1つのpart-0001-...-c000.csvファイルを使用して、指定されたファイルパスにフォルダーを作成します

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

ユーザーフレンドリーなファイル名にする


あるいは、データフレームが大きすぎない場合(〜GBまたはドライバーのメモリに収まる場合)、df.toPandas().to_csv(path)これを使用して、単一のcsvを優先ファイル名で書き込むこともできます
pprasad009

1
ええと、これがパンダに変換することによってのみ行うことができるのでとてもイライラします。UUIDを含まないファイルを書き込むのはどれほど難しいですか?
ijoseph

2

保存する前に1つのパーティションにrepartition / coalesce(フォルダを取得しますが、その中に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構成の作成)

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:-(スカラリストを文字列に変換し、目的のファイル名をhdfsフォルダ文字列に追加してから、名前の変更を適用します)

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

1

私はこれをPythonで使用して単一のファイルを取得しています:

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

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パス

DariaWriters.writeSingleFileS3でこのメソッドを使用するには、s3aパスを渡す必要があります。

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実装ではを使用しますSpark 3は引き続きHadoop 2を使用していたため、copyMergeの実装は2020年に機能します。SparkがHadoop 3にアップグレードされる時期はわかりませんが、SparkがHadoopをアップグレードするときにコードが破損するcopyMergeアプローチは避けた方がよいでしょう。fs.rename

ソースコード

DariaWriters実装を検査したい場合は、spark-dariaソースコードでオブジェクトを探します。

PySparkの実装

PySparkを使用して単一のファイルを書き出す方が簡単です。デフォルトで単一のファイルとして書き出されるPandas DataFrameに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.writeSingleFileScalaのアプローチと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を使用するもう1つの方法があります。

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、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.