2010-05-17 14 views
16

Próbuję odczytać plik zip, sprawdzić, czy ma jakieś wymagane pliki, a następnie zapisać wszystkie prawidłowe pliki do innego pliku zip. The basic introduction to java.util.zip ma wiele Java-isms i chciałbym, aby mój kod bardziej natywny dla Scala. W szczególności chciałbym uniknąć użycia vars. Oto, co mam:Jak mogę uniknąć zmiennych zmiennych w Scali podczas korzystania z ZipInputStreams i 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 

Należy dodać, że używam Scala 2.7.7.

+0

Dlaczego jest null dane? – sblundy

+0

Ponieważ byłem leniwy, a 'new Array [Byte]' powoduje, że kompilator narzeka na alternatywnych konstruktorów. Chyba powinienem używać 'nowego ArrayBuffer [Byte]'. – pr1001

+0

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

Odpowiedz

34

dI nie sądzę, nie ma nic szczególnie złego w używaniu klas Java, które są przeznaczone do pracy w bezwzględnej mody w modzie były zaprojektowane. Idiomatic Scala obejmuje możliwość używania idiomatycznej Java, tak jak było to zamierzone, nawet jeśli style są nieco sprzeczne.

Jednakże, jeśli chcesz - może jako ćwiczenie, a może dlatego, że nieco wyjaśnia logikę - aby zrobić to w bardziej funkcjonalny sposób, możesz to zrobić. W wersji 2.8 jest to szczególnie miłe, więc mimo że używasz 2.7.7, dam odpowiedź 2.8.

Po pierwsze, musimy ustawić się problem, którego nie do końca, ale załóżmy, że mamy coś takiego:

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 

Teraz, biorąc pod uwagę to chcemy skopiować plik zip. Sztuczka, którą możemy wykorzystać, to metoda continually w collection.immutable.Stream. To, co robi, to wykonać dla ciebie leniwo oszacowaną pętlę. Następnie możesz pobrać i przefiltrować wyniki, aby zakończyć i przetworzyć to, co chcesz. Jest to przydatny wzór do użycia, gdy masz coś, co chcesz być iteratorem, ale tak nie jest. (Jeśli element aktualizuje się, możesz użyć .iterate w Iterable lub Iterator - co zwykle jest jeszcze lepsze.) Oto aplikacja do tego przypadku, używana dwukrotnie: raz, aby uzyskać wpisy, i raz, aby odczytać/zapisać fragmenty danych:

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 

Zwróć szczególną uwagę na . na końcu niektórych linii! Normalnie napiszę to na jednej długiej linii, ale przyjemniej jest ją zawijać, żebyś mógł to wszystko zobaczyć tutaj.

Na wypadek, gdyby nie było to jasne, rozpakujmy jedno z zastosowań continually.

Stream.continually(zipIn.read(buffer)) 

Ten prosi wzywasz zipIn.read(buffer) tyle razy, ile jest to konieczne, przechowywania liczbę całkowitą, która skutkuje.

.takeWhile(_ != -1) 

ten określa, ile razy jest to konieczne, wracając strumień nieokreślonej długości, ale która wyjdzie gdy natrafi -1.

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

Spowoduje to przetworzenie strumienia, pobranie każdego elementu (liczby) i użycie go do zapisania bufora.Działa to w nieco podstępny sposób, ponieważ polegasz na tym, że właśnie został wywołany zipIn, aby uzyskać następny element strumienia - jeśli spróbujesz to zrobić ponownie, nie podczas pojedynczego przejścia przez strumień, ponieważ buffer zostanie nadpisany. Ale tutaj jest w porządku.

Jest więc: nieco bardziej kompaktowa, być może łatwiejsza do zrozumienia, być może mniej łatwa do zrozumienia metoda, która jest bardziej funkcjonalna (chociaż wciąż istnieją liczne efekty uboczne). W przeciwieństwie do tego, w 2.7.7 robiłbym to w Javie, ponieważ Stream.continually nie jest dostępny, a narzut na budowanie niestandardowego Iterator nie jest wart tego dla tego jednego przypadku. (Byłoby warto, jeśli miałem zamiar zrobić więcej przetwarzania pliku zip i może ponownie użyć kodu, jednak.)


Edycja: spojrzenie-na-available-to-go-Zero metoda jest rodzajem Łagodny w wykrywaniu końca pliku zip. Myślę, że "poprawnym" sposobem jest poczekanie, aż otrzymasz null z powrotem od getNextEntry. Mając to na uwadze, edytowałem poprzedni kod (był tam takeWhile(_ => zipIn.available==1), który jest teraz takeWhile(_ != null)) i dostarczono poniżej wersję 2.7.7 opartą na iteratorze (zauważ jak mała jest główna pętla, kiedy przejdziesz przez pracę definiującą te iteratory, które robią co prawda użyć 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

Dzięki, Rex, to bardzo niezła odpowiedź. – pr1001

+0

Dzięki za ciągłą sztuczkę – Patrick

+0

Ostatnia wersja tutaj z ZipIter ma poważny błąd. Wywołanie getNextEntry faktycznie przesuwa wskaźnik strumienia, więc twoja pozycja odnosi się do czegoś innego niż ustawiony strumień. Na przykład. jeśli masz A.txt B.txt otrzymasz wpis dla A.txt, ale czytasz B.txt, następnie dostajesz wpis dla B.txt i myślę, że nic nie czytasz. –

1

bez ogona rekursji by uniknąć rekursji. Wystąpiłoby ryzyko przepełnienia stosu. Możesz zawinąć zipIn.read(data) w scala.BufferedIterator[Byte] i przejść z tego miejsca.

+0

Ok ... Czy sugerujesz, że nie ma lepszego podejścia? – pr1001

+0

Przepraszam, zajęło mi kilka minut, aby coś wymyślić. – sblundy

+0

Hehe, wystarczy! – pr1001

2

Korzystanie scala2.8 i ogon wywołanie rekurencyjne:

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

Dzięki, to wygląda całkiem nieźle. Niestety powinienem był określić, że nadal jestem na 2.7.7. – pr1001

+0

Ponadto, jak to nastąpi w 2.7.7 kontra 2.8? Nie jestem zbyt biegły w kwestii rekurencji ogona. Dzięki. – pr1001

+1

@ pr1001 Scala 2.8 optymalizuj wywołanie ogona, jeśli to możliwe, aby uniknąć stackoverflow. Aby zapoznać się z tym, co jest ogonem, sugeruję przeczytanie tego wpisu na przykład: http://blog.richdougherty.com/2009/tail-calls-tailrec-and-trampolines.html – Patrick

2

chciałbym spróbować coś takiego (tak, prawie taki sam pomysł sblundy miał):

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) 

to może być uproszczone jak poniżej, ale nie bardzo to lubię. Wolałbym za read nie być w stanie powrócić 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

podstawie 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

Nie wydaje się, aby można było łatwo uzyskać dostęp do rzeczywistej zawartości pliku, ale ... co za bolesny interfejs –