2010-05-17 19 views
16

Ich versuche, eine ZIP-Datei zu lesen, zu überprüfen, dass es einige erforderliche Dateien enthält, und dann alle gültigen Dateien in eine andere ZIP-Datei zu schreiben. Die basic introduction to java.util.zip hat eine Menge Java-Ismen und ich würde gerne meinen Code mehr Scala-nativ machen. Insbesondere möchte ich die Verwendung von vars vermeiden. Hier ist, was ich habe:Wie kann ich veränderbare Variablen in Scala vermeiden, wenn ich ZipInputStreams und ZipOutpuStreams verwende?

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 

Ich sollte hinzufügen, dass ich Scala 2.7.7 benutze.

+0

Warum ist Daten null? – sblundy

+0

Weil ich faul war und 'neues Array [Byte]' den Compiler dazu bringt, sich über alternative Konstruktoren zu beschweren. Ich denke, ich sollte 'new ArrayBuffer [Byte]' verwenden. – pr1001

+0

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

Antwort

34

dI glaube nicht, dass es etwas besonders falsch mit Java-Klassen, die in imperativen Art und Weise zu arbeiten, in der Art und Weise gestaltet sind sie entworfen. Idiomatische Scala beinhaltet die Fähigkeit, idiomatisches Java so zu verwenden, wie es beabsichtigt war, auch wenn die Stile ein wenig kollidieren.

Wenn Sie jedoch möchten - vielleicht als Übung, oder vielleicht, weil es die Logik ein wenig verdeutlicht - um dies auf eine funktionellere var-freie Weise zu tun, können Sie dies tun. In 2.8 ist es besonders nett, also, obwohl Sie 2.7.7 verwenden, gebe ich eine 2.8 Antwort.

Zuerst müssen wir das Problem einzurichten, die Sie nicht ganz getan hat, aber nehmen wir an, wir etwas davon haben:

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 

Jetzt, da das wir die Zip-Datei kopieren möchten. Der Trick, den wir verwenden können, ist die continually Methode in collection.immutable.Stream. Was es tut, ist eine träge bewertete Schleife für Sie durchzuführen. Sie können dann die Ergebnisse aufnehmen und filtern, um das zu beenden und zu verarbeiten, was Sie wollen. Es ist ein praktisches Muster, wenn Sie etwas haben, das Sie als Iterator verwenden möchten, aber nicht. (Wenn die Artikel-Updates selbst Sie .iterate in Iterable oder Iterator können --that in der Regel sogar besser.) Hier ist die Anwendung auf diesen Fall, zweimal verwendet: einmal um die Einträge zu erhalten, und einmal/Schreib-Datenblöcke zu lesen:

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 

Achten Sie auf die . am Ende einiger Zeilen! Ich würde das normalerweise auf eine lange Zeile schreiben, aber es ist schöner, es zu wickeln, damit man alles hier sehen kann.

Nur für den Fall, dass es nicht klar ist, lassen Sie uns eine der Anwendungen von continually entpacken.

Stream.continually(zipIn.read(buffer)) 

Das fragt zipIn.read(buffer) für so oft wie nötig, Speichern der integer zu halten Aufruf, die Ergebnisse.

.takeWhile(_ != -1) 

Diese gibt an, wie oft notwendig sind, um einen Strom von unbestimmter Länge zurückkehrt, aber das wird beendet, wenn er ein -1 trifft.

Dies verarbeitet den Stream und nimmt jedes Element der Reihe nach (die Zählung) und verwendet es, um den Puffer zu schreiben.Dies funktioniert auf eine leicht hinterhältige Art und Weise, da Sie sich darauf verlassen, dass zipIn gerade aufgerufen wurde, um das nächste Element des Streams zu erhalten - wenn Sie dies erneut versuchen, nicht bei einem einzigen Durchlauf durch den Stream, würde es fehlschlagen weil buffer würde überschrieben werden. Aber hier ist es in Ordnung.

Also, es ist: eine etwas kompaktere, möglicherweise einfacher zu verstehen, möglicherweise weniger leicht zu verstehen, Methode, die funktionaler ist (obwohl es immer noch Nebenwirkungen in Hülle und Fülle). In 2.7.7 dagegen würde ich es tatsächlich auf Java-Art machen, weil Stream.continually nicht verfügbar ist, und der Aufwand für die Erstellung einer benutzerdefinierten Iterator ist es für diesen einen Fall nicht wert. (Es wäre wert, wenn ich will mehr Zip-Datei Verarbeitung tun und den Code wiederverwenden kann, jedoch.)


Edit: Die Suche-for-Available-to-go-Null-Methode ist eine Art flockig, um das Ende der Zip-Datei zu erkennen. Ich denke, der "korrekte" Weg ist zu warten, bis Sie eine null zurück von bekommen. In diesem Sinne habe ich den vorherigen Code bearbeitet (es gab eine takeWhile(_ => zipIn.available==1), die jetzt eine takeWhile(_ != null) ist) und eine 2.7.7 Iterator basierte Version unten zur Verfügung gestellt (beachten Sie, wie klein die Hauptschleife ist, sobald Sie durch die Definition arbeiten die Iteratoren, die zwar nicht verwenden, 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

Danke, Rex, das ist ein sehr gute Antwort. – pr1001

+0

Danke für den ständigen Trick – Patrick

+0

Die letzte Version hier mit ZipIter hat einen schweren Fehler. Beim Aufruf von getNextEntry wird der Stream-Zeiger tatsächlich weitergeschaltet, sodass sich Ihr Eintrag auf eine andere Sache bezieht, als der Stream festgelegt ist. Z.B. Wenn Sie A.txt B.txt haben, erhalten Sie den Eintrag für A.txt, lesen aber B.txt, dann erhalten Sie den Eintrag für B.txt und ich denke, nichts gelesen. –

1

Ohne Tail-Rekursion würde ich Rekursion vermeiden. Sie riskieren einen Stapelüberlauf. Sie könnten zipIn.read(data) in eine scala.BufferedIterator[Byte] wickeln und von dort aus gehen.

+0

Ok ... Sie schlagen also vor, dass es keinen besseren Ansatz gibt? – pr1001

+0

Entschuldigung, ich brauchte ein paar Minuten um über etwas nachzudenken. – sblundy

+0

Hehe, fair genug! – pr1001

2

Mit scala2.8 und Schwanz rekursive Aufruf:

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

Danke, das sieht ganz nett aus. Leider hätte ich angegeben, dass ich immer noch am 2.7.7 bin. – pr1001

+0

Auch, wie wird dies in 2.7.7 gegenüber 2.8 explodieren? Ich bin nicht sehr vertraut mit dem Thema Schwanzrekursion. Vielen Dank. – pr1001

+1

@ pr1001 Scala 2.8 optimieren Sie den Tail-Call, wenn möglich, so vermeiden Sie Stackoverflow. Für eine Einführung zu was ist Tail Call ich schlage vor, lesen Sie diesen Eintrag zum Beispiel: http://blog.richdougherty.com/2009/04/tail-calls-tailrec-and-trampolines.html – Patrick

2

ich so etwas wie dies versuchen würde (ja, so ziemlich die gleiche Idee hatte 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) 

Es ist wie unten vereinfacht werden könnte, aber ich mag es nicht sehr. Ich würde für read lieber nicht in der Lage sein 0 zurückzukehren ...

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

Basierend auf 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

Dies scheint nicht zu lassen Sie leicht auf die tatsächlichen Dateiinhalte obwohl ... Was für eine schmerzhafte Schnittstelle –