web-dev-qa-db-ja.com

Spark DataFrameのコンテンツを単一のCSVファイルとして保存する

CSVファイルとして保存するSpark DataFrameがあるとします。 Spark 2.0.の後、DataFrameWriterクラスは、CSVファイルとしての保存を直接サポートします。

デフォルトの動作では、指定されたパス内の複数のpart-*。csvファイルに出力が保存されます。

DFをどのように保存しますか:

  1. フォルダーではなく正確なファイル名へのパスマッピング
  2. 最初の行で利用可能なヘッダー
  3. 複数のファイルではなく単一のファイルとして保存します。

これに対処する1つの方法は、DFを結合してからファイルを保存することです。

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

ただし、これにはマスターマシンでの収集に不利な点があり、十分なメモリを備えたマスターが必要です。

coalesceを使用せずに単一のCSVファイルを書き込むことは可能ですか?そうでない場合、上記のコードよりも効率的な方法はありますか?

17

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は常にフォルダーの最後にあるようです。小さいファイルのみで作業し、repartition(1)またはCoalesce(1)を使用できる場合のシンプルで高速なソリューション。

9
user1217169

使用:df.toPandas().to_csv("sample_file.csv", header=True)

詳細については、ドキュメントを参照してください: https://spark.Apache.org/docs/latest/api/python/pyspark.sql.html?highlight=dataframe#pyspark.sql.DataFrame.toPandas

6
osbon123

まだこれをしたい人のために、spark 2.1をscalaで使用し、いくつかのJava.nio.fileヘルプを使用して、これを実現する方法を示します。

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(_))
1
Thien

次のscalaメソッドは、ローカルモードまたはクライアントモードで動作し、選択した名前の単一のcsvにdfを書き込みます。 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)
    }
}
1
Bryan Davis

このソリューションは、シェルスクリプトに基づいており、並列化されていませんが、特に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という名前の3つの列があります)。各パーティションにヘッダーを配置しないようにSparkに指示する必要があります(これは、シェルスクリプトが行うため、.option("header", "false")で実現されます。

1
pietrop

これが、分散コンピューティングの仕組みです。ディレクトリ内の複数のファイルは、まさに分散コンピューティングの仕組みです。これは、すべてのソフトウェアが処理できるため、まったく問題ではありません。

あなたの質問は、「複数のファイルで構成されるCSVをどのようにダウンロードできますか?」 -> SOにはすでに多くのソリューションがあります。

別のアプローチとして、SparkをJDBCソースとして(素晴らしいSpark Thriftサーバーで)使用し、SQLクエリを記述して結果をCSVに変換する方法があります。

(ドライバーはすべてのデータを取得するため)ドライバーでOOMを防ぐには、増分収集(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> 1でシャッフルはしませんか?不可能だよ。

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

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
}

spark-csvを使用して単一のCSVファイルを書き込む を参照してください

0
shants