Scrivi un singolo file CSV utilizzando spark-csv


Risposte:


168

Sta creando una cartella con più file, perché ogni partizione viene salvata individualmente. Se hai bisogno di un singolo file di output (ancora in una cartella) puoi repartition(preferito se i dati a monte sono grandi, ma richiede una riproduzione casuale):

df
   .repartition(1)
   .write.format("com.databricks.spark.csv")
   .option("header", "true")
   .save("mydata.csv")

oppure coalesce:

df
   .coalesce(1)
   .write.format("com.databricks.spark.csv")
   .option("header", "true")
   .save("mydata.csv")

frame di dati prima del salvataggio:

Tutti i dati verranno scritti mydata.csv/part-00000. Prima di utilizzare questa opzione assicurati di aver compreso cosa sta succedendo e qual è il costo del trasferimento di tutti i dati a un singolo lavoratore . Se utilizzi un file system distribuito con la replica, i dati verranno trasferiti più volte, prima recuperati su un singolo worker e successivamente distribuiti sui nodi di archiviazione.

In alternativa, puoi lasciare il codice così com'è e utilizzare strumenti generici come cato HDFSgetmerge per unire semplicemente tutte le parti in seguito.


6
puoi usare coalesce anche: df.coalesce (1) .write.format ("com.databricks.spark.csv") .option ("header", "true") .save ("mydata.csv")
ravi

spark 1.6 genera un errore quando lo impostiamo .coalesce(1)dice che alcune FileNotFoundException sulla directory _temporary. È ancora un bug in Spark: issues.apache.org/jira/browse/SPARK-2984
Harsha

@Harsha Unlikely. Piuttosto un semplice risultato di coalesce(1)essere molto costoso e di solito non pratico.
zero323

Concordato @ zero323, ma se hai un requisito speciale per il consolidamento in un file, dovrebbe essere ancora possibile dato che hai ampie risorse e tempo.
Harsha

2
@Harsha Non dico che non ci sia. Se ottimizzi correttamente il GC, dovrebbe funzionare bene, ma è semplicemente una perdita di tempo e molto probabilmente danneggerà le prestazioni complessive. Quindi personalmente non vedo alcun motivo per preoccuparmi, soprattutto perché è banalmente semplice unire file al di fuori di Spark senza preoccuparmi affatto dell'utilizzo della memoria.
zero323

36

Se stai utilizzando Spark con HDFS, ho risolto il problema scrivendo normalmente i file CSV e sfruttando HDFS per eseguire l'unione. Lo sto facendo direttamente in 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()

Non ricordo dove ho imparato questo trucco, ma potrebbe funzionare per te.


Non l'ho provato e sospetto che potrebbe non essere semplice.
Minkymorgan

1
Grazie. Ho aggiunto una risposta che funziona su Databricks
Josiah Yoder

@Minkymorgan ho problema simile, ma non in grado di farlo correttamente ..Can si prega di guardare al questa domanda stackoverflow.com/questions/46812388/...
Sudarshan

4
@SUDARSHAN La mia funzione sopra funziona con dati non compressi. Nel tuo esempio penso che tu stia usando la compressione gzip mentre scrivi i file - e poi dopo - provando a unirli insieme, cosa che non riesce. Non funzionerà, poiché non è possibile unire file gzip insieme. Gzip non è un algoritmo di compressione divisibile, quindi certamente non "unificabile". Potresti testare la compressione "scattante" o "bz2", ma la sensazione è che anche questa fallirà con l'unione. Probabilmente la cosa migliore è rimuovere la compressione, unire i file non elaborati, quindi comprimere utilizzando un codec divisibile.
Minkymorgan

e se volessi conservare l'intestazione? duplica per ogni parte del file
Normale

32

Potrei essere un po 'in ritardo per il gioco qui, ma usando coalesce(1)orepartition(1) potrebbe funzionare per piccoli set di dati, ma grandi set di dati verrebbero tutti gettati in una partizione su un nodo. È probabile che questo generi errori OOM o, nella migliore delle ipotesi, venga elaborato lentamente.

Consiglio vivamente di utilizzare la FileUtil.copyMerge()funzione dall'API di Hadoop. Questo unirà gli output in un unico file.

MODIFICA - Questo porta efficacemente i dati al driver piuttosto che a un nodo esecutore. Coalesce()andrebbe bene se un singolo esecutore avesse più RAM da usare rispetto al driver.

EDIT 2 : copyMerge()viene rimosso in Hadoop 3.0. Per ulteriori informazioni su come lavorare con la versione più recente, vedere il seguente articolo sull'overflow dello stack : Come eseguire CopyMerge in Hadoop 3.0?


Qualche idea su come ottenere un CSV con una riga di intestazione in questo modo? Non vorrei che il file producesse un'intestazione, poiché ciò intervallerebbe le intestazioni in tutto il file, una per ogni partizione.
nojo

C'è un'opzione che ho usato in passato documentata qui: markhneedham.com/blog/2014/11/30/…
etspaceman

@etspaceman Cool. Non ho ancora un buon modo per farlo, sfortunatamente, poiché devo essere in grado di farlo in Java (o Spark, ma in un modo che non consuma molta memoria e può funzionare con file di grandi dimensioni) . Non riesco ancora a credere che abbiano rimosso questa chiamata API ... questo è un utilizzo molto comune anche se non esattamente utilizzato da altre applicazioni nell'ecosistema Hadoop.
woot

20

Se stai usando Databricks e puoi adattare tutti i dati nella RAM su un worker (e quindi puoi usarlo .coalesce(1)), puoi usare dbfs per trovare e spostare il file CSV risultante:

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)

Se il tuo file non si adatta alla RAM del worker, potresti prendere in considerazione il suggerimento di chaotic3quilibrium di utilizzare FileUtils.copyMerge () . Non l'ho fatto e non so ancora se sia possibile o meno, ad esempio, su S3.

Questa risposta si basa sulle risposte precedenti a questa domanda, nonché sui miei test dello snippet di codice fornito. Inizialmente l'ho pubblicato su Databricks e lo ripubblico qui.

La migliore documentazione per l'opzione ricorsiva di dbfs rm che ho trovato si trova su un forum di Databricks .


3

Una soluzione che funziona per S3 modificata da Minkymorgan.

Basta passare il percorso della directory partizionata temporanea (con un nome diverso dal percorso finale) come srcPathe singolo csv / txt finale come destPath Specificare anche deleteSourcese si desidera rimuovere la directory originale.

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

L'implementazione copyMerge elenca tutti i file e li ripete, questo non è sicuro in s3. se scrivi i tuoi file e poi li elenchi, questo non garantisce che saranno elencati tutti. vedere [questo | docs.aws.amazon.com/AmazonS3/latest/dev/…
LiranBo

3

L' df.write()API di spark creerà più file di parti all'interno di un determinato percorso ... per forzare la scrittura di spark solo su un singolo file di parte utilizzato df.coalesce(1).write.csv(...)invece di df.repartition(1).write.csv(...)come coalesce è una trasformazione ristretta mentre la ripartizione è una trasformazione ampia vedere Spark - repartition () vs coalesce ()

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

creerà una cartella nel percorso part-0001-...-c000.csvfile specificato con un utilizzo di file

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

avere un nome file facile da usare


in alternativa se il dataframe non è troppo grande (~ GB o può entrare nella memoria del driver) puoi anche usarlo df.toPandas().to_csv(path)per scrivere un singolo csv con il tuo nome file preferito
pprasad009

1
Uffa, è così frustrante come questo possa essere fatto solo convertendosi in panda. Quanto è difficile scrivere un file senza alcun UUID in esso?
ijoseph

2

ripartizione / fusione in 1 partizione prima di salvare (si otterrebbe comunque una cartella ma avrebbe un file di parte in essa)


2

Puoi usare rdd.coalesce(1, true).saveAsTextFile(path)

memorizzerà i dati come file singolo in 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._

Ho risolto usando l'approccio seguente (hdfs rinomina il nome del file): -

Passaggio 1: - (Crate Data Frame e scrivi su HDFS)

df.coalesce(1).write.format("csv").option("header", "false").mode(SaveMode.Overwrite).save("/hdfsfolder/blah/")

Passaggio 2: - (Crea configurazione Hadoop)

val hadoopConfig = new Configuration()
val hdfs = FileSystem.get(hadoopConfig)

Passaggio 3: - (Ottieni percorso nel percorso della cartella hdfs)

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

Step4: - (Ottieni i nomi dei file spark dalla cartella hdfs)

val fileNames = hdfs.listFiles(pathFiles, false)
println(fileNames)

setp5: - (crea l'elenco scala mutabile per salvare tutti i nomi dei file e aggiungerlo all'elenco)

    var fileNamesList = scala.collection.mutable.MutableList[String]()
    while (fileNames.hasNext) {
      fileNamesList += fileNames.next().getPath.getName
    }
    println(fileNamesList)

Passaggio 6: - (filtra l'ordine dei file _SUCESS dall'elenco scala dei nomi dei file)

    // get files name which are not _SUCCESS
    val partFileName = fileNamesList.filterNot(filenames => filenames == "_SUCCESS")

passaggio 7: - (converti l'elenco scala in stringa e aggiungi il nome del file desiderato alla stringa della cartella hdfs, quindi applica la ridenominazione)

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

1

Lo sto usando in Python per ottenere un singolo file:

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

1

Questa risposta si espande sulla risposta accettata, fornisce più contesto e fornisce snippet di codice che puoi eseguire in Spark Shell sul tuo computer.

Più contesto sulla risposta accettata

La risposta accettata potrebbe darti l'impressione che il codice di esempio restituisca un singolo mydata.csvfile e non è così. Dimostriamo:

val df = Seq("one", "two", "three").toDF("num")
df
  .repartition(1)
  .write.csv(sys.env("HOME")+ "/Documents/tmp/mydata.csv")

Ecco cosa viene emesso:

Documents/
  tmp/
    mydata.csv/
      _SUCCESS
      part-00000-b3700504-e58b-4552-880b-e7b52c60157e-c000.csv

NB mydata.csv è una cartella nella risposta accettata - non è un file!

Come produrre un singolo file con un nome specifico

Possiamo usare spark-daria per scrivere un singolo mydata.csvfile.

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

Questo produrrà il file come segue:

Documents/
  better/
    mydata.csv

Percorsi S3

Dovrai passare percorsi s3a DariaWriters.writeSingleFileper utilizzare questo metodo in S3:

DariaWriters.writeSingleFile(
    df = df,
    format = "csv",
    sc = spark.sparkContext,
    tmpFolder = "s3a://bucket/data/src",
    filename = "s3a://bucket/data/dest/my_cool_file.csv"
)

Vedi qui per maggiori informazioni.

Evitare copyMerge

copyMerge è stato rimosso da Hadoop 3. L' DariaWriters.writeSingleFileimplementazione utilizza fs.rename, come descritto qui . Spark 3 utilizzava ancora Hadoop 2 , quindi le implementazioni di copyMerge funzioneranno nel 2020. Non sono sicuro quando Spark verrà aggiornato a Hadoop 3, ma è meglio evitare qualsiasi approccio copyMerge che causerà l'interruzione del codice quando Spark aggiorna Hadoop.

Codice sorgente

Cerca l' DariaWritersoggetto nel codice sorgente spark-daria se desideri ispezionare l'implementazione.

Implementazione di PySpark

È più facile scrivere un singolo file con PySpark perché puoi convertire il DataFrame in un Pandas DataFrame che viene scritto come un singolo file per impostazione predefinita.

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)

limitazioni

L' DariaWriters.writeSingleFileapproccio Scala e l' df.toPandas()approccio Python funzionano solo per piccoli set di dati. Set di dati enormi non possono essere scritti come file singoli. Scrivere i dati come un singolo file non è ottimale dal punto di vista delle prestazioni perché i dati non possono essere scritti in parallelo.


0

utilizzando Listbuffer possiamo salvare i dati in un unico file:

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

C'è un altro modo per usare 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)}

il nome "true" non è definito
Arron
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.