使用 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 文件中读取数据?
使用 Haskell 的 zip-conduit 从 zip 存档中的文件中读取行