使用 bash 一个一个地读取 zip 文件中的文件

read the files one by one in a zip file using bash

我想打开 .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

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

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= 部分防止从两端删除空格,请参阅下面的注释。)

然后我们可以只解压缩我们正在迭代的文件,再次使用 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 循环。在这种方法中,您将缺少文件边界和名称,但速度要快得多。

这比您想象的要在 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[=10=]%s[=10=]" % (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[=11=]" % (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=
  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