web-dev-qa-db-fra.com

Lire des fichiers texte entiers à partir d'une compression dans Spark

J'ai le problème suivant: supposons que j'ai un répertoire contenant des répertoires compressés contenant plusieurs fichiers, stockés sur HDFS. Je veux créer un RDD composé d’objets de type T, c’est-à-dire:

context = new JavaSparkContext(conf);
JavaPairRDD<String, String> filesRDD = context.wholeTextFiles(inputDataPath);

JavaPairRDD<String, String> filesRDD = context.wholeTextFiles(inputDataPath);
JavaRDD<T> processingFiles = filesRDD.map(fileNameContent -> {
    // The name of the file
    String fileName = fileNameContent._1();
    // The content of the file
    String content = fileNameContent._2();

    // Class T has a constructor of taking the filename and the content of each
    // processed file (as two strings)
    T t = new T(content, fileName);

    return t;
});

Maintenant, lorsque inputDataPath est un répertoire contenant des fichiers, cela fonctionne parfaitement, c’est-à-dire quand il s’agit de quelque chose comme:

String inputDataPath =  "hdfs://some_path/*/*/"; // because it contains subfolders

Mais, quand il y a un tgz contenant plusieurs fichiers, le contenu du fichier (fileNameContent._2()) me procure une chaîne binaire inutile (tout à fait prévu). J'ai trouvé une question similaire sur SO , mais ce n'est pas le même cas, car la solution consiste à ce que chaque compression soit composée d'un seul fichier et, dans mon cas, de nombreux autres fichiers que je vouloir lire individuellement sous forme de fichiers entiers. J'ai aussi trouvé une question à propos de wholeTextFiles, mais cela ne fonctionne pas dans mon cas.

Des idees pour faire cela?

EDIT:

J'ai essayé avec le lecteur de ici (essayant de tester le lecteur de ici , comme dans la fonction testTarballWithFolders()), mais chaque fois que j'appelle

TarballReader tarballReader = new TarballReader(fileName);

et je reçois NullPointerException:

Java.lang.NullPointerException
    at Java.util.Zip.InflaterInputStream.<init>(InflaterInputStream.Java:83)
    at Java.util.Zip.GZIPInputStream.<init>(GZIPInputStream.Java:77)
    at Java.util.Zip.GZIPInputStream.<init>(GZIPInputStream.Java:91)
    at utils.TarballReader.<init>(TarballReader.Java:61)
    at main.SparkMain.lambda$0(SparkMain.Java:105)
    at main.SparkMain$$Lambda$18/1667100242.call(Unknown Source)
    at org.Apache.spark.api.Java.JavaPairRDD$$anonfun$toScalaFunction$1.apply(JavaPairRDD.scala:1015)
    at scala.collection.Iterator$$anon$11.next(Iterator.scala:328)
    at scala.collection.Iterator$class.foreach(Iterator.scala:727)
    at scala.collection.AbstractIterator.foreach(Iterator.scala:1157)
    at scala.collection.generic.Growable$class.$plus$plus$eq(Growable.scala:48)
    at scala.collection.mutable.ArrayBuffer.$plus$plus$eq(ArrayBuffer.scala:103)
    at scala.collection.mutable.ArrayBuffer.$plus$plus$eq(ArrayBuffer.scala:47)
    at scala.collection.TraversableOnce$class.to(TraversableOnce.scala:273)
    at scala.collection.AbstractIterator.to(Iterator.scala:1157)
    at scala.collection.TraversableOnce$class.toBuffer(TraversableOnce.scala:265)
    at scala.collection.AbstractIterator.toBuffer(Iterator.scala:1157)
    at scala.collection.TraversableOnce$class.toArray(TraversableOnce.scala:252)
    at scala.collection.AbstractIterator.toArray(Iterator.scala:1157)
    at org.Apache.spark.rdd.RDD$$anonfun$collect$1$$anonfun$12.apply(RDD.scala:927)
    at org.Apache.spark.rdd.RDD$$anonfun$collect$1$$anonfun$12.apply(RDD.scala:927)
    at org.Apache.spark.SparkContext$$anonfun$runJob$5.apply(SparkContext.scala:1858)
    at org.Apache.spark.SparkContext$$anonfun$runJob$5.apply(SparkContext.scala:1858)
    at org.Apache.spark.scheduler.ResultTask.runTask(ResultTask.scala:66)
    at org.Apache.spark.scheduler.Task.run(Task.scala:89)
    at org.Apache.spark.executor.Executor$TaskRunner.run(Executor.scala:214)
    at Java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.Java:1142)
    at Java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.Java:617)
    at Java.lang.Thread.run(Thread.Java:745)

La ligne 105 dans MainSpark est celle que j'ai affichée en haut de mon édition du message, et la ligne 61 de TarballReader est

GZIPInputStream gzip = new GZIPInputStream(in);

qui donne une valeur nulle pour le flux d'entrée in dans la ligne supérieure:

InputStream in = this.getClass().getResourceAsStream(tarball);

Suis-je sur le bon chemin ici? Si oui, comment puis-je continuer? Pourquoi ai-je cette valeur nulle et comment puis-je la résoudre?

9
Belphegor

Une solution possible consiste à lire les données avec binaryFiles et à extraire le contenu manuellement.

Scala :

import org.Apache.commons.compress.compressors.gzip.GzipCompressorInputStream
import org.Apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.Apache.spark.input.PortableDataStream
import scala.util.Try
import Java.nio.charset._

def extractFiles(ps: PortableDataStream, n: Int = 1024) = Try {
  val tar = new TarArchiveInputStream(new GzipCompressorInputStream(ps.open))
  Stream.continually(Option(tar.getNextTarEntry))
    // Read until next exntry is null
    .takeWhile(_.isDefined)
    // flatten
    .flatMap(x => x)
    // Drop directories
    .filter(!_.isDirectory)
    .map(e => {
      Stream.continually {
        // Read n bytes
        val buffer = Array.fill[Byte](n)(-1)
        val i = tar.read(buffer, 0, n)
        (i, buffer.take(i))}
      // Take as long as we've read something
      .takeWhile(_._1 > 0)
      .map(_._2)
      .flatten
      .toArray})
    .toArray
}

def decode(charset: Charset = StandardCharsets.UTF_8)(bytes: Array[Byte]) = 
  new String(bytes, StandardCharsets.UTF_8)

sc.binaryFiles("somePath").flatMapValues(x => 
  extractFiles(x).toOption).mapValues(_.map(decode()))
libraryDependencies += "org.Apache.commons" % "commons-compress" % "1.11"

Exemple d'utilisation complète avec Java: https://bitbucket.org/zero323/spark-multifile-targz-extract/src

Python :

import tarfile
from io import BytesIO

def extractFiles(bytes):
    tar = tarfile.open(fileobj=BytesIO(bytes), mode="r:gz")
    return [tar.extractfile(x).read() for x in tar if x.isfile()]

(sc.binaryFiles("somePath")
    .mapValues(extractFiles)
    .mapValues(lambda xs: [x.decode("utf-8") for x in xs]))
27
zero323

Une légère amélioration par rapport à la réponse acceptée est de changer

Option(tar.getNextTarEntry)

à

Try(tar.getNextTarEntry).toOption.filter( _ != null)

lutter avec .tar.gzs mal formé/tronqué de manière robuste.

BTW, y at-il quelque chose de spécial à propos de la taille du tableau de mémoire tampon? Serait-il plus rapide en moyenne s'il était plus proche de la taille moyenne du fichier, peut-être 500 Ko dans mon cas? Ou est-ce le ralentissement que je vois plus probablement la surcharge de Stream par rapport à une boucle while qui était plus Java-ish, je suppose.

0
Neil Best