Comment écrire sur plusieurs sorties en fonction de la clé en utilisant Spark dans un seul travail.
Connexes: Écriture sur plusieurs sorties avec la clé Scalding Hadoop, un travail MapReduce
Par exemple.
sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c")))
.writeAsMultiple(prefix, compressionCodecOption)
s'assurerait que cat prefix/1
est
a
b
et cat prefix/2
serait
c
EDIT: J'ai récemment ajouté une nouvelle réponse incluant les importations complètes, le proxénète et le codec de compression, voir https://stackoverflow.com/a/46118044/1586965 , qui peut être utile en plus des réponses précédentes.
Cela inclut le codec tel que demandé, les importations nécessaires et le proxénète tel que demandé.
import org.Apache.spark.rdd.RDD
import org.Apache.spark.sql.SQLContext
// TODO Need a macro to generate for each Tuple length, or perhaps can use shapeless
implicit class PimpedRDD[T1, T2](rdd: RDD[(T1, T2)]) {
def writeAsMultiple(prefix: String, codec: String,
keyName: String = "key")
(implicit sqlContext: SQLContext): Unit = {
import sqlContext.implicits._
rdd.toDF(keyName, "_2").write.partitionBy(keyName)
.format("text").option("codec", codec).save(prefix)
}
}
val myRdd = sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c")))
myRdd.writeAsMultiple("prefix", "org.Apache.hadoop.io.compress.GzipCodec")
Une différence subtile avec l'OP est qu'il préfixera <keyName>=
aux noms de répertoire. Par exemple.
myRdd.writeAsMultiple("prefix", "org.Apache.hadoop.io.compress.GzipCodec")
Donnerait:
prefix/key=1/part-00000
prefix/key=2/part-00000
où prefix/my_number=1/part-00000
contiendrait les lignes a
et b
, et prefix/my_number=2/part-00000
contiendrait la ligne c
.
Et
myRdd.writeAsMultiple("prefix", "org.Apache.hadoop.io.compress.GzipCodec", "foo")
Donnerait:
prefix/foo=1/part-00000
prefix/foo=2/part-00000
Il devrait être clair comment éditer pour parquet
.
Enfin, ci-dessous, vous trouverez un exemple pour Dataset
, qui est peut-être plus pratique que d’utiliser Tuples.
implicit class PimpedDataset[T](dataset: Dataset[T]) {
def writeAsMultiple(prefix: String, codec: String, field: String): Unit = {
dataset.write.partitionBy(field)
.format("text").option("codec", codec).save(prefix)
}
}
Si vous utilisez Spark 1.4+, cela devient beaucoup plus facile grâce à l’API DataFrame API . (Les DataFrames ont été introduits dans Spark 1.3, mais partitionBy()
, dont nous avons besoin, était introduit dans 1.4 .)
Si vous commencez avec un RDD, vous devrez d'abord le convertir en un DataFrame:
val people_rdd = sc.parallelize(Seq((1, "alice"), (1, "bob"), (2, "charlie")))
val people_df = people_rdd.toDF("number", "name")
En Python, ce même code est:
people_rdd = sc.parallelize([(1, "alice"), (1, "bob"), (2, "charlie")])
people_df = people_rdd.toDF(["number", "name"])
Une fois que vous avez un DataFrame, l'écriture sur plusieurs sorties en fonction d'une clé particulière est simple. De plus, et c'est la beauté de l'API DataFrame, le code est à peu près le même en Python, Scala, Java et R:
people_df.write.partitionBy("number").text("people")
Et vous pouvez facilement utiliser d'autres formats de sortie si vous voulez:
people_df.write.partitionBy("number").json("people-json")
people_df.write.partitionBy("number").parquet("people-parquet")
Dans chacun de ces exemples, Spark créera un sous-répertoire pour chacune des clés sur lesquelles nous avons partitionné le DataFrame:
people/
_SUCCESS
number=1/
part-abcd
part-efgh
number=2/
part-abcd
part-efgh
Je le ferais comme ça, qui est évolutif
import org.Apache.hadoop.io.NullWritable
import org.Apache.spark._
import org.Apache.spark.SparkContext._
import org.Apache.hadoop.mapred.lib.MultipleTextOutputFormat
class RDDMultipleTextOutputFormat extends MultipleTextOutputFormat[Any, Any] {
override def generateActualKey(key: Any, value: Any): Any =
NullWritable.get()
override def generateFileNameForKeyValue(key: Any, value: Any, name: String): String =
key.asInstanceOf[String]
}
object Split {
def main(args: Array[String]) {
val conf = new SparkConf().setAppName("Split" + args(1))
val sc = new SparkContext(conf)
sc.textFile("input/path")
.map(a => (k, v)) // Your own implementation
.partitionBy(new HashPartitioner(num))
.saveAsHadoopFile("output/path", classOf[String], classOf[String],
classOf[RDDMultipleTextOutputFormat])
spark.stop()
}
}
Je viens de voir une réponse similaire ci-dessus, mais en réalité nous n’avons pas besoin de partitions personnalisées. MultipleTextOutputFormat créera un fichier pour chaque clé. Il est normal que plusieurs enregistrements avec les mêmes clés entrent dans la même partition.
new HashPartitioner (num), où num correspond au numéro de la partition souhaitée. Si vous avez un grand nombre de clés différentes, vous pouvez définir le nombre sur grand. Dans ce cas, chaque partition n'ouvrira pas trop de gestionnaires de fichiers hdfs.
Si vous avez potentiellement plusieurs valeurs pour une clé donnée, je pense que la solution évolutive consiste à écrire un fichier par clé par partition. Malheureusement, il n’existe pas de support intégré dans Spark, mais nous pouvons créer quelque chose.
sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c")))
.mapPartitionsWithIndex { (p, it) =>
val outputs = new MultiWriter(p.toString)
for ((k, v) <- it) {
outputs.write(k.toString, v)
}
outputs.close
Nil.iterator
}
.foreach((x: Nothing) => ()) // To trigger the job.
// This one is Local, but you could write one for HDFS
class MultiWriter(suffix: String) {
private val writers = collection.mutable.Map[String, Java.io.PrintWriter]()
def write(key: String, value: Any) = {
if (!writers.contains(key)) {
val f = new Java.io.File("output/" + key + "/" + suffix)
f.getParentFile.mkdirs
writers(key) = new Java.io.PrintWriter(f)
}
writers(key).println(value)
}
def close = writers.values.foreach(_.close)
}
(Remplacez PrintWriter
par votre choix d’utilisation du système de fichiers distribué.)
Cela effectue un seul passage sur le RDD et ne fait aucun mélange. Il vous donne un répertoire par clé, avec un nombre de fichiers à l'intérieur.
J'ai un besoin similaire et j'ai trouvé un moyen. Mais il a un inconvénient (qui ne pose pas de problème pour mon cas): vous devez re-partitionner vos données avec une partition par fichier de sortie.
Pour partitionner de cette manière, il est généralement nécessaire de connaître au préalable le nombre de fichiers que le travail va générer et de trouver une fonction qui mappera chaque clé sur chaque partition.
Commençons par créer notre classe basée sur MultipleTextOutputFormat:
import org.Apache.hadoop.mapred.lib.MultipleTextOutputFormat
class KeyBasedOutput[T >: Null, V <: AnyRef] extends MultipleTextOutputFormat[T , V] {
override def generateFileNameForKeyValue(key: T, value: V, leaf: String) = {
key.toString
}
override protected def generateActualKey(key: T, value: V) = {
null
}
}
Avec cette classe, Spark obtiendra une clé d'une partition (la première/dernière, je suppose) et nommera le fichier avec cette clé. Il n'est donc pas bon de mélanger plusieurs clés sur la même partition.
Pour votre exemple, vous aurez besoin d'un partitionneur personnalisé. Cela fera le travail:
import org.Apache.spark.Partitioner
class IdentityIntPartitioner(maxKey: Int) extends Partitioner {
def numPartitions = maxKey
def getPartition(key: Any): Int = key match {
case i: Int if i < maxKey => i
}
}
Maintenant mettons tout ensemble:
val rdd = sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c"), (7, "d"), (7, "e")))
// You need to know the max number of partitions (files) beforehand
// In this case we want one partition per key and we have 3 keys,
// with the biggest key being 7, so 10 will be large enough
val partitioner = new IdentityIntPartitioner(10)
val prefix = "hdfs://.../prefix"
val partitionedRDD = rdd.partitionBy(partitioner)
partitionedRDD.saveAsHadoopFile(prefix,
classOf[Integer], classOf[String], classOf[KeyBasedOutput[Integer, String]])
Cela générera 3 fichiers sous le préfixe (nommés 1, 2 et 7), traitant tout en un seul passage.
Comme vous pouvez le constater, vous devez avoir quelques connaissances sur vos clés pour pouvoir utiliser cette solution.
Pour moi, c'était plus facile, car j'avais besoin d'un fichier de sortie pour chaque hachage de clé et que le nombre de fichiers était sous mon contrôle, je pouvais donc utiliser le stock HashPartitioner pour faire l'affaire.
J'avais besoin de la même chose en Java. Publier ma traduction de La réponse Scala de Zhang Zhan aux utilisateurs de l'API Spark Java:
import org.Apache.hadoop.mapred.lib.MultipleTextOutputFormat;
import org.Apache.spark.SparkConf;
import org.Apache.spark.api.Java.JavaSparkContext;
import scala.Tuple2;
import Java.util.Arrays;
class RDDMultipleTextOutputFormat<A, B> extends MultipleTextOutputFormat<A, B> {
@Override
protected String generateFileNameForKeyValue(A key, B value, String name) {
return key.toString();
}
}
public class Main {
public static void main(String[] args) {
SparkConf conf = new SparkConf()
.setAppName("Split Job")
.setMaster("local");
JavaSparkContext sc = new JavaSparkContext(conf);
String[] strings = {"Abcd", "Azlksd", "whhd", "wasc", "aDxa"};
sc.parallelize(Arrays.asList(strings))
// The first character of the string is the key
.mapToPair(s -> new Tuple2<>(s.substring(0,1).toLowerCase(), s))
.saveAsHadoopFile("output/", String.class, String.class,
RDDMultipleTextOutputFormat.class);
sc.stop();
}
}
J'ai eu un cas d'utilisation similaire où j'ai divisé le fichier d'entrée sur Hadoop HDFS en plusieurs fichiers basés sur une clé (1 fichier par clé). Voici mon code scala pour spark
import org.Apache.hadoop.conf.Configuration;
import org.Apache.hadoop.fs.FileSystem;
import org.Apache.hadoop.fs.Path;
val hadoopconf = new Configuration();
val fs = FileSystem.get(hadoopconf);
@serializable object processGroup {
def apply(groupName:String, records:Iterable[String]): Unit = {
val outFileStream = fs.create(new Path("/output_dir/"+groupName))
for( line <- records ) {
outFileStream.writeUTF(line+"\n")
}
outFileStream.close()
}
}
val infile = sc.textFile("input_file")
val dateGrouped = infile.groupBy( _.split(",")(0))
dateGrouped.foreach( (x) => processGroup(x._1, x._2))
J'ai regroupé les enregistrements en fonction de la clé. Les valeurs de chaque clé sont écrites dans un fichier séparé.
saveAsText () et saveAsHadoop (...) sont implémentés sur la base des données RDD, en particulier par la méthode: PairRDD.saveAsHadoopDataset qui prend les données de PairRdd où elles sont exécutées . Deux options sont possibles: Si vos données sont de taille relativement petite, vous pouvez gagner du temps d’implémentation en les regroupant sur le RDD, en créant un nouveau RDD à partir de chaque collection et en utilisant ce RDD pour écrire les données. Quelque chose comme ça:
val byKey = dataRDD.groupByKey().collect()
val rddByKey = byKey.map{case (k,v) => k->sc.makeRDD(v.toSeq)}
val rddByKey.foreach{ case (k,rdd) => rdd.saveAsText(prefix+k}
Notez que cela ne fonctionnera pas pour les grands ensembles de données car la matérialisation de l'itérateur à v.toSeq
pourrait ne pas tenir dans la mémoire.
L’autre option que je vois, et celle que je recommanderais dans ce cas-ci est la suivante: lancez le vôtre, en appelant directement l’API hadoop/hdfs.
Voici une discussion que j'ai commencée en cherchant cette question: Comment créer des RDD à partir d'un autre RDD?
bonne nouvelle pour l'utilisateur python si vous avez plusieurs colonnes et que vous voulez sauvegarder toutes les autres colonnes non partitionnées au format CSV, ce qui échouera si vous utilisez la méthode "text" comme le suggère Nick Chammas.
people_df.write.partitionBy("number").text("people")
le message d'erreur est "AnalysisException: la source de données u'Text ne prend en charge qu'une seule colonne et vous avez 2 colonnes; '"
Dans spark 2.0.0 (mon environnement de test est l'étincelle 2.0.0 de hdp), le paquetage "com.databricks.spark.csv" est maintenant intégré et nous permet d'enregistrer le fichier texte partitionné par une seule colonne, voir l'exemple:
people_rdd = sc.parallelize([(1,"2016-12-26", "alice"),
(1,"2016-12-25", "alice"),
(1,"2016-12-25", "tom"),
(1, "2016-12-25","bob"),
(2,"2016-12-26" ,"charlie")])
df = people_rdd.toDF(["number", "date","name"])
df.coalesce(1).write.partitionBy("number").mode("overwrite").format('com.databricks.spark.csv').options(header='false').save("people")
[root@namenode people]# tree
.
├── number=1
│?? └── part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
├── number=2
│?? └── part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
└── _SUCCESS
[root@namenode people]# cat number\=1/part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
2016-12-26,alice
2016-12-25,alice
2016-12-25,tom
2016-12-25,bob
[root@namenode people]# cat number\=2/part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
2016-12-26,charlie
Dans mon environnement d'allumage 1.6.1, le code n'a généré aucune erreur, mais il n'y a qu'un seul fichier généré. ce n'est pas partitionné par deux dossiers.
J'espère que cela peut aider.
J'ai eu un cas d'utilisation similaire. Je l'ai résolu en Java en écrivant deux classes personnalisées implémentant MultipleTextOutputFormat
et RecordWriter
.
Mon entrée était un JavaPairRDD<String, List<String>>
et je voulais le stocker dans un fichier nommé par sa clé, avec toutes les lignes contenues dans sa valeur.
Voici le code de mon implémentation MultipleTextOutputFormat
class RDDMultipleTextOutputFormat<K, V> extends MultipleTextOutputFormat<K, V> {
@Override
protected String generateFileNameForKeyValue(K key, V value, String name) {
return key.toString(); //The return will be used as file name
}
/** The following 4 functions are only for visibility purposes
(they are used in the class MyRecordWriter) **/
protected String generateLeafFileName(String name) {
return super.generateLeafFileName(name);
}
protected V generateActualValue(K key, V value) {
return super.generateActualValue(key, value);
}
protected String getInputFileBasedOutputFileName(JobConf job, String name) {
return super.getInputFileBasedOutputFileName(job, name);
}
protected RecordWriter<K, V> getBaseRecordWriter(FileSystem fs, JobConf job, String name, Progressable arg3) throws IOException {
return super.getBaseRecordWriter(fs, job, name, arg3);
}
/** Use my custom RecordWriter **/
@Override
RecordWriter<K, V> getRecordWriter(final FileSystem fs, final JobConf job, String name, final Progressable arg3) throws IOException {
final String myName = this.generateLeafFileName(name);
return new MyRecordWriter<K, V>(this, fs, job, arg3, myName);
}
}
Voici le code de mon implémentation RecordWriter
.
class MyRecordWriter<K, V> implements RecordWriter<K, V> {
private RDDMultipleTextOutputFormat<K, V> rddMultipleTextOutputFormat;
private final FileSystem fs;
private final JobConf job;
private final Progressable arg3;
private String myName;
TreeMap<String, RecordWriter<K, V>> recordWriters = new TreeMap();
MyRecordWriter(RDDMultipleTextOutputFormat<K, V> rddMultipleTextOutputFormat, FileSystem fs, JobConf job, Progressable arg3, String myName) {
this.rddMultipleTextOutputFormat = rddMultipleTextOutputFormat;
this.fs = fs;
this.job = job;
this.arg3 = arg3;
this.myName = myName;
}
@Override
void write(K key, V value) throws IOException {
String keyBasedPath = rddMultipleTextOutputFormat.generateFileNameForKeyValue(key, value, myName);
String finalPath = rddMultipleTextOutputFormat.getInputFileBasedOutputFileName(job, keyBasedPath);
Object actualValue = rddMultipleTextOutputFormat.generateActualValue(key, value);
RecordWriter rw = this.recordWriters.get(finalPath);
if(rw == null) {
rw = rddMultipleTextOutputFormat.getBaseRecordWriter(fs, job, finalPath, arg3);
this.recordWriters.put(finalPath, rw);
}
List<String> lines = (List<String>) actualValue;
for (String line : lines) {
rw.write(null, line);
}
}
@Override
void close(Reporter reporter) throws IOException {
Iterator keys = this.recordWriters.keySet().iterator();
while(keys.hasNext()) {
RecordWriter rw = (RecordWriter)this.recordWriters.get(keys.next());
rw.close(reporter);
}
this.recordWriters.clear();
}
}
La plupart du code est exactement le même que dans FileOutputFormat
. La seule différence est ces quelques lignes
List<String> lines = (List<String>) actualValue;
for (String line : lines) {
rw.write(null, line);
}
Ces lignes m'ont permis d'écrire chaque ligne de mon entrée List<String>
sur le fichier. Le premier argument de la fonction write
est défini sur null
afin d'éviter d'écrire la clé sur chaque ligne.
Pour finir, il me suffit de faire cet appel pour écrire mes fichiers
javaPairRDD.saveAsHadoopFile(path, String.class, List.class, RDDMultipleTextOutputFormat.class);