Ruby解析gzip二进制字符串

4

我有一个二进制字符串,它包含两个gzip二进制文件的拼接。(我正在读取一个将两个gzip文件拼接在一起的二进制日志文件)

换句话说,我的情况相当于:

require 'zlib'
require 'stringio'

File.open('t1.gz', 'w') do |f|
  gz = Zlib::GzipWriter.new(f)
  gz.write 'part one'
  gz.close
end

File.open('t2.gz', 'w') do |f|
  gz = Zlib::GzipWriter.new(f)
  gz.write 'part 2'
  gz.close
end


contents1 = File.open('t1.gz', "rb") {|io| io.read }
contents2 = File.open('t2.gz', "rb") {|io| io.read }

c = contents1 + contents2

gz = Zlib::GzipReader.new(StringIO.new(c))

gz.each do | l |
    puts l
end

当我尝试解压合并的字符串时,只能得到第一个字符串。我该如何获取这两个字符串?


首先,有实际使用的代码会更有帮助,而不是一些近似的代码。其次,您是如何解压缩被压缩的数据的? - Frederick Cheung
@FrederickCheung 他正在使用GzipReader进行解压缩。而且这段代码可能是他的实际代码,只是没有不必要和混淆的业务逻辑。 - WattsInABox
4个回答

3
while c
  io = StringIO.new(c)
  gz = Zlib::GzipReader.new(io)
  gz.each do | l |
    puts l
  end
  c = gz.unused   # take unprocessed portion of the string as the next archive
end

请参阅 ruby-doc

1

gzip格式使用包含先前压缩数据校验和的页脚。一旦到达页脚,就不能再有相同gzip数据流的数据。

似乎Ruby Gzip reader在遇到第一个页脚后就停止读取,这在技术上是正确的,尽管许多其他实现如果仍有更多数据则会引发错误。我不太清楚Ruby在这里的确切行为。

关键是,您不能只是连接原始字节流并期望事情正常工作。您必须实际调整流并重写标题和页脚。有关详细信息,请参见this question

或者,您可以解压缩流,将它们连接起来并重新压缩它,但这显然会创建一些开销...


我并没有编写日志文件。我只是想要读取它。我想要解压已连接的两个gz文件。我希望避免创建第三个gz文件,这也是您链接的问题所涉及的。 - Tihom
1
@Tihom:根据http://en.wikipedia.org/wiki/Gzip,连接多个GZIP文件是完全有效的:“虽然它的文件格式也允许将多个这样的流串联起来(压缩文件只是解压缩串联在一起,就像它们最初是一个文件),…”当然,这与将文件压缩到一个GZIP存档中是不同的。 - undur_gongor
1
这个答案是不正确的。RFC 1952中的gzip规范明确指出,gzip流可以“只是”串联起来形成一个有效的gzip流,并且符合规范的解压器必须解压缩它们所有的内容。 - Mark Adler
仍然(至少在回答撰写时),Ruby会忽略第一个流后的任何尾随数据。 - Holger Just

0

接受的答案对我没用。这是我的修改版本。请注意gz.unused的不同用法。

此外,您应该调用GzipReader实例上的finish以避免内存泄漏。

# gzcat-test.rb
require 'zlib'
require 'stringio'
require 'digest/sha1'

# gzip -c /usr/share/dict/web2 /usr/share/dict/web2a > web-cat.gz
io = File.open('web-cat.gz')
# or, if you don't care about memory usage:
# io = StringIO.new File.read 'web-cat.gz'

# these will be hashes: {orig_name: 'filename', data_arr: unpacked_lines}
entries=[]
loop do
  entries << {data_arr: []}
  # create a reader starting at io's current position
  gz = Zlib::GzipReader.new(io)
  entries.last[:orig_name] = gz.orig_name
  gz.each {|l| entries.last[:data_arr] << l }
  unused = gz.unused  # save this before calling #finish
  gz.finish

  if unused
    # Unused is not the entire remainder, but only part of it.
    # We need to back up since we've moved past the start of the next entry.
    io.pos -= unused.size
  else
    break
  end
end

io.close

# verify the data
entries.each do |entry_hash|
  p entry_hash[:orig_name]
  puts Digest::SHA1.hexdigest(entry_hash[:data_arr].join)
end

运行:

> ./gzcat-test.rb
web2"
a62edf8685920f7d5a95113020631cdebd18a185
"web2a"
b0870457df2b8cae06a88657a198d9b52f8e2b0a

我们解压后的内容与原始内容匹配:
> shasum /usr/share/dict/web*
a62edf8685920f7d5a95113020631cdebd18a185  /usr/share/dict/web2
b0870457df2b8cae06a88657a198d9b52f8e2b0a  /usr/share/dict/web2a

0

这是确保整个文件被读取的正确方法。即使未使用可能为nil,也不意味着已经到达原始gzipped文件的末尾。

File.open(path_to_file) do |file|
  loop do
    gz = Zlib::GzipReader.new file
    puts gz.read

    unused = gz.unused
    gz.finish

    adjust = unused.nil? ? 0 : unused.length
    file.pos -= adjust
    break if file.pos == file.size
  end
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接