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:
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?
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).
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(_))
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)
}
}
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.
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.
df.coalesce(1).write.option("inferSchema","true").csv("/newFolder",header =
'true',dateFormat = "yyyy-MM-dd HH:mm:ss")
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
}