【问题标题】:Reading a zip file using java api from clojure使用 clojure 中的 java api 读取 zip 文件
【发布时间】:2011-03-24 12:31:41
【问题描述】:

我正在尝试在clojure中重写sn-p,但这一切都很难看,也许有人会提出更优雅的解决方案?

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)))

【问题讨论】:

  • 有点遗憾的是,您的示例没有明确要求对ZipEntry 的内容做任何事情,只打印条目名称。恕我直言,ZipInputStream API 的更复杂和容易出错的细节是如何检索条目本身的InputStream,如何在条目之间正确迭代,最后如何正确关闭ZipInputStream。这也是不幸的,因为下面的所有答案都以一个简单且不那么令人费解但同时显着不那么有用的解决方案(即阅读内容)。

标签: java clojure


【解决方案1】:

我会选择以下内容:

(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))))

编辑:上述代码最终经过测试和更正。

编辑:以下内容按预期工作,并且更加简洁,即使它使用不同的 Java API

(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)))))

【讨论】:

  • 注意:如果 entries 被返回而不是立即消耗,with-open 将与惰性求值冲突并在读取完成之前关闭您的输入流,从而触发异常。
【解决方案2】:

这是一个更简单的例子:

(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"))

它会给你一个很好的清单。

【讨论】:

  • 虽然您不需要直接关闭 ZipFile,但 strongly advised 这样做是为了在您不再需要资源时立即释放资源。
  • 是的,关闭资源是个好主意。问题/示例非常简单,但实际上不保证添加额外的宏而不解释它的作用。
  • 使用with-open 代替let 将自动关闭ZipFile。
  • @BradKoch 但这会返回一个惰性序列,因此它具有您在 cmets 中描述的可接受答案的缺陷。您可以将mapdoall 包装在一起,这样压缩文件在关闭之前就被完全使用了,尽管实际上是将其全部内容放入内存中。
  • @omiel ,同意,应该同时处理文件关闭和惰性序列,这是一个需要处理的微妙情况。
【解决方案3】:

Clojure-Contrib 有库 IOJar,这使得代码更短:

(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不会列出压缩文件中的目录条目,只列出实际文件的名称。

【讨论】:

  • 在这里加入 contrib 并没有给你太多帮助。它并没有真正使代码更短,并且当你真的不需要时它会添加一个依赖项。
【解决方案4】:

这类似于使用 ZipInputStream 的 skuro 的答案,但对 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 实用程序简单地解压缩文件。它的美妙之处在于,由于条目位于惰性序列中,您可以 filterdroptake 到您心中(或要求)的内容。嗯,我很确定你可以。还没有真正尝试过:)

也请注意。您必须在打开 zip 流的函数内部处理 seq !!!

【讨论】:

  • 从表面上看,这个解决方案看起来很不错,但遗憾的是它有一个缺陷:-(entries 函数似乎在copy-file 开始工作之前第一次运行了两次。.getNextEntry移动指针在流中的位置第一个文件名获取第二个文件内容,最后一个文件为空。我花了一段时间才弄清楚。也许这就是take-while/repeatedly如何协同工作¯_ (ツ)_/¯
  • 有缺陷的行为可以这样模拟:``` (defn a [] (take-while #(not= 1 %) (repeatedly #(let [found (rand-int 3)] (println "found" found) found)))) (loop [[b & rest] (a)] (when b (println b) (recur rest))) ```
  • @Jacob 感谢 cmets,但是我无法重现您所描述的行为。我也不清楚你的有缺陷的行为模拟应该输出什么与它输出什么。也就是说,我很乐意用我的解决方案解决您的疑虑,但据我所知,它确实可以正常工作。
  • 这应该给你一个想法:repl.it/repls/BothPortlyInteger
  • 我的新示例是否为您阐明了问题?欢迎您直接联系我进行更直接的沟通。
【解决方案5】:

我的首选解决方案是从 zip 文件创建 lazy-seq[#ZipEntry, #InputStream]

(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"))))))

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-10
    • 1970-01-01
    • 2012-09-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多