2010-05-17 13 views
16

Estoy tratando de leer un archivo zip, verificar que tenga algunos archivos necesarios y luego escribir todos los archivos válidos en otro archivo zip. El basic introduction to java.util.zip tiene muchos ismos de Java y me encantaría hacer que mi código sea más nativo de Scala. Específicamente, me gustaría evitar el uso de vars. Esto es lo que tengo:¿Cómo puedo evitar variables variables en Scala cuando uso ZipInputStreams y 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 

Debo agregar que estoy usando Scala 2.7.7.

+0

¿Por qué es nula de datos? – sblundy

+0

Porque estaba siendo flojo y 'new Array [Byte]' hace que el compilador se queje sobre constructores alternativos. Supongo que debería usar 'new ArrayBuffer [Byte]'. – pr1001

+0

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

Respuesta

34

D No creo que haya nada malo en particular con el uso de las clases de Java que están diseñados para trabajar de manera imprescindible de la manera que fueron diseñados. Idiomatic Scala incluye poder usar Java idiomático como estaba previsto, incluso si los estilos chocan un poco.

Sin embargo, si lo desea, quizás como ejercicio, o tal vez porque aclara un poco la lógica, para hacerlo de una manera más funcional y sin var, puede hacerlo. En 2.8, es particularmente agradable, así que aunque estés usando 2.7.7, daré una respuesta de 2.8.

En primer lugar, tenemos que plantear el problema, que no lo hizo del todo, pero vamos a suponer que tenemos algo como esto:

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 

Ahora, teniendo en cuenta esto queremos copiar el archivo zip. El truco que podemos usar es el método continually en collection.immutable.Stream. Lo que hace es realizar un bucle evaluado de forma lenta para ti. A continuación, puede tomar y filtrar los resultados para finalizar y procesar lo que desea. Es un patrón útil para usar cuando tiene algo que desea que sea un iterador, pero no lo es. (Si el artículo se actualiza solo puede usar .iterate en Iterable o Iterator --que suele ser incluso mejor.) Aquí está la aplicación a este caso, utilizada dos veces: una para obtener las entradas y una para leer/escribir fragmentos de datos:

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 

Preste mucha atención al . al final de algunas líneas! Normalmente escribiría esto en una línea larga, pero es más agradable tenerlo envuelto para que pueda verlo todo aquí.

Justo en caso de que no esté claro, desagilemos uno de los usos de continually.

Stream.continually(zipIn.read(buffer)) 

Esta pregunta que seguir llamando zipIn.read(buffer) para tantas veces como sea necesario, almacenar el entero resultante.

.takeWhile(_ != -1) 

Esto especifica cuántas veces son necesarias, volviendo una corriente de longitud indefinida, sino que será dejar de fumar cuando se realiza un -1.

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

Este procesa el flujo, teniendo cada elemento a su vez (la cuenta), y usarlo para escribir la memoria intermedia.Esto funciona de una manera un poco furtiva, ya que se basa en el hecho de que se ha llamado al zipIn para obtener el siguiente elemento de la transmisión: si intentaba hacer esto de nuevo, no en un solo paso a través de la secuencia, fallaría porque buffer se sobrescribirá. Pero aquí está bien.

Por lo tanto, ahí está: un método un poco más compacto, posiblemente más fácil de entender, posiblemente menos fácil de entender que sea más funcional (aunque todavía hay abundancia de efectos secundarios). En 2.7.7, en cambio, realmente lo haría de la manera Java porque Stream.continually no está disponible, y la sobrecarga de construir un Iterator personalizado no vale la pena en este caso. (Sería la pena si iba a hacer más procesamiento de archivos zip y podría reutilizar el código, sin embargo.)


Editar: El mirar-para-disponibles-a-go-cero método es una especie de escamosa para detectar el final del archivo comprimido. Creo que la forma "correcta" es esperar hasta que obtenga null de vuelta desde getNextEntry. Con eso en mente, he editado el código anterior (había un takeWhile(_ => zipIn.available==1) que ahora es un takeWhile(_ != null)) y proporcioné una versión basada en el iterador 2.7.7 a continuación (observe qué tan pequeño es el bucle principal, una vez que termina el trabajo de definir los iteradores, que sí es cierto utilizan 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

Gracias, Rex, eso es muy buena respuesta. – pr1001

+0

Gracias por el truco continuo – Patrick

+0

La última versión aquí con ZipIter tiene un error grave. Llamar a getNextEntry en realidad avanza el puntero de la secuencia, por lo que su entrada se refiere a algo diferente de lo que está configurado el flujo. P.ej. si tiene A.txt B.txt obtendrá la entrada para A.txt pero leerá B.txt, luego obtendrá la entrada para B.txt y creo que no lea nada. –

1

Sin repetición de cola, evitaría la recursión. Correrías el riesgo de un desbordamiento de pila. Puede envolver zipIn.read(data) en un scala.BufferedIterator[Byte] y continuar desde allí.

+0

Ok ... ¿Entonces está sugiriendo que no hay un mejor enfoque? – pr1001

+0

Lo siento, me tomó unos minutos pensar en algo. – sblundy

+0

Hehe, ¡es suficiente! – pr1001

2

Usando scala2.8 y la cola llamada recursiva:

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

Gracias, eso se ve bastante bien. Lamentablemente debería haber especificado que todavía estoy en 2.7.7. – pr1001

+0

Además, ¿cómo explotará esto en 2.7.7 contra 2.8? No estoy muy familiarizado con el tema de la recursividad de la cola. Gracias. – pr1001

+1

@ pr1001 Scala 2.8 optimiza la llamada de cola cuando sea posible para evitar el stackoverflow. Para una introducción a lo que es una llamada a la cola, le sugiero que lea esta entrada por ejemplo: http://blog.richdougherty.com/2009/04/tail-calls-tailrec-and-trampolines.html – Patrick

2

me gustaría probar algo como esto (sí, más o menos la misma idea sblundy tenía):

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) 

podría ser simplificado como el de abajo, pero no me gusta mucho. Yo prefiero para read no ser capaz de volver ... 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

Basado en 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

Esto no parece permitirle acceder fácilmente al contenido real del archivo aunque ... qué interfaz tan dolorosa –

Cuestiones relacionadas