Чтение ZIP-файла с использованием Java API от Clojure

Я пытаюсь переписать следующий фрагмент в ближайшем будущем, но все это выглядит ужасно, может быть, кто-то предложит более элегантное решение?

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class ZipFileRdrExp {

  public static void main(String[] args) {

    try {

      FileInputStream fis = new FileInputStream("C:\\MyZip.zip");
      ZipInputStream zis = new ZipInputStream(fis);
      ZipEntry ze;
      while((ze=zis.getNextEntry())!=null){
        System.out.println(ze.getName());
        zis.closeEntry();
      }

      zis.close();

    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Вот моя уродливая попытка с двойным вызовом getNextEntry:

(ns app.core
  (:import
  (java.io FileInputStream FileNotFoundException IOException File)
  (java.util.zip ZipInputStream ZipEntry)))


(defn- read-zip [zip-file]
  (let [fis (FileInputStream. zip-file)
        zis (ZipInputStream. fis)]
    (loop [ze (.getNextEntry zis)]
      (when ze
        (println (.getName ze))
        (.closeEntry zis)
        (recur (.getNextEntry zis))))
    (.close zis)))

5 ответов

Решение

Я хотел бы пойти с чем-то вроде следующего:

(defn entries [zipfile]
 (lazy-seq
  (if-let [entry (.getNextEntry zipfile)]
   (cons entry (entries zipfile)))))

(defn walkzip [fileName]
 (with-open [z (ZipInputStream. (FileInputStream. fileName))]
  (doseq [e (entries z)]
   (println (.getName e))
   (.closeEntry z))))

РЕДАКТИРОВАТЬ: приведенный выше код был в конечном итоге проверен и исправлен.

РЕДАКТИРОВАТЬ: следующее работает, как ожидалось, и это гораздо более кратким, хотя он использует другой API Java

(defn entries [zipfile]
  (enumeration-seq (.entries zipfile)))

(defn walkzip [fileName]
  (with-open [z (java.util.zip.ZipFile. fileName)]
             (doseq [e (entries z)]
                    (println (.getName e)))))

Это более простой пример:

(defn filenames-in-zip [filename]
  (let [z (java.util.zip.ZipFile. filename)] 
    (map #(.getName %) (enumeration-seq (.entries z)))))

Это похоже на приведенный выше код, но здесь нет причин использовать with-open. В этом примере возвращается последовательность данных, которую вы можете затем распечатать или, что еще лучше, отформатировать. Лучше иметь функцию, которая извлекает данные, просто возвращать данные, а не иметь побочный эффект печати, содержащийся внутри этой функции.

Если вы хотите распечатать содержимое, вы можете использовать

(pprint (filenames-in-zip "my.zip"))

и это даст вам хороший список.

Это похоже на ответ Скуро, который использует ZipInputStream, но чуть более краткое определение entries,

(defn entries [zip-stream]
  (take-while #(not (nil? %))
              (repeatedly #(.getNextEntry zip-stream))))

(defn walkzip [fileName]
  (with-open [z (ZipInputStream. (FileInputStream. fileName))]
             (doseq [e (entries z)]
                    (println (.getName e))
                    (.closeEntry z))))

Или, если вы действительно хотите извлечь файлы, вам нужна другая вспомогательная функция для копирования. Я использовал clojure.java.io для сокращения кода, но то же самое может быть достигнуто без этой зависимости.

(require '[clojure.java.io :as io])

(defn entries [zip-stream]
  (take-while #(not (nil? %))
              (repeatedly #(.getNextEntry zip-stream))))

(defn copy-file [zip-stream filename]
  (with-open [out-file (file-out-stream filename)]
             (let [buff-size 4096
                             buffer (byte-array buff-size)]
               (loop [len (.read zip-stream buffer)]
                     (when (> len 0)
                       (.write out-file buffer 0 len)
                       (recur (.read zip-stream buffer)))))))

(defn extract-stream [zip-stream to-folder]
  (let [extract-entry (fn [zip-entry]
                          (when (not (.isDirectory zip-entry))
                            (let [to-file (io/file to-folder
                                                   (.getName zip-entry))
                                          parent-file (io/file (.getParent to-file))]
                              (.mkdirs parent-file)
                              (copy-file zip-stream to-file))))]
    (->> zip-stream
      entries
      (map extract-entry)
      dorun)))

Это фактически эквивалентно простому разархивированию файла с unzip полезность. Прелесть этого в том, что поскольку записи находятся в ленивом следствии, вы можете filter или же drop или же take к вашему сердцу (или требованиям) контента. Ну, я уверен, что вы можете. На самом деле еще не пробовал:)

Также сделайте примечание. Вы ДОЛЖНЫ обработать последовательность внутри функции, в которой вы открываете поток zip!!!

Clojure-Contrib имеет библиотеки IO и Jar, которые делают код короче:

(require 'clojure.contrib.jar
         'clojure.contrib.io)

(import [java.util.jar JarFile])

(defn- read-zip [zip-file]
  (clojure.contrib.jar/filenames-in-jar (JarFile. (clojure.contrib.io/file zip-file))))

Предостережение: Функция filenames-in-jar не перечисляет записи каталога в zip-файле, только имена реальных файлов.

Мое предпочтительное решение - создать lazy-seqиз [#ZipEntry, #InputStream]из zip-файла.

      (defn lazy-zip
  "returns a lazy-seq of [entry inputstream] for a zip file

  The zipfile will be closed when the seq is exhausted. All processing has to be done transient through `map` or similar methods."
  [filename]
  (let [zf (java.util.zip.ZipFile. filename)]
    (letfn [(helper [entries]
              (lazy-seq
               (if-let [s (seq entries)]
                 (cons [(first entries)
                        (.getInputStream zf (first entries))]
                       (helper (rest entries)))
                 (do (println "closing zipfile") (.close zf) nil))))]
      (helper (->> zf (.stream) (.toArray))))))

Вот тест, который показывает использование:

      (deftest test-lazy-zip
  (testing "sample zip is read correctly"
    (is (=
         '(["sample.xml" "<?xml version=\"1.0\" encoding=\"UTF-8\">" "<foo>" "<bar>" "<baz>The baz value</baz>" "</bar>" "</foo>"])
         (map (fn [[entry reader]]
                (into [(.getName entry)]
                      (line-seq (stream-to-buffered-reader reader))))
              (lazy-zip "sample.xml.zip"))))))
Другие вопросы по тегам