2010-05-17 4 views
16

J'essaie de lire un fichier zip, de vérifier qu'il contient des fichiers requis, puis d'écrire tous les fichiers valides dans un autre fichier zip. Le basic introduction to java.util.zip a beaucoup de Java-isms et j'aimerais faire mon code plus Scala-natif. Plus précisément, je voudrais éviter l'utilisation de vars. Voici ce que j'ai:Comment éviter les variables mutables dans Scala lors de l'utilisation de ZipInputStreams et de ZipOutpuStreams?

val fos = new FileOutputStream("new.zip"); 
val zipOut = new ZipOutputStream(new BufferedOutputStream(fos)); 

while (zipIn.available == 1) { 
    val entry = zipIn.getNextEntry 
    if (entryIsValid(entry)) { 
    zipOut.putNewEntry(new ZipEntry("subdir/" + entry.getName()) 
    // read data into the data Array 
    var data = Array[Byte](1024) 
    var count = zipIn.read(data, 0, 1024) 
    while (count != -1) { 
     zipOut.write(data, 0, count) 
     count = zipIn.read(data, 0, 1024) 
    } 
    } 
    zipIn.close 
} 
zipOut.close 

Je devrais ajouter que j'utilise Scala 2.7.7.

+0

Pourquoi null données? – sblundy

+0

Parce que j'étais paresseux et 'new Array [Byte]' provoque le compilateur à se plaindre des constructeurs alternatifs. Je suppose que je devrais utiliser 'new ArrayBuffer [Byte]'. – pr1001

+0

var data = new Array [Byte] (1024) –

Répondre

34

dI ne pense pas qu'il y ait quoi que ce soit particulièrement mal à utiliser des classes Java qui sont conçus pour fonctionner en mode impératif de la façon qu'ils ont été conçus. Idiomatic Scala inclut la possibilité d'utiliser le Java idiomatique comme il était prévu, même si les styles se contrarient un peu. Cependant, si vous le souhaitez - peut-être en tant qu'exercice, ou peut-être parce qu'il clarifie légèrement la logique - vous pouvez le faire d'une manière plus fonctionnelle, sans var-free. En 2.8, c'est particulièrement agréable, donc même si vous utilisez 2.7.7, je vais donner une réponse de 2.8.

D'abord, nous avons besoin de mettre en place le problème, que vous ne l'avez pas tout à fait, mais supposons que nous avons quelque chose comme ceci:

import java.io._ 
import java.util.zip._ 
import scala.collection.immutable.Stream 

val fos = new FileOutputStream("new.zip") 
val zipOut = new ZipOutputStream(new BufferedOutputStream(fos)) 
val zipIn = new ZipInputStream(new FileInputStream("old.zip")) 
def entryIsValid(ze: ZipEntry) = !ze.isDirectory 

Maintenant, compte tenu de ce que nous voulons copier le fichier zip. L'astuce que nous pouvons utiliser est la méthode continually en collection.immutable.Stream. Ce qu'il fait est effectuer une boucle paresseusement évaluée pour vous. Vous pouvez ensuite prendre et filtrer les résultats pour terminer et traiter ce que vous voulez. C'est un modèle pratique à utiliser lorsque vous avez quelque chose que vous voulez être un itérateur, mais ce n'est pas le cas. (Si l'article se met à jour, vous pouvez utiliser .iterate dans Iterable ou Iterator - c'est généralement encore mieux.) Voici l'application à ce cas, utilisé deux fois: une fois pour obtenir les entrées, et une fois pour lire/écrire des blocs de données:

val buffer = new Array[Byte](1024) 
Stream.continually(zipIn.getNextEntry). 
    takeWhile(_ != null).filter(entryIsValid). 
    foreach(entry => { 
    zipOut.putNextEntry(new ZipEntry("subdir/"+entry.getName)) 
    Stream.continually(zipIn.read(buffer)).takeWhile(_ != -1). 
     foreach(count => zipOut.write(buffer,0,count)) 
    }) 
} 
zipIn.close 
zipOut.close 

Portez une attention particulière au . à la fin de certaines lignes! J'écrirais normalement ceci sur une longue ligne, mais c'est plus agréable de l'avoir envelopper pour que vous puissiez tout voir ici.

Juste au cas où il est difficile, nous allons déballer l'une des utilisations de continually.

Stream.continually(zipIn.read(buffer)) 

Cette demande de continuer à appeler zipIn.read(buffer) pour autant de fois que nécessaire, le stockage du nombre entier qui en résulte.

.takeWhile(_ != -1) 

Ceci indique combien de fois sont nécessaires, le retour d'un flux de longueur indéfinie, mais qui va quitter quand il frappe un -1.

.foreach(count => zipOut.write(buffer,0,count)) 

Ceci traite le flux, en prenant chaque élément à son tour (le nombre), et en l'utilisant pour écrire le tampon.Cela fonctionne d'une manière un peu sournoise, puisque vous comptez sur le fait que zipIn vient d'être appelé pour obtenir l'élément suivant du flux - si vous avez essayé de le faire à nouveau, pas en un seul passage dans le flux, cela échouerait car buffer serait écrasé. Mais ici c'est bon. Donc, voilà: un peu plus compact, peut-être plus facile à comprendre, peut-être moins facile à comprendre, une méthode plus fonctionnelle (bien qu'il y ait encore beaucoup d'effets secondaires). En 2.7.7, en revanche, je le ferais en Java car Stream.continually n'est pas disponible, et le coût de construction d'un Iterator personnalisé ne vaut pas le coup pour ce cas. (Il vaudrait la peine si je devais faire plus le traitement des fichiers zip et pourrait réutiliser le code, cependant.)


Edit: La méthode recherche-pour-disponible-to-go-zéro est une sorte de flaky pour détecter la fin du fichier zip. Je pense que le "bon" moyen est d'attendre jusqu'à ce que vous obteniez un null de getNextEntry. Dans cet esprit, j'ai édité le code précédent (il y avait un takeWhile(_ => zipIn.available==1) qui est maintenant un takeWhile(_ != null)) et fourni une version basée sur l'itérateur 2.7.7 ci-dessous (notez combien la boucle principale est petite, une fois que vous passez à travers le travail de définition les itérateurs, qui n'utilisent certes vars):

val buffer = new Array[Byte](1024) 
class ZipIter(zis: ZipInputStream) extends Iterator[ZipEntry] { 
    private var entry:ZipEntry = zis.getNextEntry 
    private var cached = true 
    private def cache { if (entry != null && !cached) { 
    cached = true; entry = zis.getNextEntry 
    }} 
    def hasNext = { cache; entry != null } 
    def next = { 
    if (!cached) cache 
    cached = false 
    entry 
    } 
} 
class DataIter(is: InputStream, ab: Array[Byte]) extends Iterator[(Int,Array[Byte])] { 
    private var count = 0 
    private var waiting = false 
    def hasNext = { 
    if (!waiting && count != -1) { count = is.read(ab); waiting=true } 
    count != -1 
    } 
    def next = { waiting=false; (count,ab) } 
} 
(new ZipIter(zipIn)).filter(entryIsValid).foreach(entry => { 
    zipOut.putNextEntry(new ZipEntry("subdir/"+entry.getName)) 
    (new DataIter(zipIn,buffer)).foreach(cb => zipOut.write(cb._2,0,cb._1)) 
}) 
zipIn.close 
zipOut.close 
+0

Merci, Rex, c'est un très Bonne réponse. – pr1001

+0

Merci pour l'astuce continue – Patrick

+0

La dernière version ici avec ZipIter a un bug sérieux. L'appel de getNextEntry avance réellement le pointeur de flux, de sorte que votre entrée fait référence à une chose différente de celle définie dans le flux. Par exemple. Si vous avez A.txt B.txt vous obtiendrez l'entrée pour A.txt mais lisez B.txt, puis obtenez l'entrée pour B.txt et je pense que je ne lis rien. –

1

Sans récursion de queue, j'éviterais la récursivité. Vous courriez le risque d'un débordement de pile. Vous pouvez envelopper zipIn.read(data) dans un scala.BufferedIterator[Byte] et partir de là.

+0

Ok ... Alors, dites-vous qu'il n'y a pas de meilleure approche? – pr1001

+0

Désolé, il m'a fallu quelques minutes pour penser à quelque chose. – sblundy

+0

Hehe, assez juste! – pr1001

2

En utilisant scala2.8 et la queue appel récursif:

def copyZip(in: ZipInputStream, out: ZipOutputStream, bufferSize: Int = 1024) { 
    val data = new Array[Byte](bufferSize) 

    def copyEntry() { 
    in getNextEntry match { 
     case null => 
     case entry => { 
     if (entryIsValid(entry)) { 
      out.putNextEntry(new ZipEntry("subdir/" + entry.getName())) 

      def copyData() { 
      in read data match { 
       case -1 => 
       case count => { 
       out.write(data, 0, count) 
       copyData() 
       } 
      } 
      } 
      copyData() 
     } 
     copyEntry() 
     } 
    } 
    } 
    copyEntry() 
} 
+0

Merci, ça a l'air plutôt sympa. Malheureusement, j'aurais dû préciser que je suis toujours sur 2.7.7. – pr1001

+0

En outre, comment cela va exploser dans 2.7.7 contre 2.8? Je ne suis pas très au courant sur le sujet de la récursivité de la queue. Merci. – pr1001

+1

@ pr1001 Scala 2.8 optimiser l'appel de queue lorsque c'est possible afin d'éviter le stackoverflow. Pour une introduction à ce que l'appel de queue je vous suggère de lire cette entrée par exemple: http://blog.richdougherty.com/2009/04/tail-calls-tailrec-and-trampolines.html – Patrick

2

Je vais essayer quelque chose comme ça (oui, à peu près la même idée avait sblundy):

Iterator.continually { 
    val data = new Array[Byte](100) 
    zipIn.read(data) match { 
    case -1 => Array.empty[Byte] 
    case 0 => new Array[Byte](101) // just to filter it out 
    case n => java.util.Arrays.copyOf(data, n) 
    } 
} filter (_.size != 101) takeWhile (_.nonEmpty) 

Il pourrait être simplifié comme ci-dessous, mais je ne l'aime pas beaucoup. Je préfère pour read de ne pas pouvoir retourner 0 ...

Iterator.continually { 
    val data = new Array[Byte](100) 
    zipIn.read(data) match { 
    case -1 => new Array[Byte](101) 
    case n => java.util.Arrays.copyOf(data, n) 
    } 
} takeWhile (_.size != 101) 
2

Basé sur http://harrah.github.io/browse/samples/compiler/scala/tools/nsc/io/ZipArchive.scala.html:

private[io] class ZipEntryTraversableClass(in: InputStream) extends Traversable[ZipEntry] { 
    val zis = new ZipInputStream(in) 

    def foreach[U](f: ZipEntry => U) { 
    @tailrec 
    def loop(x: ZipEntry): Unit = if (x != null) { 
     f(x) 
     zis.closeEntry() 
     loop(zis.getNextEntry()) 
    } 
    loop(zis.getNextEntry()) 
    } 

    def writeCurrentEntryTo(os: OutputStream) { 
    IOUtils.copy(zis, os) 
    } 
} 
+0

Cela ne semble pas vous permettre d'obtenir facilement le contenu réel du fichier ... quelle interface douloureuse –

Questions connexes