使用 bash 逐一读取 zip 文件中的文件

Posted

技术标签:

【中文标题】使用 bash 逐一读取 zip 文件中的文件【英文标题】:read the files one by one in a zip file using bash 【发布时间】:2017-08-24 13:34:48 【问题描述】:

我想打开 .zip 文件中的文件并阅读它们。在这个 zip 文件中,我有许多 .gz 文件,例如 a.dat.gz、b.dat.gz 等。

到目前为止我的代码:

for i in $(unzip -p sample.zip)
do
    for line in $(zcat "$i")
    do
        # do some stuff here
    done
done

【问题讨论】:

请详细说明“没用”。 @JohnnyRockex – 您的编辑改变了问题。我不得不将它们放回去以了解这个问题所询问的嵌套存档结构。您可以在我的答案的past iteration 中看到我的错误假设。 只是想知道,@JohnnyRockex 编辑是如何被确认的,我没有这样做;' 小伙伴们,也许是矩阵中的一个小故障,但我所做的编辑很少,最新的修订版仍然有拼写错误:D Johnny 提到的版本有一个小的语法错误(“file”代替了“files”),但没有拼写错误。我现在也解决了这个问题。 @ggupta – Stack Exchange 站点使用同行评审系统对信誉低于 2000 的用户(称为edit privilege)进行编辑,要求 (iirc) 拥有 2000+ 信誉的 2+ 用户批准它才能被接受。约翰尼和我都有这个特权,所以同行评审系统被绕过了。 (所以我猜“the matrix”在约翰尼的脑海里。幸运的家伙!) 【参考方案1】:

您需要两个循环是正确的。首先,您需要存档中的文件列表。然后,您需要在每个文件中进行迭代。

unzip -l sample.zip |sed '
  /^ *[0-9][0-9]* *2[0-9-]*  *[0-9][0-9]:[0-9][0-9]  */!d; s///
' |while IFS= read file
  unzip -p sample.zip "$file" |gunzip -c |while IFS= read line
    # do stuff to "$line" here
  done
done

这假定 zip 存档中的每个文件本身都是一个 gzip 存档。否则你会从 gunzip 中得到一个错误。

代码漫游

unzip -l archive.zip 将列出内容。它的原始输出如下所示:

Archive:  test.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        9  2017-08-24 13:45   1.txt
        9  2017-08-24 13:45   2.txt
---------                     -------
       18                     2 files

因此我们需要解析它。我选择使用 sed 进行解析是因为它快速、简单并且可以正确保留空格(如果您的文件名称中包含制表符怎么办?)注意,如果文件中有换行符,这将不起作用。不要那样做。

sed 命令使用正则表达式 (explanation here) 来匹配包含文件名的所有行,但文件名本身除外。当匹配器触发时,sed 被告知不要删除 (!d),这实际上是告诉 sed 跳过任何不匹配的内容(如标题行)。第二个命令s/// 告诉 sed 用空字符串替换先前匹配的文本,因此输出是每行一个文件名。这会以$file 的形式传送到一个while 循环中。 (read 之前的 IFS= 部分可防止从两端剥离空格,请参见下面的 cmets。)

然后我们可以只解压缩我们正在迭代的文件,再次使用unzip -p 将其打印到标准输出,这样它就可以作为$line 存储在内部while 循环中。

实验简化

我不确定这有多可靠,但您可以更简单地做到这一点:

unzip -p sample.zip |gunzip -c |while read line
  # do stuff to "$line"
done

应该起作用,因为unzip -p archive 会吐出存档中每个文件的内容,所有文件都连接在一起,没有任何分隔符或元数据(如文件名)因为 gzip 格式接受连接在一起的存档(请参阅my notes on concatenated archives),所以gunzip -c 管道命令会看到原始 gzip 数据并在控制台上将其解压缩,然后将其传递到 shell 的 while 循环。在这种方法中,您将缺少文件边界和名称,但它要快得多。

【讨论】:

这段代码unzip -p sample.zip |gunzip -c |while read line # do stuff to "$line" done,太好了,只是想知道 gunzip -c 到底在做什么?提前致谢 @ggupta – 我为答案添加了更多解释。您将 zip 存档描述为包含多个 gzip 压缩的存档,因此我们需要先解压缩 zip,然后再解压缩 gzip。 gunzip -c 执行第二次解压缩。 while read file 不能正确表示所有文件名。以空格开头或结尾的名称或包含文字反斜杠的名称将被歪曲。 (另外,unzip -l 的输出格式没有特别明确,因此依赖它是一个值得商榷的选择——事实上,它被明确记录为具有不同于您的输出格式'重新假设何时给出了一些可选的编译时标志;并且文档没有指定日期格式 &c. 是否跨区域设置)。 @AdamKatz, IFS= read -r line 对处理前后空格大小写和文字反斜杠的侵入性较小。但是,不仅仅是换行符仍然是一个问题 - zip 以不同于名称在输出中的字面意思出现的方式转义列表中的文件(因此在文件名中的换行符示例中,zip -l将它们显示为^J;我确信它对于其他不可打印的字符还有其他非文字转义)。【参考方案2】:

这比你想象的要在 shell 中稳健地完成要困难得多。 (现有答案在常见情况下有效,但包含令人惊讶的文件名的档案会混淆它)。更好的选择是使用具有本机 zip 文件支持的语言——例如 Python。 (这还具有不需要多次打开输入文件的优点!)

如果单个文件足够小,您可以在内存中放置每个文件的几个副本,则以下内容将很好地工作:

read_files() 
  python -c '
import sys, zipfile, zlib

zf = zipfile.ZipFile(sys.argv[1], "r")
for content_file in zf.infolist():
    content = zlib.decompress(zf.read(content_file), zlib.MAX_WBITS|32)
    for line in content.split("\n")[:-1]:
        sys.stdout.write("%s\0%s\0" % (content_file.filename, line))
' "$@"


while IFS= read -r -d '' filename && IFS= read -r -d '' line; do
  printf 'From file %q, read line: %s\n' "$filename" "$line"
done < <(read_files yourfile.zip)

如果您真的想将文件列表和文件读取操作彼此分开,那么稳健地执行此操作可能如下所示:

### Function: Extract a zip's content list in NUL-delimited form
list_files() 
  python -c '
import sys, zipfile, zlib

zf = zipfile.ZipFile(sys.argv[1], "r")
for content_file in zf.infolist():
    sys.stdout.write("%s\0" % (content_file.filename,))
' "$@"


### Function: Extract a single file's contents from a zip file
read_file() 
  python -c '
import sys, zipfile, zlib

zf = zipfile.ZipFile(sys.argv[1], "r")
sys.stdout.write(zf.read(sys.argv[2]))
' "$@"


### Main loop
process_zip_contents() 
  local zipfile=$1
  while IFS= read -r -d '' filename; do
    printf 'Started file: %q\n' "$filename"
    while IFS= read -r line; do
      printf '  Read line: %s\n' "$line"
    done < <(read_file "$zipfile" "$filename" | gunzip -c)
  done < <(list_files "$zipfile")

要对上述内容进行烟雾测试——如果输入文件创建如下:

printf '%s\n' '1: line one' '1: line two' '1: line three' | gzip > one.gz
printf '%s\n' '2: line one' '2: line two' '2: line three' | gzip > two.gz
cp one.gz 'name
with
newline.gz'
zip test.zip one.gz two.gz $'name\nwith\nnewline.gz'
process_zip_contents test.zip

...那么我们有以下输出:

Started file: $'name\nwith\nnewline.gz'
  Read line: 1:line one
  Read line: 1:line two
  Read line: 1:line three
Started file: one.gz
  Read line: 1: line one
  Read line: 1: line two
  Read line: 1: line three
Started file: two.gz
  Read line: 2: line one
  Read line: 2: line two
  Read line: 2: line three

【讨论】:

以上是关于使用 bash 逐一读取 zip 文件中的文件的主要内容,如果未能解决你的问题,请参考以下文章

如何在不使用临时文件的情况下从 Java 中的嵌套 zip 文件中读取数据?

从 Zip 文件中的文件中读取内容

使用 Haskell 的 zip-conduit 从 zip 存档中的文件中读取行

如何使用 python 从位于同一目录中的多个 zip 文件夹中读取 csv 文件?

在 Powershell 中读取 ZIP 中的文本文件

如何使用 spark(python)读取 zip 文件中的 CSV 文件的内容 [重复]