Sto usando https://github.com/databricks/spark-csv , sto cercando di scrivere un singolo CSV, ma non ci riesco, sta creando una cartella.
Serve una funzione Scala che prenda parametri come il percorso e il nome del file e scriva quel file CSV.
Sto usando https://github.com/databricks/spark-csv , sto cercando di scrivere un singolo CSV, ma non ci riesco, sta creando una cartella.
Serve una funzione Scala che prenda parametri come il percorso e il nome del file e scriva quel file CSV.
Risposte:
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 cat
o HDFSgetmerge
per unire semplicemente tutte le parti in seguito.
.coalesce(1)
dice che alcune FileNotFoundException sulla directory _temporary. È ancora un bug in Spark: issues.apache.org/jira/browse/SPARK-2984
coalesce(1)
essere molto costoso e di solito non pratico.
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.
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?
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 .
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 srcPath
e singolo csv / txt finale come destPath
Specificare anche deleteSource
se 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' 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.csv
file specificato con un utilizzo di file
cat filepath/part-0001-...-c000.csv > filename_you_want.csv
avere un nome file facile da usare
df.toPandas().to_csv(path)
per scrivere un singolo csv con il tuo nome file preferito
ripartizione / fusione in 1 partizione prima di salvare (si otterrebbe comunque una cartella ma avrebbe un file di parte in essa)
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)
Lo sto usando in Python per ottenere un singolo file:
df.toPandas().to_csv("/tmp/my.csv", sep=',', header=True, index=False)
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.csv
file 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.csv
file.
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.writeSingleFile
per 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.writeSingleFile
implementazione 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' DariaWriters
oggetto 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.writeSingleFile
approccio 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.
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()
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)}