web-dev-qa-db-fra.com

Enregistrer le contenu de Spark DataFrame en tant que fichier CSV unique

Supposons que je dispose d'un DataFrame Spark que je souhaite enregistrer en tant que fichier CSV. Après Spark 2.0.0 , DataFrameWriter class prend directement en charge l’enregistrement en tant que fichier CSV.

Le comportement par défaut consiste à enregistrer la sortie dans plusieurs fichiers part - *. Csv dans le chemin indiqué. 

Comment enregistrer un DF avec:

  1. Mappage de chemin vers le nom de fichier exact au lieu du dossier
  2. En-tête disponible en première ligne
  3. Enregistrer en tant que fichier unique au lieu de plusieurs fichiers.

Une façon de résoudre ce problème consiste à fusionner le fichier DF, puis à enregistrer le fichier. 

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

Cependant, cela a un inconvénient de le collecter sur la machine maître et nécessite d'avoir un maître avec suffisamment de mémoire. 

Est-il possible d'écrire un seul fichier CSV sans utiliser coalesce ? Sinon, existe-t-il un moyen plus efficace que le code ci-dessus?

15
Spandan Brahmbhatt

Je viens de résoudre ce problème moi-même avec pyspark avec dbutils pour obtenir le fichier .csv et le renommer en nom de fichier souhaité.

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)

Cette réponse peut être améliorée en n'utilisant pas [-1], mais le fichier .csv semble toujours figurer en dernier dans le dossier. Solution simple et rapide si vous travaillez uniquement sur des fichiers plus petits et pouvez utiliser la répartition (1) ou la fusion (1).

6
user1217169

Pour ceux qui veulent encore le faire, voici comment je l’ai fait en utilisant spark 2.1 dans scala avec de l’aide Java.nio.file.

Basé sur 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

La méthode scala suivante fonctionne en mode local ou client et écrit le df dans un seul fichier CSV du nom choisi. Cela nécessite que le df rentre dans la mémoire, sinon collect () va exploser. 

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

Cette solution est basée sur un script shell et n’est pas parallélisée, mais reste très rapide, en particulier sur les disques SSD. Il utilise cat et la redirection de sortie sur les systèmes Unix. Supposons que le répertoire CSV contenant les partitions se trouve sur /my/csv/dir et que le fichier de sortie est /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"

Il supprimera chaque partition après l'avoir ajoutée au fichier CSV final afin de libérer de l'espace.

"col1,col2,col3" est l'en-tête CSV (nous avons ici trois colonnes de nom col1, col2 et col3). Vous devez dire à Spark de ne pas placer l'en-tête dans chaque partition (ceci est accompli avec .option("header", "false") car le script shell le fera.

1
pietrop

Voici comment fonctionne l'informatique distribuée! La multiplicité des fichiers dans un répertoire correspond exactement au fonctionnement de l'informatique distribuée. Ce n'est pas du tout un problème puisque tous les logiciels peuvent le gérer.

Votre question devrait être "comment est-il possible de télécharger un fichier CSV composé de plusieurs fichiers?" -> il y a déjà beaucoup de solutions en SO.

Une autre approche pourrait consister à utiliser Spark comme source JDBC (avec l’impressionnant serveur Spark Thrift), à écrire une requête SQL et à transformer le résultat en CSV. 

Afin d’empêcher le MOO dans le pilote (car le pilote obtiendra TOUTES les données), utilisez la collecte incrémentielle (spark.sql.thriftServer.incrementalCollect=true), plus d'infos sur http://www.russellspitzer.com/2017/05/19/Spark-Sql-Thriftserver/ .


Petit récapitulatif sur le concept de "partition de données" de Spark:

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

Entre les "étapes", les données peuvent être transférées entre les partitions, c'est le "shuffle". Vous voulez "Z" = 1, mais avec Y> 1, sans shuffle? c'est impossible.

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

FileUtil.copyMerge () de l'API Hadoop devrait résoudre votre problème. 

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
}

Voir Écrire un seul fichier CSV en utilisant spark-csv

0
shants